Scala的未来是monad吗?

Nie*_*che 46 monads scala

为什么以及如何具体地说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(_) } ? m
  • 相关性:bind(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

如果不将ExecutionContext放在隐式作用域中,我们就无法定义unitbind在我们的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将两个操作都固定到单个上下文并且一致地使用这个实例,也许你可以忽略这个问题.但这不是人们大多数时间做的事情.大多数时候,人们利用期货与收益的内涵是成为mapflatMap电话.为了使for-yield理解起作用,必须在某些非全局隐式作用域中定义执行上下文(因为for-yield不提供为mapflatMap调用指定其他参数的方法).

为了清楚起见,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之前,尽量不要调用任何这些"不安全"的函数.

  • 你证明期货不是价值是错误的 - 你的未来会产生副作用 - 同样的技术可以证明任何单子都不是价值.事实上,未来是一个时间价值 - 这正是它的本质.你甚至在你的"证明"中承认这一点 - 如果你知道你的证据是错误的 - 为什么要包括它?执行上下文参数完全相同 - 它似乎也不正确. (4认同)
  • 我不确定Haskell是如何与除了作为FP语言之外的问题相关的......至于论证 - 你在Scala中执行副作用的事实并不会使它成为monad而不是`task`s (因为我可以轻松地构建一个例子,其中那些也导致干扰副作用)."未来"是monad的事实仅仅是因为他们实际上遵守了monad定律(就像任务与comonads一样). (4认同)
  • 你能更清楚地了解法律如何不成立吗?强有力的修辞和"甚至不错"都是好的,但是具体的例子更具建设性(比如[SI-6284](https://issues.scala-lang.org/browse/SI-) 6284),其中有一个代码示例,说明为什么"Try"违反了仿函数法则. (4认同)
  • 我感觉你正在将副作用与价值观混合在一起,从而产生一种奇怪的,大多数是错误的结论.如果您对所有术语提供更严格的定义,您可能会自己看到实际上没有法律在这里被打破. (4认同)
  • @Sukant Hajra - 除非人们说"未来是单身",否则他们不会谈论副作用.有点可疑,你甚至应该考虑副作用,因为Monad是一种类型,它描述了值(或者你喜欢的对象),而且值没有副作用.表达式有副作用,值不具备.这就是你对monadic定律和那​​些说Future是Monad的那些解释的不同之处:你说表达式X必须等同于表达式Y,并且他们说表达式X的值等价于表达式Y的值. (3认同)
  • 效果和副作用之间是有区别的。Haskell的IO是一个值,只是描述一个效果。Scalaz 的任务也只是一个效果。未来有副作用。在做FP的时候理解这一点非常重要。 (2认同)
  • 您认为Scalaz的Task像comonads一样,很大程度上是由于“不安全”运行似乎与copoint相似。这只是JVM上的一种编码,它不是旨在运行功能程序的机器。我们在FP中编写表达式,直到“在世界的尽头”执行它们。表达式可以定义效果和无效计算。 (2认同)
  • 使用Task,你/可以/打破它,老实说,如果这困扰你,你应该向Scalaz提交一个bug.随着期货,它总是被打破. (2认同)
  • 你是对的,Future 对于非影响性计算来说是很好的,我在我的文章中指出了这一点,但这根本不是大多数人使用 Future 的方式,打破错误信息非常重要。此外,将 Task 称为 comonad 是非常非常错误的。尽管存在 Comonad 类型类,但 Scalaz 中的 Task 没有 Comonad 实例。这有一个很大的原因。 (2认同)
  • 从这个意义上讲,我相信单子法则适用于期货,使它们像列表一样成为单子,也可能是单子:它们是可以单子方式操纵的价值。人们容易想到,期货就像是Haskell的IO Monad(使用Monad的最著名方法)或Scala'z Task,但是正如您所指出的,它们的语义完全不同。但是,据我所知,这些语义并不是严格意义上的单子律。 (2认同)

Tha*_*yne 7

我相信 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)

考虑三个定律:

  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。我认为急切地评估期货这一事实无关紧要。