引用自己:
既然我们有了单子,为什么还要使用应用函子呢?首先,对于某些我们想要处理的抽象类型,根本无法提供单子实例——Validation
就是完美的例子。
其次(并且相关),在开发实践中使用最不强大的抽象能够完成工作只是一种可靠的做法。原则上,这可能允许进行不能否则进行的优化,但更重要的是,它使我们编写的代码更具可重用性。
稍微详细解释第一段:有时你没有选择单子或应用函子代码之间的选择。请参见该答案的其余部分,以了解为何要使用 Scalaz 的 Validation
(它没有单子实例)来模拟验证。
关于优化点:在 Scala 或 Scalaz 中普遍相关需要一段时间,但可以参考例如 Haskell 的 Data.Binary
文档:
应用风格有时可能会导致更快的代码,因为binary
将尝试通过分组读取来优化代码。
编写应用函子代码可以避免做出关于计算之间依赖关系的不必要声明,而这种声明在类似单子的代码中是必需的。足够聪明的库或编译器原则上可以利用这一事实。
为了使这个想法更具体化,考虑以下单子代码:
case class Foo(s: Symbol, n: Int)
val maybeFoo = for {
s <- maybeComputeS(whatever)
n <- maybeComputeN(whatever)
} yield Foo(s, n)
for
推导式被展开为类似以下的形式:
val maybeFoo = maybeComputeS(whatever).flatMap(
s => maybeComputeN(whatever).map(n => Foo(s, n))
)
我们知道,假设这些方法不会在幕后更改某些可变状态,
maybeComputeN(whatever)
不依赖于
s
,但编译器并不知道 —— 从它的角度来看,在开始计算
n
之前需要了解
s
。
使用 Scalaz 的 Applicative 版本如下:
val maybeFoo = (maybeComputeS(whatever) |@| maybeComputeN(whatever))(Foo(_, _))
这里我们明确指出这两个计算之间没有依赖关系。
(是的,这个 |@| 语法相当可怕 - 可以看看这篇博客中的一些讨论和替代方法。)
然而,最后一个观点才是最重要的。选择解决问题的最不强大的工具是一个非常有效的原则。有时候你确实需要单子合成——例如在你的 getPhoneByUserId 方法中——但通常情况下你并不需要。
遗憾的是,Haskell 和 Scala 目前让使用 monads 比应用 functors 更方便(在语法等方面),但这主要是历史偶然,像 idiom brackets 这样的发展是朝着正确方向迈出的一步。
flatMap
方法的特质,还有一个unit
方法和一些必须验证的法则。 - Yann Moisan<*>
,但我仍然不明白为什么带有<*>
的例子比没有它的例子更好。所有的例子似乎都解决了这个问题。 - Michael