Scala的未来是monad吗?

时间:2014-12-13 02:09:21

标签: scala monads

为什么以及如何具体的Scala Future而不是Monad;有人请将它与Monad的东西进行比较,比如选项吗?

我问的原因是Daniel Westheide The Neophyte's Guide to Scala Part 8: Welcome to the Future我询问Scala Future是否是Monad,作者回答说它不是,它扔了离开基地。我来这里要求澄清。

3 个答案:

答案 0 :(得分:88)

首先摘要

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

幸运的是,一个名为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。这个很受欢迎。

效果导致非值

我所看到的几乎所有Future的使用都将它用于异步效果,使用外部系统(如Web服务或数据库)进行输入/输出。当我们这样做时,未来甚至不是一个价值,像monad这样的数学术语只描述价值。

出现此问题是因为期货在数据构建后立即执行。这会混淆用表达式替换表达式的能力(有些人称之为#34;参考透明度")。这是理解为什么Scala的期货不适合具有效果的函数式编程的一种方法。

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

import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits._


def twoEffects =
  ( Future { println("hello") },
    Future { println("hello") } )

我们将打印两张"你好"在致电twoEffects时:

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时效果不会运行。

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

没有替代,法律破裂

在效果的早期,monad法则破裂。从表面上看,法律似乎适用于简单的案例,但是当我们开始用表达式替换表达式时,我们最终会遇到上面说明的相同问题。当我们首先没有价值观时,我们根本无法谈论像monad这样的数学概念。

说得直白,如果你对期货使用效果,说他们的monad是not even wrong因为它们的价值不均匀。

要了解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") }

同样,它只会运行一次,但你需要它运行两次 - 一次是法律的右侧,另一次是左侧。我将说明正确的身份法的问题:

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的实例,将两个操作都固定到单个上下文并始终如一地使用此实例,则可以忽略此问题。但这不是人们大多数时间做的事情。大多数情况下,人们使用Futures来获得mapflatMap调用的for-yield理解。为了使for-yield理解起作用,必须在某些非全局隐式范围内定义执行上下文(因为for-yield不提供为map和{{1}指定其他参数的方法。 }来电)。

为了清楚起见,Scala允许你使用很多东西,其中for-yield理解并不是真正的monad,所以不要因为它与for-yield语法一起工作而认为你有monad。

更好的方法

Scala的一个很好的库名为Scalaz,它有一个名为scalaz.concurrent.Task的抽象。这种抽象并不像标准库Future那样对数据构建产生影响。此外,Task实际上是一个monad。我们单独构成任务(如果我们愿意的话,我们可以使用for-yield理解),并且在我们编写时没有效果。当我们编写一个评估为flatMap的表达式时,我们有了最终的程序。这最终成为我们相当于" main"功能,我们终于可以运行了。

这是一个示例,说明我们如何用各自的评估值替换任务表达式:

Task[Unit]

我们将打印两张"你好"在致电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_

事实上,我们是否使用惰性值或严格的值与Task相关并不重要;无论如何我们两次打印你好。

如果您想进行功能性编程,请考虑在任何地方使用任务,您可以使用Futures。如果API迫使您使用Futures,您可以将Future转换为任务:

scala> twoEffects.run
hello
hello

"不安全"函数运行任务,将任何内部效果暴露为副作用。因此,尽量不要将这些中的任何一个称为“不安全的”#34;功能,直到你为整个程序组成一个巨大的任务。

答案 1 :(得分:2)

正如其他评论者所说,你错了。 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 - 理解语法。

答案 2 :(得分:2)

我相信未来是一个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))。检查。

  4. 反驳"没有替代,法律破裂"

    monad定律中等价的含义,据我所知,你可以在代码的另一面替换表达式的一面而不改变程序的行为。假设您始终使用相同的执行上下文,我认为就是这种情况。在@sukant给出的示例中,如果使用Option而不是Future,则会遇到相同的问题。我不认为期货得到热切评估的事实是相关的。