Lambdaカクテル

京都在住Webエンジニアの日記です

Invite link for Scalaわいわいランド

おい少年、g(x)(f(x))になるパターンはScalaのCatsでg <*> fにできるぜ

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"

内容はだいたい以下の内容に沿ってるからこちらを読んでもらうとする。

itchyny.hatenablog.com

勉強するたびにコードがどんどん短くなっていって誉れを感じるよね。

で、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

ところで、fgも、第一引数にIntを受け取ることがわかっていて、なおかつそれらは同一のxであることがわかっている。これはReaderのApplicativeになる。

ReaderのApplicativeについては、過去に解説記事を書いていた!!! なんという幸運なんだ:

blog.3qe.us

まあ要するに、Readerというのは、共通の引数を1つ渡してくれる君のこと。fgも、共通の引数xを要求しているから、Readerだといえる。

そして、ReaderはApplicativeとして振る舞うことができる。どちらかというとReader Monadのほうが有名な気がするけど、Applicative版もある。

で、Applicativeになると何が嬉しいかというと、mapNという関数を使えるようになる。

val fb3 = (f, g) mapN ((ff, gg) => gg(ff))

mapNが「最初に共通の引数を渡す」という作業を勝手にやってくれるので、引数xを消すことができた。

ffggとの型はそれぞれ以下の通り:

  • ff: Option[String]
  • gg: Option[String] => String

ちなみに、fgとの型も示しておこう:

  • f: Int => Option[String]
  • g: Int => Option[String] => String

mapN が先頭のIntを切り出して、内部のffggとの合成だけ考えればよくなった。 そしてffggとは単に適用するだけでよいので、gg(ff)と書いて終了だ。

ちょっと順番いじる

ちょっと順番を変えると、関数名をうまく匿名化できる:

val fb4 = g -> f mapN (_(_))

<*>に変形

ちょっと巻き戻して、fb3のことを思い出そう:

val fb3 = (f, g) mapN ((ff, gg) => gg(ff))
  • mapNの中で、ggffに適用するだけ

このパターンは、<*>(またの名をap)に変形できる:

val fb5 = g <*> f

ちょっと表記が独特だけれど、gg(ff)を適用するという形がg <*> fとして写されているのが心の目で見えるだろうか・・・?(この点Haskellだとgg ffg <*> fになるからパッと見て分かりやすいんだけど)

g <*> f は、Applicativeの文脈に則ってgfに適用するという意味合いだ。今回の例では、Maybe Applicativeの文脈でgfに適用され、最終的にInt => Stringが得られる。

fb5(15) // => "FizzBuzz"

おわり

この記事では、Reader Applicativeを用いて、共通の引数を持つ関数同士の合成を簡単化した。

ちなみに、適用できずに合成するときは、(g, f) mapN (_(_))ではなく(g, f) mapN ( _ <<< _ )と書くことになるよ。

blog.3qe.us

★記事をRTしてもらえると喜びます
Webアプリケーション開発関連の記事を投稿しています.読者になってみませんか?