【问题标题】:How can I access the context in every function of a call chain with Kleisli?如何使用 Kleisli 访问调用链的每个函数中的上下文?
【发布时间】:2020-06-15 14:21:19
【问题描述】:

我有一些方法的调用链,我通过 Kleisli 传递上下文。基本上我想将一个上下文传递给数据库访问层,但我想在两者之间的任何地方访问这个上下文。

以下示例完美运行。不过我的问题是,我也想访问OrderService.findAll(...) 中的上下文。我尝试了几种方法,但总是失败。

object OrderRepository {
    fun findAll(userId: String): Kleisli<Context, ForIO, List<Order>> =
        Kleisli { ctx ->
            IO {
                ctx.db.query("someQuery")
            }
        }
}

object OrderService {
    fun findAll(userId: String): Kleisli<Context, ForIO, List<OrderResponse>> =
         OrderRepository.findAll(userId).map(IO.monad()) { orderList ->
            orderList.map {
                //Create OrderResponse from Order
            }
        }
}

是否可以访问那里的上下文,或者这没有任何意义?感谢您的帮助:)

【问题讨论】:

    标签: kotlin functional-programming arrow-kt


    【解决方案1】:

    @brewcode @bob 在 FP 中有一个高于 MTL 等编码的高阶模式,它是定界的延续,是所有 monad 的母亲。这包含了整个 Functor 层次结构的命令式语法,包括像阅读器这样的单子,通过延续将应用程序语法引入环境。这就是 kotlin 的暂停。

    @Jorge Castillo 的问题是正确的,但是任何支持延续的语言都比以分配成本包装数据类型具有更好的抽象。

    对于 JS 和 TS 而不是 Haskell 之类的编码,ReaderT 或任何回调样式编码,如 Scala 和 Haskell 提出的一种基于定界延续的编码,如果这些语言中有一种方法对初学者来说会更干净,更容易使用比 ReaderT 建议的包装回调样式。查看下面的程序,看看它是否比包装版本有任何缺点。

    interface OrderApi
    
    interface OrderDB {
      fun query(query: String): List<Order> = TODO()
    }
    
    data class Order(val id: String)
    
    data class OrderResponse(val order: Order)
    
    data class Context(
      val api: OrderApi,
      val repository: OrderRepository,
      val db: OrderDB) : OrderApi by api, OrderRepository by repository, OrderDB by db
    
    interface OrderRepository {
      suspend fun Context.findAll(userId: String): List<Order> =
        query("someQuery")
    }
    
    object OrderService {
      suspend fun Context.findAll(userId: String): List<OrderResponse> =
        findAll(userId).map(::OrderResponse)
    }
    

    【讨论】:

      【解决方案2】:

      您需要的是从 D 到 D 的 Kleisli,以 D 作为上下文。这样,您也可以将 D(上下文)作为结果类型,并且您可以使用 flatMap 并访问它。这就是为什么 ask() 方法提供的原因,可通过同伴获得。

      假设您的OrderRepository 也是上下文中的依赖项,而不是纯函数(为了示例),因此您需要从服务的上下文中访问它。见:

      interface OrderApi
      interface OrderDB {
        fun query(query: String): List<Order> = TODO()
      }
      
      data class Order(val id: String)
      data class OrderResponse(val order: Order)
      data class Context(val api: OrderApi, val repository: OrderRepository, val db: OrderDB)
      
      class OrderRepository {
        fun findAll(userId: String): Kleisli<Context, ForIO, List<Order>> =
          Kleisli { ctx ->
            IO {
              ctx.db.query("someQuery")
            }
          }
      }
      
      object OrderService {
        fun findAll(userId: String): Kleisli<Context, ForIO, List<OrderResponse>> {
          val monad = IO.monad()
          return Kleisli.ask<Context, ForIO>(monad).flatMap(monad) { ctx ->
            ctx.repository.findAll(userId).map(monad) { orderList ->
              orderList.map { OrderResponse(it) }
            }
          }
        }
      }
      

      也就是说,Kleisli 是一个 Monad 转换器(也称为 ReaderT),使用起来可能有点复杂。如果您想在功能代码库上注入依赖项并保持更简单,我的建议是通过 Context 接收器使用扩展函数,它已经隐式地跨所有级别传递您的依赖项,这在 on this post by Paco 中进行了描述。

      【讨论】:

      • 哇,感谢您的帮助。那工作得很好。顺便说一句,你的文章很棒,我从中学到了很多。
      • 感谢您的客气话 :) 我刚刚编辑了答案以在最后添加一个可能更好的建议。
      • 请注意,ReaderT 是函数式世界中众所周知的常见模式,而 Context 接收器上的扩展函数 是 Kotlin 特有的。我认为 FP 的优势之一是能够跨语言共享通用模式。
      • @bob 在 FP 中,在 MTL 之类的编码类型之上有一个更高阶的模式,它是定界的延续,是所有 monad 的母亲。这包含了整个 Functor 层次结构的命令式语法,包括像阅读器这样的单子,通过延续将应用程序语法引入环境。这就是 kotlin 的暂停。对于 JS 和 TS,Continuations 可能是更好的抽象,而不是像 ReaderT 那样的 Haskell 编码或 Scala 和 Haskell 提出的任何回调样式编码。
      • @RaúlRajaMartínez Delimited continuations 确实非常有趣。我不知道 Kotlin 使用了这个概念。我知道非 Haskell 社区对 monad 持怀疑态度。然而,monad 和它们的堆栈毕竟基本上只是关于在有效上下文中的函数应用。我不认为这一定比可组合的延续更难。但是,我会记住这一点。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-10-20
      • 2012-10-16
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多