tl;dr
g(x)(f(x))
===g <*> f
- 共通の引数を持つ1引数関数たちはReaderとして合成可能であり、ReaderがApplicativeのインスタンスであることを利用している
本編
おい少年と呼び掛けてみたものの、関数型女子高生がいるかもしれないよね。それはさておき、
プログラマたるもの、定期的にFizzBuzzを書きたくなるよね。バズりネタに瞬時に襲い掛かれるように関数型の刃を研いでおく必要があるんだよね。
import cats._ import cats.implicits._ def ~> = (d: Int) => (s: String) => (x: Int) => (x % d == 0).guard[Option].as(s) val f = ~>(3)("Fizz") |+| ~>(5)("Buzz") val g = (default: Int) => (maybe: Option[String]) => maybe.getOrElse(default.toString) val fb = (x: Int) => g(x)(f(x))
できた。
fb(15) // => "FizzBuzz"
内容はだいたい以下の内容に沿ってるからこちらを読んでもらうとする。
勉強するたびにコードがどんどん短くなっていって誉れを感じるよね。
で、fb
を見てもらうと掲題の構造 g(x)(f(x))
の構造になっている。x
が二回も登場する必要はなさそうだから、良い感じに書き直していこう。
g(x)(f(x))
構造をよく見ると、 g(x)
にf
を合成して、最後にx
をもう一度渡しているのと同じになっている。
val fb2 = (x: Int) => (f andThen g(x))(x)
ReaderのApplicative
ところで、f
もg
も、第一引数にInt
を受け取ることがわかっていて、なおかつそれらは同一のx
であることがわかっている。これはReaderのApplicativeになる。
ReaderのApplicativeについては、過去に解説記事を書いていた!!! なんという幸運なんだ:
まあ要するに、Readerというのは、共通の引数を1つ渡してくれる君のこと。f
もg
も、共通の引数x
を要求しているから、Readerだといえる。
そして、ReaderはApplicativeとして振る舞うことができる。どちらかというとReader Monadのほうが有名な気がするけど、Applicative版もある。
で、Applicativeになると何が嬉しいかというと、mapN
という関数を使えるようになる。
val fb3 = (f, g) mapN ((ff, gg) => gg(ff))
mapN
が「最初に共通の引数を渡す」という作業を勝手にやってくれるので、引数x
を消すことができた。
ff
とgg
との型はそれぞれ以下の通り:
ff
:Option[String]
gg
:Option[String] => String
ちなみに、f
とg
との型も示しておこう:
f
:Int => Option[String]
g
:Int => Option[String] => String
mapN
が先頭のInt
を切り出して、内部のff
とgg
との合成だけ考えればよくなった。
そしてff
とgg
とは単に適用するだけでよいので、gg(ff)
と書いて終了だ。
ちょっと順番いじる
ちょっと順番を変えると、関数名をうまく匿名化できる:
val fb4 = g -> f mapN (_(_))
<*>
に変形
ちょっと巻き戻して、fb3
のことを思い出そう:
val fb3 = (f, g) mapN ((ff, gg) => gg(ff))
mapN
の中で、gg
はff
に適用するだけ
このパターンは、<*>
(またの名をap
)に変形できる:
val fb5 = g <*> f
ちょっと表記が独特だけれど、gg(ff)
を適用するという形がg <*> f
として写されているのが心の目で見えるだろうか・・・?(この点Haskellだとgg ff
がg <*> f
になるからパッと見て分かりやすいんだけど)
g <*> f
は、Applicativeの文脈に則ってg
をf
に適用するという意味合いだ。今回の例では、Maybe Applicativeの文脈でg
がf
に適用され、最終的にInt => String
が得られる。
fb5(15) // => "FizzBuzz"
おわり
この記事では、Reader Applicativeを用いて、共通の引数を持つ関数同士の合成を簡単化した。
ちなみに、適用できずに合成するときは、(g, f) mapN (_(_))
ではなく(g, f) mapN ( _ <<< _ )
と書くことになるよ。