Skip to main content
 首页 » 编程设计

scala之Scala 中的 Future 是一个 monad 吗

2024年05月22日43JeffreyZhao

为什么 Scala Future 不是 Monad?有人可以将它与 Monad 的东西进行比较,例如 Option 吗?

我问的原因是 Daniel Westeide 的 The Neophyte's Guide to Scala Part 8: Welcome to the Future在那里我问 Scala Future 是否是 Monad,作者回答说它不是,这不合时宜。我是来求证的。

请您参考如下方法:

先总结一下

如果您从不使用有效块(纯内存计算)构造 Future,或者如果生成的任何效果不被视为语义等价的一部分(如日志消息),则可以将其视为 monad。然而,这并不是大多数人在实践中使用它们的方式。对于大多数使用有效 Futures(包括 Akka 和各种 Web 框架的大多数用途)的人来说,它们根本不是 monad。

幸运的是,一个名为 Scalaz 的库提供了一个称为 Task 的抽象,它在有或没有效果的情况下没有任何问题。

单子(monad)定义

让我们简要回顾一下 monad 是什么。一个 monad 必须至少能够定义这两个函数:

def unit[A](block: => A) 
    : Future[A] 
 
def bind[A, B](fa: Future[A])(f: A => Future[B]) 
    : Future[B] 

