何时以及为什么要在Scala中使用Applicative Functors [英] When and why should one use Applicative Functors in Scala

查看:118
本文介绍了何时以及为什么要在Scala中使用Applicative Functors的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

 <$ c $ (F:A => F [B]):F [A] => F [B] 
}

我明白为什么它很有用。例如,给定两个函数:

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

code> getPhoneByUserId(userId:Int)既然选项是一个单子:

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

...



现在我看到 Scala中的Applicative Functor

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

我不知道何时应该使用它而不是 monad。我想Option和List都是 Applicatives 。你可以举个简单的例子,在选项和列表中使用 apply >,并解释为什么我应该使用而不是 flatMap

解决方案




那么为什么还要用到应用函子呢?有单子吗?
首先,为
提供monad实例根本不可能,我们想要使用的一些抽象 - Validation 是$ b $一个完美的例子。

第二个(也是相关的),使用
最强大的抽象将完成工作只是一个可靠的开发实践。在
原则中,这可能会使优化不会成为
的可能,但更重要的是,它使得我们编写的代码更多
可重用。


在第一段中扩展一下:有时你在单向代码和应用代码之间没有选择。请参阅回答的其余部分,了解您为什么要使用Scalaz的验证(它不能也没有monad实例)来建模
验证。



关于优化点:它会可能会有一段时间,这通常与Scala或Scalaz有关,但请参阅 Haskell的 Data.Binary 的文档:


应用风格有时会导致代码更快,因为 binary
会尝试通过将读取组合在一起来优化代码。


编写应用程序代码可以避免对计算之间的依赖关系做出不必要的声明 - 声称类似的一元代码会提交给您。一个足够智能的库或编译器可以原则上利用这一事实。

为了让这个想法更具体一些,请考虑以下monadic代码:

  case class Foo(s:Symbol,n:Int)

val maybeFoo = for {
s< - maybeComputeS(无论)
n< - maybeComputeN(无论)
}产生Foo(s,n)

-comprehension的解析为如下所示的内容:

  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(_,_))

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

(是的,这个 | @ | 语法非常可怕 - 参见< a href =http://meta.plasm.us/posts/2013/06/05/applicative-validation-syntax/ =noreferrer>这篇博文有一些讨论和选择。)



但最后一点确实是最重要的。挑选能够解决您的问题的至少强大的工具是一个非常强大的原则。例如,有时你确实需要monadic组合,例如在你的 getPhoneByUserId 方法中,但通常你不需要。



遗憾的是,Haskell和Scala目前使单子组合的工作变得比使用适用函子更方便(语法等),但这主要是历史事件的问题,而像 idiom brackets 是朝着正确方向迈出的一步。


I know that Monad can be expressed in Scala as follows:

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

I see why it is useful. For example, given two functions:

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

I can easily write function getPhoneByUserId(userId: Int) since Option is a monad:

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

...

Now I see Applicative Functor in Scala:

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

I wonder when I should use it instead of monad . I guess both Option and List are Applicatives. Could you give simple examples of using apply with Option and List and explain why I should use it instead of flatMap ?

解决方案

To quote myself:

So why bother with applicative functors at all, when we've got monads? First of all, it's simply not possible to provide monad instances for some of the abstractions we want to work with—Validation is the perfect example.

Second (and relatedly), it's just a solid development practice to use the least powerful abstraction that will get the job done. In principle this may allow optimizations that wouldn't otherwise be possible, but more importantly it makes the code we write more reusable.

To expand a bit on the first paragraph: sometimes you don't have a choice between monadic and applicative code. See the rest of that answer for a discussion of why you might want to use Scalaz's Validation (which doesn't and can't have a monad instance) to model validation.

About the optimization point: it'll probably be a while before this is generally relevant in Scala or Scalaz, but see for example the documentation for Haskell's Data.Binary:

The applicative style can sometimes result in faster code, as binary will try to optimize the code by grouping the reads together.

Writing applicative code allows you to avoid making unnecessary claims about dependencies between computations—claims that similar monadic code would commit you to. A sufficiently smart library or compiler could in principle take advantage of this fact.

To make this idea a little more concrete, consider the following monadic code:

case class Foo(s: Symbol, n: Int)

val maybeFoo = for {
  s <- maybeComputeS(whatever)
  n <- maybeComputeN(whatever)
} yield Foo(s, n)

The for-comprehension desugars to something more or less like the following:

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

We know that maybeComputeN(whatever) doesn't depend on s (assuming these are well-behaved methods that aren't changing some mutable state behind the scenes), but the compiler doesn't—from its perspective it needs to know s before it can start computing n.

The applicative version (using Scalaz) looks like this:

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

Here we're explicitly stating that there's no dependency between the two computations.

(And yes, this |@| syntax is pretty horrible—see this blog post for some discussion and alternatives.)

The last point is really the most important, though. Picking the least powerful tool that will solve your problem is a tremendously powerful principle. Sometimes you really do need monadic composition—in your getPhoneByUserId method, for example—but often you don't.

It's a shame that both Haskell and Scala currently make working with monads so much more convenient (syntactically, etc.) than working with applicative functors, but this is mostly a matter of historical accident, and developments like idiom brackets are a step in the right direction.

这篇关于何时以及为什么要在Scala中使用Applicative Functors的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