何时以及为什么应该在Scala中使用应用函子?

73

我知道在Scala中可以使用以下方式表达Monad:

trait Monad[F[_]] {
  def flatMap[A, B](f: A => F[B]): F[A] => F[B]
}

我明白它的用处。例如,给定两个函数:

getUserById(userId: Int): Option[User] = ...
getPhone(user: User): Option[Phone] = ...

因为Option是一个单子,所以我可以轻松编写函数getPhoneByUserId(userId: Int)

def getPhoneByUserId(userId: Int): Option[Phone] = 
  getUserById(userId).flatMap(user => getPhone(user))

现在我在Scala中看到了Applicative Functor(应用函子)

trait Applicative[F[_]] {
  def apply[A, B](f: F[A => B]): F[A] => F[B]
}

我想知道何时应该使用它,而不是单子。 我猜Option和List都是应用程序。你能给出使用Option和List的应用示例,并解释为什么我应该使用它而不是flatMap吗?


1
检查“scalaz” [示例](http://www.casualmiracles.com/2012/01/16/a-small-example-of-applicative-functors-with-scalaz/),[教程示例](http://eed3si9n.com/learning-scalaz/Applicative.html),[scalaz教程](http://eed3si9n.com/learning-scalaz/)。 - Gabriel Riba
Monad不仅是一个带有flatMap方法的特质,还有一个unit方法和一些必须验证的法则。 - Yann Moisan
3
@YannMoisan 没错。为了简洁起见,我省略了它们。 - Michael
@GabrielRiba 我看了一下scalaz的例子。我知道他们如何使用<*>,但我仍然不明白为什么带有<*>的例子比没有它的例子更好。所有的例子似乎都解决了这个问题。 - Michael
看起来 scalaz (<*>) 在右侧使用了 lifted 函数(与 Haskell 相反),但 (<*>) 是左结合的(右结合操作以 ':' 结尾),因此您需要在所有 (<*>) 术语中使用右括号。使用 (|@|) 的替代语法更加简洁。 - Gabriel Riba
这是一组非常有用的示例和问题。然而,我对第一个示例有疑问。flatMap的返回类型不应该是F[B]吗?而不是一个函数 F[A]=>F[B]?请参见D. Wampler, Programming in Scala, p. 417,了解 flatMap 返回 F[B] 的示例。换句话说,在您的示例中,getPhoneByUserId()将返回Option[Phone],而不是函数Option[User]=>Option[Phone]。 - Ben Weaver
2个回答

80

引用自己:

既然我们有了单子,为什么还要使用应用函子呢?首先,对于某些我们想要处理的抽象类型,根本无法提供单子实例——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 这样的发展是朝着正确方向迈出的一步。


30

Functor 用于将计算提升到一个范畴中。

trait Functor[C[_]] {
  def map[A, B](f : A => B): C[A] => C[B]
}

对于一个变量的函数,它能够完美地工作。

val f = (x : Int) => x + 1

但是对于两个或更多变量的函数,在提升到一个范畴之后,我们有以下签名:

val g = (x: Int) => (y: Int) => x + y
Option(5) map g // Option[Int => Int]

这是一个应用函子的签名。如果要将下一个值应用于函数 g,则需要一个应用函子。

trait Applicative[F[_]] {
  def apply[A, B](f: F[A => B]): F[A] => F[B]
} 

最后:

(Applicative[Option] apply (Functor[Option] map g)(Option(5)))(Option(10))

应用函子是一种将特殊值(范畴中的值)应用于提升函数的函子。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接