この前ScalazにFree Applicativeが入りました。
https://github.com/scalaz/scalaz/blob/8f339d9ca2/core/src/main/scala/scalaz/FreeAp.scala
Free MonadのApplicativeなやつです。それの話をします
Haskellのekmett/freeには、ある程度前からありますね。
https://github.com/ekmett/free/blob/v4.10.0.1/src/Control/Applicative/Free.hs
あと、元論文も貼り付けておきます
http://www.paolocapriotti.com/assets/applicative.pdf
論文の日付が2013年4月と書いてあって?かなり最近みたいですね。
さて、入ったからには、実用的に使ってみたいですね?Free Monadはもうググったりすれば結構な量の例がでてくると思います。Free Monadが実用的で便利なのは、もう常識ですね?*1
この論文や、ekmett/freeの中にも、Free Applicativeの例がでてくるのですが、まだイマイチうまく説明できるほどには理解できていません。
さて、まずはFree Monadのおさらいというか、Free Monadと比べてFree Applicativeの説明を自分なりにしてみると
- Free MonadをDSLとして使う場合、まず適当な代数的データを作って、それをFunctorにする(もしくはCoyonedaの力をかりてFunctorにする)
- そのデータを、Freeモナドに当てはめたものを使って、プログラムを作成
- 最後にそのプログラムを走らせるために
def foldMap[ M[_] ](f: S ~> M)(implicit S: Functor[S], M: Monad[M]): M[A]
https://github.com/scalaz/scalaz/blob/v7.1.0/core/src/main/scala/scalaz/Free.scala#L142 というメソッドに、インタプリタであるscalaz.NaturalTransformationを渡して実行 - 最後に渡すNaturalTransformationを変えることにより、動作を変える事ができて便利
という感じだと思います。上記はあくまで典型的な使い方の一つであり、Free Monadそのものは、色んなことに使えると思いますが、そこは割愛します。
さてFree Applicativeの場合でも、大体上記で書いたこと当てはまるとおもうのですが、違いは
- Free Monadの場合はFunctorである必要があったが、Functorである必要すらない https://github.com/scalaz/scalaz/blob/8f339d9ca23525fbcdbc31a5ee470cd11b30c6b5/core/src/main/scala/scalaz/FreeAp.scala#L85
- 最後の実行のためのインタプリタが要求するのが、MonadではなくApplicativeである https://github.com/scalaz/scalaz/blob/8f339d9ca23525fbcdbc31a5ee470cd11b30c6b5/core/src/main/scala/scalaz/FreeAp.scala#L15
という点です。前者の「Functorである必要すらない 」は、そもそもFree Monadの場合、Coyonedaがあったりすることによりあまりそこ負荷にならないので、どうでもいい感じがあります。重要なのは後者です。
MonadとApplicativeの関係として、世の中には「Monadにはならないけれど、Applicativeにはなる」というものが存在します。なので、自分の理解としては
「もしインタプリタに"Monadにはならないけれど、Applicativeにはなる"ものが使いたくなったときこそ、Free Applicativeを使うときだ!」
という理解です。
さて「Monadにはならないけれど、Applicativeにはなる」といって思いつくのが、scalaz.Validationですね?
というわけで、例を思いついたので、まずコード貼っておきます。
簡単に何やってるか説明すると
- Jsonからcase classに変換する処理してるだけ
- その際、argonautはエラーを一つしか保持しない、play-jsonはエラーをaccumulateする、などの違いがある
- KleisliにValidationNelを当てはめたものと、scalazのEitherを当てはめたインタプリタの2種類を最後に渡す、ということをしている
- それにより「エラーをaccumulateするかどうか?」というエラーに関する処理と、「正常系のcase classを組み立てたりする処理」を、Free Applicativeによって分離した!と言えるはず?
以上ですが、デメリットというか自分でも納得がいっていない点として
- (Scalaの型推論が残念なことなどが影響して)大したことしてないのに、だいぶコード量が多い
- わざわざこの程度のことをするためだけに、本当にFree Applicative必要なのか
- そもそも「後からエラーをaccumulateするか?一つでもエラーが出たら終了するか?」というのを切り替えたい需要なんてそんなに発生しなそう(つまりあまり実用的な例と言えない?)
- 副作用がある例ではないので、accumulateするのをデフォルトにしておいて、1つだけ欲しいなら1つだけ取り出すでも(細かいパフォーマンスを除いて)問題ないのでは?
と、色々だめな感じです。というわけで書いた自分自身も全くこの例に納得がいかないので、この記事のタイトルが
「わかりやすい例を思いつきたかった」
となりました・・・。
誰かが、Free Applicativeのもっと面白いわかりやすい実用的な例の解説を書いてくれるのを期待してます。
以下余談的なもの
twitter検索したらでてきたtweet
Free Applicativeが役に立つ場面を発見した
Free Applicativeの論文の最後のRelated workにoptparse-applicativeが紹介されていた
(以前それのScala版紹介した Scalaにおいて一番関数型というかHaskellっぽいコマンドラインオプション解析のライブラ )
https://github.com/pcapriotti/optparse-applicative
ので、改めてoptparse-applicative自体や、コマンドラインオプションの解析にFree Applicativeを実用する方向を、今後考えてみたい
*1:Free Applicativeの論文にもそういう感じで書いてあって面白い