为什么以及如何具体地说Scala Future不是Monad; 有人请将它与Monad的东西比较,比如选项吗?
我问的原因是Daniel Westheide的Scala新手指南第8部分:欢迎来到未来,我问Scala Future是否是Monad,作者回答说它不是,它已经脱离了基础.我来这里要求澄清.
Suk*_*jra 93
如果您从未使用有效块(纯粹的内存计算)构建它们,或者如果生成的任何效果不被视为语义等效的一部分(如记录消息),则可以将期货视为monads.然而,这并不是大多数人在实践中使用它们的方式.对于大多数使用有效期货(包括Akka的大多数用途和各种网络框架)的人来说,他们根本就不是单子.
幸运的是,一个名为Scalaz的库提供了一个名为Task的抽象,它在有或没有效果的情况下都没有任何问题.
让我们简要回顾一下monad是什么.monad必须能够至少定义这两个函数:
def unit[A](block: => A)
: Future[A]
def bind[A, B](fa: Future[A])(f: A => Future[B])
: Future[B]
Run Code Online (Sandbox Code Playgroud)
这些功能必须满足三个法则:
bind(unit(a))(f) ? f(a)bind(m) { unit(_) } ? mbind(bind(m)(f))(g) ? bind(m) { x => bind(f(x))(g) }根据monad的定义,这些法律必须适用于所有可能的值.如果他们不这样做,那么我们根本就没有monad.
还有其他方法来定义或多或少相同的monad.这个很受欢迎.
我见过的几乎所有Future的使用都将它用于异步效果,使用外部系统(如Web服务或数据库)进行输入/输出.当我们这样做时,一个未来甚至不是一个价值,像monad这样的数学术语只描述价值.
出现这个问题是因为期货在数据构建后立即执行.这会混淆用表达式替换表达式的能力(有些人称之为"参考透明度").这是了解为什么Scala的Futures不适合具有效果的函数式编程的一种方法.
这是问题的一个例子.如果我们有两个影响:
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits._
def twoEffects =
( Future { println("hello") },
Future { println("hello") } )
Run Code Online (Sandbox Code Playgroud)
我们将在打电话时打印两个"hello" twoEffects:
scala> twoEffects
hello
hello
scala> twoEffects
hello
hello
Run Code Online (Sandbox Code Playgroud)
但如果期货是价值,我们应该能够分解出共同的表达:
lazy val anEffect = Future { println("hello") }
def twoEffects = (anEffect, anEffect)
Run Code Online (Sandbox Code Playgroud)
但这并没有给我们带来同样的效果:
scala> twoEffects
hello
scala> twoEffects
Run Code Online (Sandbox Code Playgroud)
第一次调用twoEffects运行效果并缓存结果,因此第二次调用时不会运行效果twoEffects.
有了Futures,我们最终不得不考虑语言的评估政策.例如,在上面的例子中,我使用惰性值而不是严格值的事实在操作语义上有所不同.这正是函数式编程旨在避免的扭曲推理 - 它通过使用值编程来实现.
在效果的早期,monad法则破裂.从表面上看,法律似乎适用于简单的案例,但是当我们开始用表达式替换表达式时,我们最终会遇到上面说明的相同问题.当我们首先没有值时,我们根本无法谈论像monad这样的数学概念.
说穿了,如果你对你的期货使用效果,说它们是monad 甚至没有错,因为它们甚至不是值.
要了解monad法律如何破裂,只需将你有效的未来分解出来:
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") }
Run Code Online (Sandbox Code Playgroud)
同样,它只运行一次,但你需要它运行两次 - 一次是法律的右边,另一次是左边.我将说明正确的身份法的问题:
scala> effect // RHS has effect
hello
scala> bind(effect) { unit(_) } // LHS doesn't
Run Code Online (Sandbox Code Playgroud)
如果不将ExecutionContext放在隐式作用域中,我们就无法定义unit或bind在我们的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]
}
Run Code Online (Sandbox Code Playgroud)
作为用户的"便利",标准库鼓励用户在隐式范围内定义执行上下文,但我认为这是API中的一个巨大漏洞,只会导致缺陷.一个计算范围可以定义一个执行上下文,而另一个范围可以定义另一个上下文.
如果你定义一个实例unit并且bind将两个操作都固定到单个上下文并且一致地使用这个实例,也许你可以忽略这个问题.但这不是人们大多数时间做的事情.大多数时候,人们利用期货与收益的内涵是成为map和flatMap电话.为了使for-yield理解起作用,必须在某些非全局隐式作用域中定义执行上下文(因为for-yield不提供为map和flatMap调用指定其他参数的方法).
为了清楚起见,Scala允许你使用很多东西,而不是实际上是monad的for-yield理解,所以不要因为它使用for-yield语法而认为你有monad.
Scalaz有一个很好的Scalaz库,它有一个名为scalaz.concurrent.Task的抽象.这种抽象不像标准库Future那样对数据构造产生影响.此外,Task实际上是一个monad.我们单独构成任务(如果我们愿意,我们可以使用for-yield理解),并且在我们编写时没有效果.当我们编写一个表达式来评估时,我们有了最终的程序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_
Run Code Online (Sandbox Code Playgroud)
我们将在打电话时打印两张"hello" twoEffects:
scala> twoEffects.run
hello
hello
Run Code Online (Sandbox Code Playgroud)
如果我们分解出共同的影响,
lazy val anEffect = Task delay { println("hello") }
def twoEffects =
IList(anEffect, anEffect).sequence_
Run Code Online (Sandbox Code Playgroud)
我们得到了我们所期望的:
scala> twoEffects.run
hello
hello
Run Code Online (Sandbox Code Playgroud)
事实上,我们是否使用惰性值或严格的值与Task相关并不重要; 无论哪种方式,我们打印两次你好.
如果您想进行功能编程,请考虑在任何地方使用任务,您可以使用Futures.如果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)
Run Code Online (Sandbox Code Playgroud)
"不安全"函数运行任务,将任何内部效果暴露为副作用.因此,在为整个程序编写一个巨大的Task之前,尽量不要调用任何这些"不安全"的函数.
我相信 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)
Run Code Online (Sandbox Code Playgroud)
考虑三个定律:
左身份:
Future.successful(a).flatMap(f)相当于f(a)。查看。
正确身份:
m.flatMap(Future.successful _)相当于m(减去一些可能的性能影响)。查看。
结合
m.flatMap(f).flatMap(g)性等价于m.flatMap(x => f(x).flatMap(g))。查看。
根据我的理解,monad 定律中等价的含义是,您可以在不改变程序行为的情况下,用代码中的另一侧替换表达式的一侧。假设你总是使用相同的执行上下文,我认为就是这种情况。在@sukant给的例子,它将不得不如果它使用了相同的问题Option,而不是Future。我认为急切地评估期货这一事实无关紧要。