Scala中的Future是monad吗?

ntjbwcob  于 5个月前  发布在  Scala
关注(0)|答案(4)|浏览(56)

为什么Scala Future不是Monad,具体是怎么样的?有人能把它和Monad做个比较吗?比如Option。
我问这个问题的原因是丹尼尔韦斯特海德的The Neophyte's Guide to Scala Part 8: Welcome to the Future,我问Scala Future是否是Monad,作者回答说不是,这就大错特错了。

xxls0lw8

xxls0lw81#

先做摘要

如果你从未使用有效的块(纯内存计算)来构造Futures,或者如果生成的任何效果都不被认为是语义等价的一部分(比如日志消息),那么Futures可以被认为是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) }

根据单子的定义,这些定律必须适用于所有可能的值,如果它们不成立,那么我们就没有单子。
定义单子还有其他的方法,它们或多或少都是相同的,这一种比较流行。

效果导致无价值

我所见过的几乎所有Future的用法都是将其用于异步效果,与外部系统(如Web服务或数据库)进行输入/输出。当我们这样做时,Future甚至不是一个值,而数学术语(如monad)只描述值。
这个问题的出现是因为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


但是,如果期货是价值观,我们应该能够分解出共同的表达:

lazy val anEffect = Future { println("hello") }

def twoEffects = (anEffect, anEffect)


但这并不能给我们带来给予同样的效果:

scala> twoEffects
hello

scala> twoEffects


twoEffects的第一次调用运行效果并缓存结果,因此第二次调用twoEffects时效果不会运行。
对于Futures,我们最终不得不考虑语言的求值策略。例如,在上面的例子中,我使用了一个懒惰的值而不是一个严格的值,这一事实在操作语义上产生了差异。这正是函数式编程旨在避免的扭曲推理-它通过使用值编程来避免。

没有替代,法律就会被打破

在效应出现时,单子定律就失效了。表面上,这些定律似乎适用于简单的情况,但当我们开始用表达式的求值值替换表达式时,我们最终会遇到上面提到的同样的问题。当我们一开始就没有值时,我们根本不能谈论像单子这样的数学概念。
坦率地说,如果你在Futures中使用了effects,说它们是monad是not even wrong,因为它们甚至不是值。
要想知道单子定律是如何被打破的,只需分解出你的有效未来:

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 Futures的API有以下签名:

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的示例,将这两个操作都绑定到一个上下文,并一致地使用这个示例,也许你可以忽略这个问题。但这不是人们大多数时候所做的。大多数时候,人们使用Futures和for-yield解析,它们变成了mapflatMap调用。为了使for-yield解析工作,执行上下文必须在某个非全局隐式范围内定义(因为for-yield没有提供一种方法来指定mapflatMap调用的附加参数)。
需要明确的是,Scala允许你使用很多实际上不是monad的for-yield解析,所以不要仅仅因为它使用for-yield语法就相信你有monad。

更好的方式

有一个很好的Scala库,名为Scalaz,它有一个抽象,名为scalaz. concurrent. Task。这个抽象不像标准库Future那样对数据构造产生影响。此外,Task实际上是一个monad。我们以monadically方式组成Task(如果愿意,我们可以使用for-yield理解),当我们编写了一个计算结果为Task[Unit]的表达式时,我们就有了最终的程序。这最终成为了我们的“main”函数的等价物,我们终于可以运行它了。

这里有一个例子说明了我们如何用它们各自的评估值替换任务表达式:

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中使用lazy值还是strict值并不重要;无论哪种方式,hello都会打印两次。
如果你想进行功能性编程,可以考虑在所有可能使用Futures的地方使用Task。如果一个API强制你使用Futures,你可以将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)


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

u0sqgete

u0sqgete2#

我相信未来是一个单子,定义如下:

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)。检查。
1.正确身份:
m.flatMap(Future.successful _)等效于m(减去一些可能的性能影响)。检查。
1.结合性m.flatMap(f).flatMap(g)等价于m.flatMap(x => f(x).flatMap(g))。检查。

反驳“没有替代,法律就不存在”

在monad定律中等价的含义,据我所知,是你可以在代码中用另一边替换表达式的一边,而不改变程序的行为。假设你总是使用相同的执行上下文,我认为情况就是这样。在@sukant给出的例子中,如果它使用Option而不是Future,它也会有同样的问题。我不认为期货被急切地评估的事实是相关的。

5tmbdcev

5tmbdcev3#

我必须在课堂上教期货,所以我看了这个,回来后真的很困惑,特别是这个陈述和它所附带的例子。
要想知道单子定律是如何被打破的,只需分解出你的有效未来:
我盯着代码看了很长时间。我可以在repl中复制它,但无法弄清楚为什么行为会有所不同。(futures start immediately)似乎是错误的,我看不出这有什么区别。然后它点击了。这是因为JVM的怪癖,一个程序在main结束时关闭,不管是否还有任务要执行。只是在其中一个例子中,当repl行完成时,future已经完成,而在另一个例子中,它还没有完成。如果你在代码之后执行线程睡眠,两个例子都将打印。
换句话说,它是程序和JVM在main之后退出之间的竞争条件。

woobm2wo

woobm2wo4#

正如其他评论者所说,你错了。Scala的Future类型 * 具有 * 一元属性:

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中对futures使用for-comprehension语法。

相关问题