而这些函数必须满足三个定律:
  • 左身份 :bind(unit(a))(f) ≡ f(a)
  • 正确身份 :bind(m) { unit(_) } ≡ m
  • 结合性 :bind(bind(m)(f))(g) ≡ bind(m) { x => bind(f(x))(g) }

  • 根据 monad 的定义,这些定律必须适用于所有可能的值。如果他们没有,那么我们根本就没有 monad。

    还有其他方法可以定义或多或少相同的 monad。这个很受欢迎。

    影响导致非值

    我见过的几乎所有 Future 用法都将其用于异步效果、与外部系统(如 Web 服务或数据库)的输入/输出。当我们这样做时,Future 甚至不是一个值,像 monads 这样的数学术语只描述了值。

    出现这个问题是因为 Futures 在数据构建后立即执行。这会破坏用评估值替换表达式的能力(有些人称之为“引用透明度”)。这是理解为什么 Scala 的 Futures 不适合带效果的函数式编程的一种方式。

    这是问题的说明。如果我们有两个效果:
    import scala.concurrent.Future 
    import scala.concurrent.ExecutionContext.Implicits._ 
     
     
    def twoEffects = 
      ( Future { println("hello") }, 
        Future { println("hello") } ) 
    

    我们将在拨打 twoEffects 时打印两次“hello” :
    scala> twoEffects 
    hello 
    hello 
     
    scala> twoEffects 
    hello 
    hello 
    

    但是如果 Futures 是值,我们应该能够分解出常见的表达方式:
    lazy val anEffect = Future { println("hello") } 
     
    def twoEffects = (anEffect, anEffect) 
    

    但这并没有给我们同样的效果:
    scala> twoEffects 
    hello 
     
    scala> twoEffects 
    

    第一次拨打 twoEffects运行效果并缓存结果,所以效果不会在我们第二次调用 twoEffects 时运行.

    对于 Futures,我们最终不得不考虑语言的评估策略。例如,在上面的示例中,我使用惰性值而不是严格值这一事实在操作语义上有所不同。这正是函数式编程旨在避免的那种扭曲推理——它通过使用值编程来实现。

    没有替代,法律就会失效

    在效果面前,monad 法则被打破。从表面上看,这些定律似乎适用于简单的情况,但是当我们开始用表达式的评估值替换表达式时,我们最终会遇到与上面说明的相同的问题。当我们首先没有值时,我们根本无法谈论像单子(monad)这样的数学概念。

    坦率地说,如果你使用你的 Futures 的效果,说它们是单子(monad)是 not even wrong因为它们甚至都不是值。

    要了解 monad 定律是如何被破坏的,只需考虑您的有效 Future:
    import scala.concurrent.Future 
    import scala.concurrent.ExecutionContext.Implicits._ 
     
     
    def unit[A] 
        (block: => A) 
        : Future[A] = 
      Future(block) 
     
    def bind[A, B] 
        (fa: Future[A]) 
        (f: A => Future[B]) 
        : Future[B] = 
      fa flatMap f 
     
    lazy val effect = Future { println("hello") } 
    

    同样,它只会运行一次,但您需要它运行两次——一次用于右侧的法律,另一次用于左侧。我将说明正确身份法的问题:
    scala> effect  // RHS has effect 
    hello 
     
    scala> bind(effect) { unit(_) }  // LHS doesn't 
    

    隐含的 ExecutionContext

    如果不将 ExecutionContext 置于隐式作用域中,我们就无法定义 unitbind在我们的 monad 中。这是因为 Scala API for Futures 具有以下签名:
    object Future { 
      // what we need to define unit 
      def apply[T] 
          (body: ⇒ T) 
          (implicit executor: ExecutionContext) 
          : Future[T] 
    } 
     
    trait Future { 
       // what we need to define bind 
       flatMap[S] 
           (f: T ⇒ Future[S]) 
           (implicit executor: ExecutionContext) 
           : Future[S] 
    } 
    

    作为对用户的“方便”,标准库鼓励用户在隐式范围内定义执行上下文,但我认为这是 API 中的一个巨大漏洞,只会导致缺陷。计算的一个范围可以定义一个执行上下文,而另一个范围可以定义另一个上下文。

    如果你定义一个 unit 的实例,也许你可以忽略这个问题。和 bind将两个操作固定到单个上下文并一致地使用此实例。但这不是人们大多数时候所做的。大多数情况下,人们使用 Futures 的 yield 理解为 mapflatMap调用。要使 for-yield 推导式工作,必须在某个非全局隐式范围内定义执行上下文(因为 for-yield 不提供为 mapflatMap 调用指定附加参数的方法)。

    需要明确的是,Scala 允许您使用很多实际上不是 monad 的 for-yield 推导式,所以不要仅仅因为它与 for-yield 语法一起工作就相信您有一个 monad。

    更好的方法

    Scala 有一个不错的库,名为 Scalaz它有一个名为 scalaz.concurrent.Task 的抽象。这种抽象不会像标准库 Future 那样对数据构造产生影响。此外,Task 实际上是一个 monad。我们以单一方式组合 Task(如果我们愿意,我们可以使用 for-yield comprehensions),并且在我们组合时没有任何效果运行。当我们编写了一个计算结果为 Task[Unit] 的表达式时,我们就有了最终的程序。 .这最终相当于我们的“主”函数,我们终于可以运行它了。

    这是一个示例,说明我们如何将 Task 表达式替换为其各自的评估值:
    import scalaz.concurrent.Task 
    import scalaz.IList 
    import scalaz.syntax.traverse._ 
     
     
    def twoEffects = 
      IList( 
        Task delay { println("hello") }, 
        Task delay { println("hello") }).sequence_ 
    

    我们将在拨打 twoEffects 时打印两次“hello” :
    scala> twoEffects.run 
    hello 
    hello 
    

    如果我们把共同效应排除在外,
    lazy val anEffect = Task delay { println("hello") } 
     
    def twoEffects = 
      IList(anEffect, anEffect).sequence_ 
    

    我们得到了我们所期望的:
    scala> twoEffects.run 
    hello 
    hello 
    

    实际上,对于 Task 使用惰性值还是严格值并不重要;无论哪种方式,我们都会打印出两次 hello。

    如果您想进行功能性编程,请考虑在任何可能使用 Futures 的地方使用 Task。如果 API 将 Futures 强加给您,您可以将 Future 转换为任务:
    import concurrent. 
      { ExecutionContext, Future, Promise } 
    import util.Try 
    import scalaz.\/ 
    import scalaz.concurrent.Task 
     
     
    def fromScalaDeferred[A] 
        (future: => Future[A]) 
        (ec: ExecutionContext) 
        : Task[A] = 
      Task 
        .delay { unsafeFromScala(future)(ec) } 
        .flatMap(identity) 
     
    def unsafeToScala[A] 
        (task: Task[A]) 
        : Future[A] = { 
      val p = Promise[A] 
      task.runAsync { res => 
        res.fold(p failure _, p success _) 
      } 
      p.future 
    } 
     
    private def unsafeFromScala[A] 
        (future: Future[A]) 
        (ec: ExecutionContext) 
        : Task[A] = 
      Task.async( 
        handlerConversion 
          .andThen { future.onComplete(_)(ec) }) 
     
    private def handlerConversion[A] 
        : ((Throwable \/ A) => Unit) 
          => Try[A] 
          => Unit = 
      callback => 
        { t: Try[A] => \/ fromTryCatch t.get } 
          .andThen(callback) 
    

    “不安全”函数运行任务,将任何内部效果暴露为副作用。因此,在为整个程序编写了一个巨大的 Task 之前,请尽量不要调用这些“不安全”函数中的任何一个。