flatMap 和 map 提供“一元魔法”是完全正确的。幸运或不幸(取决于你见过多少糟糕的代码)编程没有魔法。再多的抽象也无法使您(或其他人)免于最终编写执行您想要的事情的代码。抽象“只是”让您可以重用以前编写的代码并澄清您对问题的想法。那么,monad 只是一个概念、一个想法、一个抽象等。
在 Scala 的情况下,这实际上就是编译器对 for 的理解,它变成了一系列 flatMap、map、withFilter 和 filter 语句。
可以将 monad(在 Scala 中)视为恰好有一个类型构造函数 T[_] 和两个函数的现象的标签1
def f0[A](x: T[A], f: X => T[A]): T[A]
def f1[A](x: A): T[A]
按照惯例,当他们看到这种现象时,Scala 社区会调用f0 flatMap 并通常将其设为方法,以便x 始终是父类而不是单独的参数。还有一个约定叫f1point或pure(见scalaz或cats)。 f1 通常也是一种方法,因此它最终不会显式地接受参数,而只是将其父类用作 x。
每当有人说“某某”是一个单子时,总有一个隐含的f0 和f1,说话者希望听者推断出来。严格来说,“List 是一个单子”是对术语的轻微滥用。它是List 的简写,与函数(xs: List[A], f: A => List[A]) => xs.map(f).flatten(形成f0)和(x: A) => List(x)(形成f1)一起形成一个monad。或者稍微不那么晦涩,List 与列表上的标准 flatMap 和 List.apply 构造函数形成一个 monad。
因此从来没有任何魔法。作为将某物分类为Monad 的一部分,您必须提供flatMap 和pure 的概念。
您可以通过多种方式将这种 monad 抽象化为代码。天真的方法(即没有第三方库的 Scala)是就 f0 和 f1 的通用名称达成一致(例如 flatMap),然后将具有适当类型签名的方法命名为这些名称。这基本上就是scalac 期望你为for 理解所做的事情。您可以更进一步,尝试使用trait 或abstract class 将事情正式化。也许称它为Monad 是可爱的并且有类似以下的东西:
trait Monad[A] {
def flatMap(f: A => Monad[A]): Monad[A]
def pure(x: A): Monad[A]
}
然后,您可以将扩展此 Monad 的任何东西称为 monad 想法的实现(您可能会想像 class List[A] extends Monad[A] 这样的东西。
由于各种实际原因,结果并不令人满意,因此您最终得到的通常解决方案看起来像(挥手消除了许多其他复杂性)
trait Monad[F[_]] {
def flatMap[A](f: A => F[A]): F[A]
def pure[A](x: A): F[A]
}
由implicits 实现。
脚注:
- 以及一些管理它们相互作用的法律/公约。这些定律存在的实际原因是为了让程序员的生活更加清醒,这样当有人告诉他们这些函数是“单子的”时,他们知道会发生什么。正是这些规律使得推理诸如 monad 之类的结构如此有用,但我不会在这里深入研究它们,因为它们在其他地方已经得到充分解释。