什么时候应该使用 Kleisli?

When should one use a Kleisli?

我最近偶然发现了 Kleisli 的概念,我阅读的每一篇 tutorial/link/reference 都通过以下结构激发了 Kleisli 的使用:

  1. 组合 return monads 的函数:f: a -> m[b]g: b -> m[c] - 我认为非常 定义 的 monad 已经捕获了这种情况 - do/bind/for/flatMap 做到这一点。人们不需要依靠 Kleisli 结构来实现这一目标。所以这不可能是 Kleisli IMO 的 "primary" 用例。
  2. Inserting configuration: 这说明如果多个对象(类型,case/data 类等)需要有一个Config injected 然后可以使用 Kleisli 构造来抽象掉可重复注入。有许多方法可以实现这一点(例如在 Scala 中使用 implicits)可能不需要调用 Kleisli。同样,IMO 这并不突出作为一个 "primary" 用例。
  3. Monad 变形金刚: 我对此没有很透彻的理解,但这是我的解释:如果你需要 "composing monads" 你 需要 一个允许您参数化 monad 本身的结构。例如 M1[M2[M1[M2[a]]]] 可以转换为 [M1[M2[a]]] ,其中 可以 (我可能错了) 跨越单子边界 扁平化为可与 a -> M3[b] (比方说)组合。对于这个 可以 使用 Kleisli 三元组并调用构造,因为如果你要从头开始做,你可能只是 重新发明 Kleisli。 似乎是证明使用 Kleisli 的一个很好的候选者。这是正确的吗?

我相信#1-#2以上是"secondary uses"。也就是说,如果您确实碰巧使用了 Kleisli 构造,您可以 获得组合函数的模式 return 单子以及配置注入。但是,它们不能激发问题 提倡 Kleislis 的力量。

假设使用最不强大的抽象来解决手头的问题,哪些激励的问题可以用来展示他们的使用?

备选论文:完全有可能我完全错了,我对 Kleislis 的理解是不正确的。我缺乏必要的范畴论背景,但是 可能 Kleisli 是一个 正交构造 可以用来代替 monad 并且它们( Kleisli)是一个范畴论镜头,通过它我们可以查看函数世界的问题(即,Klesli 简单地包装了一个单子函数 a -> M[b] 现在我们可以在更高的抽象层次上工作,其中函数是操纵使用 的对象)。因此,Kleisli的使用可以简单理解为“Functional Programming with Kleisli”。如果这是真的,那么 应该 是 Kleisli 可以解决问题的情况比现有结构更好,我们回到这个问题激励问题。如果只是一个 lens 为同一问题提供 不同 解决方案,那么它本身也可能不存在这样一个激励问题。是哪个?

获得一些输入能够重建对 Kleislis 的需求真的很有帮助。

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

val makeDB: Config => IO[Database]
val makeHttp: Config => IO[HttpClient]
val makeCache: Config => IO[RedisClient]

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

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

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

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

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

val program: Kleisli[IO, Config, Result] = for {
  db <- makeDB
  http <- makeHttp
  cache <- makeCache
  ...
} yield someResult

这可能会流行的另一个原因是:无标签最终版和 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

如果您定义 type F[A] = Kleisli[IO, Cache, A] 并提供必要的隐含函数(此处:Monad[Kleisli[IO, Cache, *]]ApplicativeAsk[Kleisli[IO, Cache, *], Cache]),您将能够 运行 这个程序与之前的程序相同Kleisli 的例子。

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

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

就是说,在这种情况下有设计解决方案的替代方法(例如,ZIO 以不需要 monad 转换器的方式定义自己),而许多人只是简单地以不会产生它们的方式编写代码需要任何类似 monad 转换器的东西。

至于你对范畴论的担忧Kleisli is

one of two extremal solutions to the question "Does every monad arise from an adjunction?"

然而,我无法指出许多每天都在使用它并为这种动机而烦恼的程序员。至少我个人不认识任何人将此视为 "occasionally useful utility".

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

在#1 上, 说得非常好:

Kleisli is not some big important thing that needs to be strongly motivated to use. It's just a way you can tilt your head when there's a monad around.

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

m >>= f >>= g >>= h

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

m >>= \a -> f a >>= \b -> g b >>= \c -> h c

...或者,等价地...

m >>= (f >=> g >=> h)

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

(>=>)       :: Monad m => (a -> m b) -> (b -> m c) -> (a -> m c)
f >=> g     = \x -> f x >>= g

除了给我们 a nicer presentation of the monad laws than the one using bind, (>=>) is occasionally an ergonomic way to write monadic computations. One illustration I can think of is the xml-conduit library; for instance, the following snippet was taken from a chapter of the Yesod book:

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 

此处,XML 轴已实现 as list monad Kleisli arrows。在这种情况下,使用 (>=>) 来组合它们而不明确提及它们的应用感觉非常自然。


关于 #2,在你的问题和 , I have just learned that some of the relevant Scala-centric documentation identifies ReaderT and Kleisli on the basis of both having Monad m => a -> m b as their underlying type. Still, I'd say a clearer picture is obtained by thinking of ReaderT and Kleisli as expressing different concepts, even if their implementations happen to coincide in some sense. In particular, the kind of composition done through (>=>) or the Category instance of Kleisli 之间,就 ReaderT 的典型使用方式(即表达对固定环境的依赖性)而言,感觉很陌生。


关于#3,我认为这仅与 Kleisli 无关。 and related matters about monad transformers 的问题无法通过求助于 Kleisli 箭头来解决。虽然在处理此类问题时考虑 Kleisli 箭头和 Kleisli 类别有时很有用,但我想说这主要是因为 Kleisli 箭头和类别通常是考虑 monad 的有用方法,而不是因为一些更具体的联系。

有时我们可能希望以一种表达能力较差的方式构建计算,比完整的 Monad 接口更 "rigid",但也可能更易于检查。 Kleislis 可用于将单子效应嵌入到这些计算中。

例如,假设我们正在构建计算管道,其中每个步骤都附有某种注释。注释可以表示对完成该步骤的时间的估计,或与该步骤相关的一些其他资源的估计。我们希望能够在实际 "running" 其效果之前检查整个管道的累积注释:

import Prelude hiding (id,(.))
import Control.Category (Category,(.),id)
import Control.Arrow (Kleisli (..))

data Step w m i o = Step w (Kleisli m i o) 

instance (Monoid w, Monad m) => Category (Step w m) where
    id = Step mempty (Kleisli pure)
    (.) (Step wf f) (Step wg g) = Step (wg <> wf) (f . g)

投入使用:

main :: IO ()
main = do
    let Step w (Kleisli _) = 
              Step "b" (Kleisli putStrLn) 
            . Step "a" (Kleisli (\() -> getLine))
    putStrLn w
    -- result: ab