scala - 什么时候应该使用 Kleisli?

标签 scala haskell functional-programming monads

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

  • 组合返回 monad 的函数 : f: a -> m[b]g: b -> m[c] - 我认为 monad 的定义已经捕获了这种情况 - do/bind/for/flatMap去做。不需要依靠 Kleisli 结构来实现这一目标。所以这不可能是 Kleisli IMO 的“主要”用例。
  • 插入配置 : 这个声明如果多个对象(类型、案例/数据类等)需要有一个 Config注入(inject),然后可以使用 Kleisli 构造来抽象出可重复的注入(inject)。有很多方法可以实现这一点(例如在 Scala 中使用 implicit s),可能不需要调用 Kleisli。同样,IMO 这并不是一个“主要”用例。
  • Monad 变形金刚:我对此没有扎实的理解,但这是我的解释:如果您需要“组合 monad”,则需要一个允许您参数化 monad 本身的构造。例如 M1[M2[M1[M2[a]]]]可以转换为 [M1[M2[a]]]可以(我可能错了)跨单子(monad)边界展平以与 a -> M3[b] 组合(说)。对于这个,我们可以使用 Kleisli 三元组并调用该构造,因为如果您要从头开始,您可能只是重新发明 Kleisli。这似乎是证明使用 Kleisli 合理性的一个很好的候选者。这个对吗?

  • 我相信#1-#2以上是“次要用途”。也就是说,如果您碰巧使用了 Kleisli 构造,您还可以获得组合返回 monad 的函数以及配置注入(inject)的模式。但是,它们不能成为倡导 Kleislis 力量的动机问题。

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

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

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

    最佳答案

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

    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,那么我可以跳过 Kleisli(...) for理解的一部分。
    val program: Kleisli[IO, Config, Result] = for {
      db <- makeDB
      http <- makeHttp
      cache <- makeCache
      ...
    } yield someResult
    

    这可能是流行的另一个原因:无标签决赛和 MTL。您可以定义您的函数以某种方式使用 Config运行并使其成为契约(Contract),但未指定 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.IOmonix.eval.Task .你结合了几个单子(monad)变压器,例如ReaderTStateTEitherT .或 2 个不同的 Kleisli/ReaderT注入(inject) 2 个不同的依赖项。因为Kleisli/ReaderT是“只是简单的类型”,您可以与其他 monad 组合,您可以根据需要将东西堆叠在一起。使用无标记 final 和 MTL,您可以将程序的声明性要求与您定义将要使用的实际类型的部分分开,您可以在其中写下每个组件需要工作的内容(然后能够与扩展方法一起使用) ,并且您可以从更小、更简单的构建 block 构建它。

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

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

    至于你对范畴论的关心Kleisli is

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



    但是,我无法指出许多每天都在使用它并为这种动机而烦恼的程序员。至少我个人不知道有人将其视为“偶尔有用的实用程序”。

    关于scala - 什么时候应该使用 Kleisli?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/61899370/

    相关文章:

    mongodb - 不坚持 Scala None's 而不是坚持为空值

    scala - 22 程序的列限制

    haskell - 编写具有通用量化返回类型的函数

    haskell - 如何测试 Applicatives 上的多态函数?

    scala - 链式 + Scala 选项

    scala - Zeppelin 集群模式不适用于 spark 1.2 Ambari、Hortonworks Cluster

    用于桌面的 Scala

    haskell - `[ (x !! 0, x !! 1) | x <- mapM (const [' A', 'B' , 'C' ] ) [1..2], head x < head (tail x) ]` 是如何工作的?

    f# - F#中两种偏应用方式的区别

    .net - 受歧视的联盟和 let 绑定(bind)