什么时候应该使用 Kleisli?

PhD*_*PhD 35 monads haskell functional-programming scala

我最近偶然发现了 Kleisli 的概念,我阅读的每个教程/链接/参考都通过以下结构激发了 Kleisli 的使用:

  1. 组合返回 monad 的函数f: a -> m[b]with g: b -> m[c]- 我认为monad 的定义已经捕捉到了这种情况 -do/bind/for/flatMap这样做。人们不必依靠 Kleisli 构造来实现这一目标。所以这不可能是 Kleisli IMO 的“主要”用例。
  2. 插入配置:该配置指出,如果多个对象(类型、案例/数据类等)需要Config 注入,则可以使用 Kleisli 构造来抽象出可重复注入。有很多方法可以实现这一点(例如implicit在 Scala 中使用s),可能不需要调用 Kleisli。同样,IMO 这并没有作为“主要”用例脱颖而出。
  3. Monad Transformers:我对此没有深入的理解,但这是我的解释:如果您需要“组合 monad”,您需要一个允许您参数化 monad 本身的构造。例如M1[M2[M1[M2[a]]]],可以转化成[M1[M2[a]]](I可能是错误的)来跨越一元边界压扁是可组合与a -> M3[b](比方说)。为此我们可以使用 Kleisli 三元组并调用该构造,因为如果您要从头开始,您可能会重新发明Kleisli。似乎是证明使用 Kleisli 合理的一个很好的候选者。这样对吗?

我相信#1-#2以上是“二次用途”。也就是说,如果您碰巧使用了 Kleisli 构造,您可以获得用于组合返回 monad 和配置注入的函数的模式。但是,它们不能被激励问题崇尚Kleislis的力量。

在使用最不强大的抽象来解决手头问题的假设下,可以用什么激励问题来展示它们的用途?

替代论点:我完全错了,我对 Kleislis 的理解完全有可能是错误的。我缺乏必要的范畴论背景,但Kleisli可能是一个正交结构,可以用来代替单子,它们(Kleisli)是一个范畴论镜头,我们通过它来看待功能世界的问题(即, Klesli 简单地包装了一个 monadic 函数a -> M[b],现在我们可以在更高的抽象级别上工作,其中函数是操作对象而不是使用对象)。因此,使用 Kleisli 可以简单地理解为“使用 Kleisli进行函数式编程”。如果这是真的,那么应该有在这种情况下,Kleisli 可以比现有结构更好地解决问题,我们又回到了激励问题的问题上。同样有可能的是,如果它只是一个为同一问题提供不同解决方案的镜头,那么它本身并不存在这样一个激励性的问题。是哪个?

获得一些能够重建对 Kleislis 的需求的输入会非常有帮助。

Mat*_*zok 14

Kleisli 又名 ReaderT从实践的角度 #2(以及我稍后展示的 #3) - 将一个相同的组件依赖注入到多个函数中。如果我有:

val makeDB: Config => IO[Database]
val makeHttp: Config => IO[HttpClient]
val makeCache: Config => IO[RedisClient]
Run Code Online (Sandbox Code Playgroud)

然后我可以通过这种方式将事物组合为 monad:

def program(config: Config) = for {
  db <- makeDB(config)
  http <- makeHttp(config)
  cache <- makeCache(config)
  ...
} yield someResult
Run Code Online (Sandbox Code Playgroud)

但是手动传递东西会很烦人。因此,我们可以将那Config =>部分作为类型,并在没有它的情况下进行我们的 monadic 组合。

val program: Kleisli[IO, Config, Result] = for {
  db <- Kleisli(makeDB)
  http <- Kleisli(makeHttp)
  cache <- Kliesli(makeCache)
  ...
} yield someResult
Run Code Online (Sandbox Code Playgroud)

如果我所有的函数首先都是 Kleisli,那么我就可以跳过理解的那Kleisli(...)部分。

val program: Kleisli[IO, Config, Result] = for {
  db <- makeDB
  http <- makeHttp
  cache <- makeCache
  ...
} yield someResult
Run Code Online (Sandbox Code Playgroud)

这还有另一个可能流行的原因:无标签决赛和 MTL。您可以定义您的函数以某种方式用于Config运行并使其成为契约,但无需指定F[_]您究竟拥有的方式和类型:

import cats.Monad
import cats.mtl.ApplicativeAsk

// implementations will summon implicit ApplicativeAsk[F, Config]
// and Monad[F] to extract Config and use it to build a result
// in a for comprehension
def makeDB[F[_]: Monad: ApplicativeAsk[*, Config]]: F[Database]
def makeHttp[F[_]: Monad: ApplicativeAsk[*, Config]]: F[HttpClient]
def makeCache[F[_]: Monad: ApplicativeAsk[*, Config]]: F[RedisClient]

