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 ( _ <<< _ )と書くことになるよ。