先总结
如果您从不使用有效块(纯内存中计算)构造期货,或者如果生成的任何效果不被视为语义等价的一部分(如记录消息),则可以将期货视为 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 中定义 unit 或 bind。这是因为 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 的实例,将两个操作固定到单个上下文并一致地使用此实例,也许您可以忽略此问题。但这不是人们大多数时候做的事情。大多数情况下,人们使用带有 for-yield 理解的 Futures,它们变成了 map 和 flatMap 调用。为了使 for-yield 理解起作用,必须在某个非全局隐式范围内定义执行上下文(因为 for-yield 不提供为 map 和 flatMap 调用指定附加参数的方法)。
需要明确的是,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)
“不安全”函数运行任务,将任何内部效果暴露为副作用。因此,在您为整个程序编写一个巨大的任务之前,请尽量不要调用任何这些“不安全”的函数。