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


71

我知道Monad可以在Scala中表示如下:

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] = ...

我很容易编写函数,getPhoneByUserId(userId: Int)因为Option它是monad:

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

...

现在我Applicative Functor在Scala中看到了:

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

我不知道何时应该使用它代替monad。我猜Option和List都是Applicatives。您能举几个简单的例子来说明如何使用applyOption和List并解释为什么我应该使用它而不是 flatMap


1
查看“ scalaz”示例教程示例scalaz教程
Gabriel Riba

monad不仅是一种flatMap方法的特征,而且还是一种unit方法以及一些必须验证的定律。
Yann Moisan 2013年

3
@YannMoisan是的。为了简单起见,我省略了它们。
Michael

@GabrielRiba我检查了scalaz示例。我了解了它们的用法,<*>但我仍然不明白为什么带有<*>示例的示例要比没有示例的示例更好。所有的例子似乎都解决了这个问题。
迈克尔

似乎scalaz(<*>)在右侧具有提升的功能,(与Haskell相反),但是(<*>)是左关联的(右关联操作以':'结尾),因此您需要右括号所有(<*>)项。(| @ |)的替代语法更简洁。
加布里埃尔·里巴

Answers:


81

引用自己

那么,当我们有了单子时,为什么还要烦恼所有应用函子呢?首先,完全不可能为我们要使用的某些抽象提供monad实例,这Validation是一个完美的例子。

其次(以及相关),使用最不强大的抽象来完成工作只是一种可靠的开发实践。原则上,这可能允许优化,否则将无法实现,但更重要的是,它使我们编写的代码更具可重用性。

在第一段中进行扩展:有时,您在单子代码和应用代码之间没有选择。请参阅该答案的其余部分,获取有关您为什么要使用Scalaz的模型Validation(没有和没有monad实例)进行模型验证的讨论。

关于优化点:在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-comprehension desugars的东西或多或少这样的:

val maybeFoo = maybeComputeS(whatever).flatMap(
  s => maybeComputeN(whatever).map(n => Foo(s, n))
)

我们知道这maybeComputeN(whatever)并不依赖s(假设它们是行为规范的方法,不会在幕后改变某些可变状态),但是编译器并不依赖-从其角度出发,它需要知道s才能开始计算n

适用版本(使用Scalaz)如下所示:

val maybeFoo = (maybeComputeS(whatever) |@| maybeComputeN(whatever))(Foo(_, _))

在这里,我们明确指出两个计算之间没有依赖关系。

(是的,这种|@|语法非常可怕,请参阅此博客文章以获取一些讨论和替代方法。)

最后一点确实是最重要的。挑选有力的工具,将解决你的问题是一个非常强大的原则。有时候,您确实确实需要单调成分(getPhoneByUserId例如在您的方法中),但通常不需要。

令人遗憾的是,Haskell和Scala当前都使与monad的工作比与应用函子的工作更加方便(在语法上等),但这主要是历史性的问题,并且成语括号之类的开发是正确的一步。方向。


28

Functor用于将计算提升到一个类别。

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

它非常适合一个变量的功能。

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

但是对于2或更大的函数,在升至类别后,我们具有以下签名:

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))

应用函子是将特殊值(类别中的值)应用于提升函数的函子。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.