gpt4 book ai didi

scala - Scala 中的 Future 是一个 monad 吗?

转载 作者:行者123 更新时间:2023-12-03 06:12:38 25 4
gpt4 key购买 nike

为什么 Scala Future 不是 Monad?有人可以将它与 Monad 的东西进行比较,例如 Option 吗?

我问的原因是 Daniel Westeide 的 The Neophyte's Guide to Scala Part 8: Welcome to the Future在那里我问 Scala Future 是否是 Monad,作者回答说它不是,这不合时宜。我是来求证的。

最佳答案

先总结一下

如果您从不使用有效块(纯内存计算)构造 Future,或者如果生成的任何效果不被视为语义等价的一部分(如日志消息),则可以将其视为 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) }

  • 根据 monad 的定义,这些定律必须适用于所有可能的值。如果他们没有,那么我们根本就没有 monad。

    还有其他方法可以定义或多或少相同的 monad。这个很受欢迎。

    影响导致非值

    我见过的几乎所有 Future 用法都将其用于异步效果、与外部系统(如 Web 服务或数据库)的输入/输出。当我们这样做时,Future 甚至不是一个值,像 monads 这样的数学术语只描述了值。

    出现这个问题是因为 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

    但是如果 Futures 是值,我们应该能够分解出常见的表达方式:
    lazy val anEffect = Future { println("hello") }

    def twoEffects = (anEffect, anEffect)

    但这并没有给我们同样的效果:
    scala> twoEffects
    hello

    scala> twoEffects

    第一次拨打 twoEffects运行效果并缓存结果,所以效果不会在我们第二次调用 twoEffects 时运行.

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

    没有替代,法律就会失效

    在效果面前,monad 法则被打破。从表面上看,这些定律似乎适用于简单的情况,但是当我们开始用表达式的评估值替换表达式时,我们最终会遇到与上面说明的相同的问题。当我们首先没有值时,我们根本无法谈论像单子(monad)这样的数学概念。

    坦率地说,如果你使用你的 Futures 的效果,说它们是单子(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 置于隐式作用域中,我们就无法定义 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]
    }

    作为对用户的“方便”,标准库鼓励用户在隐式范围内定义执行上下文,但我认为这是 API 中的一个巨大漏洞,只会导致缺陷。计算的一个范围可以定义一个执行上下文,而另一个范围可以定义另一个上下文。

    如果你定义一个 unit 的实例,也许你可以忽略这个问题。和 bind将两个操作固定到单个上下文并一致地使用此实例。但这不是人们大多数时候所做的。大多数情况下,人们使用 Futures 的 yield 理解为 mapflatMap调用。要使 for-yield 推导式工作,必须在某个非全局隐式范围内定义执行上下文(因为 for-yield 不提供为 mapflatMap 调用指定附加参数的方法)。

    需要明确的是,Scala 允许您使用很多实际上不是 monad 的 for-yield 推导式,所以不要仅仅因为它与 for-yield 语法一起工作就相信您有一个 monad。

    更好的方法

    Scala 有一个不错的库,名为 Scalaz它有一个名为 scalaz.concurrent.Task 的抽象。这种抽象不会像标准库 Future 那样对数据构造产生影响。此外,Task 实际上是一个 monad。我们以单一方式组合 Task(如果我们愿意,我们可以使用 for-yield comprehensions),并且在我们组合时没有任何效果运行。当我们编写了一个计算结果为 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_

    我们将在拨打 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 使用惰性值还是严格值并不重要;无论哪种方式,我们都会打印出两次 hello。

    如果您想进行功能性编程,请考虑在任何可能使用 Futures 的地方使用 Task。如果 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)

    “不安全”函数运行任务,将任何内部效果暴露为副作用。因此,在为整个程序编写了一个巨大的 Task 之前,请尽量不要调用这些“不安全”函数中的任何一个。

    关于scala - Scala 中的 Future 是一个 monad 吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/27454798/

    25 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com