def program[F[_]: Monad: ApplicativeAsk[*, Config]]: F[Result] = for {
  db <- makeDB
  http <- makeHttp
  cache <- makeCache
  ...
} yield result
Run Code Online (Sandbox Code Playgroud)

如果您定义type F[A] = Kleisli[IO, Cache, A]并提供必要的隐式(此处:Monad[Kleisli[IO, Cache, *]]ApplicativeAsk[Kleisli[IO, Cache, *], Cache]),您将能够以与前面使用 Kleisli 的示例相同的方式运行此程序。

但是,您可以切换cats.effect.IOmonix.eval.Task. 您组合了几个 monad 转换器,例如ReaderTandStateTEitherT。或者 2 个不同的Kleisli/ReaderT注入 2 个不同的依赖项。而且因为Kleisli/ReaderT是“简单类型”,您可以与其他 monad 组合,因此您可以根据需要将内容堆叠在一起。使用无标记 final 和 MTL,您可以将程序的声明性需求与您定义将使用的实际类型的部分分开,其中您写下每个组件需要工作的内容(然后能够使用扩展方法) ,并且您可以从更小、更简单的构建块构建它们。

据我所知,这种简单性和可组合性是许多人使用 Kleisli 的原因。

也就是说,在这种情况下有其他方法来设计解决方案(例如,ZIO 以不需要 monad 转换器的方式定义自己),而许多人只是按照不需要任何 monad 转换器的方式编写代码 -喜欢。

至于你对范畴论的关注,Kleisli 是

问题的两个极值解决方案之一“每个 monad 都来自一个附属吗?”

但是,我无法指出许多每天都在使用它并且根本不关心这种动机的程序员。至少我个人不认识任何人将其视为“偶尔有用的实用程序”。


dup*_*ode 10

初步说明:这是一个非常以 Haskell 为中心的答案。

在 #1 上,luqui 的评论说得非常好:

Kleisli 不是什么大的重要的东西,需要有强烈的使用动机。这只是当周围有一个 monad 时你可以倾斜头部的一种方式。

如果你有一些链式绑定......

m >>= f >>= g >>= h
Run Code Online (Sandbox Code Playgroud)

... 结合性 monad 定律允许您将它们重写为...

m >>= \a -> f a >>= \b -> g b >>= \c -> h c
Run Code Online (Sandbox Code Playgroud)

……或者,等价地……

m >>= (f >=> g >=> h)
Run Code Online (Sandbox Code Playgroud)

... 其中(>=>)是执行 Kleisli 组合的运算符:

(>=>)       :: Monad m => (a -> m b) -> (b -> m c) -> (a -> m c)
f >=> g     = \x -> f x >>= g
Run Code Online (Sandbox Code Playgroud)

除了为我们提供比使用 bind 更好的 monad 定律之外(>=>)偶尔也是一种编写 monadic 计算的符合人体工程学的方式。我能想到的一个例子是xml-conduit库;例如,以下片段取自Yesod 书的一章

main :: IO ()
main = do
    doc <- readFile def "test2.xml"
    let cursor = fromDocument doc
    print $ T.concat $
        cursor $// element "h2"
               >=> attributeIs "class" "bar"
               >=> precedingSibling
               >=> element "h1"
               &// content 
Run Code Online (Sandbox Code Playgroud)

在这里,XML 轴实现为 list monad Kleisli arrows。在这种情况下,使用(>=>)组合它们而不明确提及它们被应用到什么感觉非常自然。


在 #2 上,在您的问题和Mateusz Kubuszok 的回答之间,我刚刚了解到一些相关的以 Scala 为中心的文档识别ReaderTKleisli基于两者都具有Monad m => a -> m b它们的基础类型。冒着通过草率判断的风险,我会说这样的识别让人感觉不舒服。ReaderTKleisli表达不同的概念,即使它们的实现在某种意义上是一致的。特别是,通过(>=>)Category实例Kleisli完成的组合类型没有意义ReaderT,因为ReaderT表达了对固定环境的依赖,这避免了将所述环境链接转换的概念。


在 #3 上,我认为这仅与Kleisli. 的问题时的单子导致单子组成相关事项有关的单子变压器不是通过诉诸解决了Kleisli箭头。虽然在处理此类问题时考虑 Kleisli 箭头和 Kleisli 类别有时很有用,但我认为这主要是因为 Kleisli 箭头和类别通常是考虑 monad 的有用方法,而不是因为某些更具体的联系。