【问题标题】:Is Future in Scala a monad?Scala 中的 Future 是一个单子吗?
【发布时间】:2015-02-11 19:44:38
【问题描述】:

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

我问的原因是 Daniel Westheide 的 The Neophyte's Guide to Scala Part 8: Welcome to the Future,在那里我问了 Scala Future 是否是 Monad,而作者回答说不是,这很离谱。我是来要求澄清的。

【问题讨论】:

  • 有平面地图吗?是的,它可以用于理解吗?是的,那么它是一个单子。
  • @ElectricCoffee no.
  • @PabloFernandez Scala 的 flatMap 是 Haskell 的 >>=,Scala 的 for-comprehensions 等价于 Haskell 的 do 表示法。如果它长得像鸭子,叫起来像鸭子……
  • @ElectricCoffee 当然,但 monad 不仅是绑定(或 >>=)
  • @ElectricCoffee 它不是一个 monad,因为它只是实现了两个具有特定名称的函数。必须满足三个一元定律,请参阅已接受的答案。

标签: scala monads


【解决方案1】:

先总结

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

幸运的是,一个名为 Scalaz 的库提供了一个名为 Task 的抽象,无论有无效果都没有任何问题。

单子定义

让我们简要回顾一下什么是 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。这个很受欢迎。

效果导致无价值

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

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

以下是问题的说明。如果我们有两个效果:

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 这样的数学概念。

坦率地说,如果你在 Future 中使用效果,说它们是 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 置于隐式范围内,我们将无法在 monad 中定义 unitbind。这是因为 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 中的一个巨大漏洞,只会导致缺陷。一个计算范围可以定义一个执行上下文,而另一个范围可以定义另一个上下文。

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

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

更好的方法

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

下面是一个示例,说明我们如何将 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时,我们将打印两次“你好”:

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 强制您使用 Future,您可以将 Future 转换为 Task:

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)

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

【讨论】:

  • 你证明期货不是价值的证据是错误的——你有一个执行副作用的未来——同样的技术可以证明任何单子都不是价值。未来实际上是一个时间价值——它就是这样。你甚至在你的“证明”中承认这一点——如果你知道你的证明是错误的——为什么要包括它?您的执行上下文参数完全相同 - 它似乎也不正确。
  • 您认为 Scalaz 的任务类似于共生词很大程度上是因为“不安全”的运行似乎类似于 copoint。这只是 JVM 上的一种编码,它不是为运行功能程序而设计的机器。我们在 FP 中编写表达式,直到我们“在世界尽头”执行它们。表达式可以定义效果和无效的计算。
  • 我不确定 Haskell 与这个问题的关系,而不仅仅是一种 FP 语言......至于论点 - 你 可以 执行副作用的事实Scala 并没有使它不再是一个单子,就像Tasks 一样(因为我可以很容易地构建一个例子,其中这些也会导致干扰副作用)。 Futures 是 monad 的事实仅仅是因为它们实际上确实遵守 monad 法则(就像 Tasks 对comonads 所做的那样)。
  • 您能否更明确地说明法律不成立的原因?有力的言辞和“甚至没有错”都很好,但有具体的例子会更有建设性(比较例如SI-6284,其中有一个代码示例说明为什么Try 违反了函子定律)。跨度>
  • 我感觉您将副作用与价值混为一谈,因此得出了一个奇怪的、大部分是错误的结论。如果您对所有术语提供更严格的定义,您可能会自己看到这里实际上没有违反任何法律。
【解决方案2】:

我相信 Future 是 Monad,具有以下定义:

def unit[A](x: A): Future[A] = Future.successful(x)

def bind[A, B](m: Future[A])(fun: A => Future[B]): Future[B] = fut.flatMap(fun)

考虑三大定律:

  1. 左身份:

    Future.successful(a).flatMap(f) 等价于f(a)。检查。

  2. 正确的身份:

    m.flatMap(Future.successful _) 等价于m(减去一些可能的性能影响)。检查。

  3. 关联性 m.flatMap(f).flatMap(g) 等价于 m.flatMap(x => f(x).flatMap(g))。检查。

反驳“不换人,法破”

按照我的理解,monad 法则中等价的含义是,您可以在代码中将表达式的一侧替换为另一侧,而不会改变程序的行为。假设您总是使用相同的执行上下文,我认为情况就是如此。在@sukant 给出的示例中,如果它使用Option 而不是Future,也会遇到同样的问题。我认为对期货进行热切评估的事实并不重要。

【讨论】:

    【解决方案3】:

    正如其他评论者所建议的那样,您错了。 Scala 的Future 类型具有 monadic 属性:

    import scala.concurrent.Future
    import scala.concurrent.ExecutionContext.Implicits._
    
    def unit[A](block: => A): Future[A] = Future(block)
    def bind[A, B](fut: Future[A])(fun: A => Future[B]): Future[B] = fut.flatMap(fun)
    

    这就是为什么您可以在 Scala 中将 for-comprehension 语法与期货一起使用。

    【讨论】:

    • 嗨 0__,我相信 @Sukan 的回答是正确的;拥有一元属性是不够的,但它们还必须满足 3 条定律。
    猜你喜欢
    • 1970-01-01
    • 2016-08-01
    • 2012-05-16
    • 1970-01-01
    • 2023-03-20
    • 1970-01-01
    • 2017-05-14
    • 2014-10-21
    相关资源
    最近更新 更多