web-dev-qa-db-ja.com

ScalaでApplicative Functorを使用するタイミングと理由

Monadは、Scalaで次のように表現できることを知っています。

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

なぜそれが役立つのかわかります。たとえば、次の2つの関数がある場合:

_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]
}
_

私はそれをいつ使うべきか疑問に思いますの代わりに monad。 OptionとListは両方ともApplicativesであると思います。 OptionとListでapplyを使用する簡単な例を挙げて説明してくださいwhy使用すべきですではなくflatMap

65
Michael

自分自身を引用する

それでは、モナドがあるのに、なぜ応用ファンクターに悩まされるのでしょうか?まず第一に、使用したい抽象化の一部にモナドインスタンスを提供することは不可能です。Validationは完璧な例です。

第二に(そして関連して)、仕事を成し遂げる最も強力でない抽象化を使用することは、単なる堅実な開発プラクティスです。原則として、これにより、他の方法では不可能な最適化が可能になりますが、さらに重要なことは、記述したコードをより再利用可能にすることです。

最初の段落を少し拡張するには、モナドと適用コードのどちらかを選択できない場合があります。検証をモデル化するためにScalazのValidation(モナドインスタンスを持たず、持つこともできない)を使用する理由については、残りの その回答 を参照してください。

最適化ポイントについて:これはおそらく、これがScalaまたはScalazに一般的に関連するのにしばらくかかりますが、たとえば HaskellのData.Binary のドキュメントを参照してください。

binaryは読み取りをグループ化することでコードを最適化しようとするため、applicativeスタイルを使用するとコードが高速になる場合があります。

適用可能なコードを書くことで、計算間の依存関係について不必要な主張をすることを避けることができます-同様のモナドコードがあなたに約束する主張。十分にスマートなライブラリまたはコンパイラcouldは、原則としてこの事実を利用します。

このアイデアをもう少し具体的にするために、次のモナドコードを検討してください。

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

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

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

ここでは、2つの計算間に依存関係がないことを明示的に述べています。

(そして、はい、この|@|構文は非常に恐ろしいものです。いくつかの議論と代替案については このブログ投稿 を参照してください。)

ただし、最後のポイントが本当に最も重要です。問題を解決するleast強力なツールを選択することは、非常に強力な原則です。場合によっては、たとえばgetPhoneByUserIdメソッドでモナド合成を本当に必要とすることがありますが、多くの場合必要ありません。

HaskellとScalaの両方が現在、適用可能なファンクターを使用するよりもモナドを使用する方がはるかに便利です(構文的になど)が残念ですが、これは主に歴史的な事故と開発の問題です イディオム括弧 は、正しい方向へのステップです。

77
Travis Brown

Functorは、計算をカテゴリに変換するためのものです。

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

そして、それは1つの変数の関数に対して完全に機能します。

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

Applicative functorは、特別な値(カテゴリ内の値)をリフト関数に適用するためのファンクターです。

24
Yuriy