先日、Haskellで書かれたおもしろFizzBuzzの事を思い出した。読んだときはよく分からなかったけれど、型クラスへの理解が進んで、結構意味が分かるようになりつつある。そこで、それにまつわる要素をちゃんと勉強することにした。勉強といってもCatsでの使い方なので、数学的な背景などは飛ばすことにする。
分からない要素は大きく2つあった。Alternative
とApplicative (->) r
だ。Alternativeについては以下の記事で勉強した。
このエントリでは、(->) r
のアプリカティブについて学ぶことにする。しかし、今はほとんど事前知識が無いので、いきなりこんなことを言われてもわからない:
(->) r
は、Applicative
のインスタンスである。
?????
というわけで、なるだけ簡単な要素から勉強していくことにした。
そもそも(->) r
ってなんだよ
いきなり(->) r
とか書かれてもぜんぜん分からんので、より簡単な所から考えていこう。->
というのは、Haskellの記法であるから、ちょっとだけHaskellの話をする。
Haskellでr -> a
とは、r
を受け取ってa
を返す1引数関数の型のことである。中置記法ではr -> a
と書くが、前置記法では(->) r a
とも書ける。
ちなみに(->)
単体では、「型を2つ取る型コンストラクタ」である。型コンストラクタとは、型を受け取って初めて実際の型になるもののことで、例えば[]
は型コンストラクタである(([]) a
のように型を与えることで、実際に型となる)。
そして型コンストラクタも、関数と同様、型を部分適用できる。
(->)
型を2つ受け取る型コンストラクタ(->) r
型を1つ受け取る型コンストラクタ(->) r a
===r -> a
型
どうして(->)
でもr -> a
でもなく(->) r
なのかは、いずれ分かる。
とりあえずここからは、(->) r
が関手になること、そしてそのうちそれがモナドになっていくこと、それが多分便利らしいことを確認していこう。
Reader関手
まず最初に、(->) r
が関手であることについて考えていく。関手とは型クラスの1つで、関数fmap
に加えて、いくつか守るべき規則を実装すれば、関手を名乗ることができる(型クラスの説明は、ここでは割愛する)。
ちなみに型クラスの中でも有名なのがモナドだが、より制約が弱い(実装しやすい)ものとしてアプリカティブや
- モナド (表現力が高い) (守るべき制約が強い)
- ↑ 高級
- アプリカティブ
- 関手 (お手軽)
- アプリカティブな関手を、アプリカティブ関手と呼ぶ
- ↓ ドンキで売ってる
イメージとしてはこういう感じである。
とりあえず定義が小さなものから順に考えて、最終的にアプリカティブ関手に辿り着きたい。そこで、まずは(->) r
が関手であることについて考えていく。
関手f
が唯一実装すべきfmap
の定義は fmap :: (a -> b) -> f a -> f b
である。(->) r
が関手になることについて考えているので、f
に実際の型(->) r
を埋めてみると、以下のように変形できる。
fmap :: (a -> b) -> f a -> f b
(->) r
が関手になることについて考えているので、f
には(->) r
が入る(->) r
はr -> ...
なので・・・fmap :: (a -> b) -> (r -> a) -> (r -> b)
なんだか見覚えのある型だ。a -> b
とr -> a
を渡すと、r -> b
が得られる計算とは何だろう?
どうして (->) r
なのか
閑話休題。ちなみにこのあたりで、(->) r
である必要が分かってくる。ただ1つの引数を持つ型コンストラクタでなければ、関手(そしてアプリカティブ、モナド)になれないのだ。
ScalaのライブラリであるCatsのドキュメント Functor を見たほうが、そのことがより際立って見えるはずだ(fmap
がmap
という名称になっていることに注意)。
trait Functor[F[_]] { def map[A, B](fa: F[A])(f: A => B): F[B] }
Functor
は明らかに、1引数を取る型コンストラクタF[_]
を要求している。この型上の制約に合致させるために、部分適用した(->) r
(CatsだとFunction1[In, ?]
という表記になっている。平たく言うと「r
をもらう関数」)である必要があるのだ。
関手の続き
さて(->) r
を関手にするfmap
は、a -> b
とr -> a
を受け取りr -> b
を返すような関数だとわかった。そしてよく見ると、これは関数合成そのものではないか。
fmap :: (a -> b) -> (r -> a) -> (r -> b)
a -> b
があり、r -> a
があるとき、r -> b
が得られる
というわけで、fmap
の実装はこうなる(満たすべきファンクタ則は割愛)。
fmap = (.)
シンプル!
こうして得られた(->) r
の関手は、Reader関手と呼ばれるらしい。何が便利かというと、関数合成ができる。
import cats._ import cats.implicits._ // (->) r is a functor val f = (n: Int) => n * 2 val g = (n: Int) => n + 1 // functor can fmap val fg = f map g // f andThen g fg(10) // 21
CatsだとScalaの色々な都合で、f
してからg
という順序で合成される。Haskellではg
してからf
の順になる。
これだけだとあまりお徳な感じがしないというか、だから何という感じなので、このままアプリカティブを実装しに行こう。
Reader Applicative
Reader Applicativeと言うのが一般的なのかは知らないけど、便宜上ここではそう呼ぼう。
関手にApplicativeを注射してApplicative関手にするためには、2つの関数を追加で実装すればよい。pure
とap
(<*>
)だ。
型表記はHaskellの方がシンプルなので、そちらで書こう:
pure :: a -> f a (<*>) :: f (a -> b) -> f a -> f b
pure
を実装する
pure
は、何でもないa
型の値を、f a
型に昇格させるような関数だ。
これも例によって、f
を(->) r
で置換して、型から内容を考えてみよう。
pure :: a -> f a
(->) r
が関手になることについて考えているので、f
には(->) r
が入る(->) r
はr -> ...
なので・・・pure:: a -> r -> a
a
型を受け取り、r
型を受け取り、a
型を返す関数- 2つ引数を取るけど、2つめは捨てて1つめの引数だけ返す関数だと言えそう
これはconst
だ。constは畳み込みにも出現する面白い概念だ:
というわけでpure
の実装は以下の通りになる。
pure = const
シンプル。
<*>
/ ap
を実装する
pure
を実装し終わったところで、Applicativeのもう一つの柱である<*>
を実装しよう。文脈によってはap
と書かれることもあるぞ。
(<*>) :: f (a -> b) -> f a -> f b
(->) r
が関手になることについて考えているので、f
には(->) r
が入る(->) r
はr -> ...
なので・・・(<*>) :: (r -> a -> b) -> (r -> a) -> (r -> b)
何これ?(ヒント!r ->
を取り除くと(a -> b) -> a -> b
という素直な関数適用の形をしているぞ。)
(->) r
は関数なので、実際に使われるときはf <*> g
というふうに書かれるはず。f
とg
それぞれに型の定義を割り当ててみると・・・
f :: (r -> a -> b)
g :: (r -> a)
f <*> g :: (r -> b)
となる。fmap
を実装したときと異なり、f
もg
もr
を要求している。だから単純な合成では実装できないぞ。
f <*> g
全体ではr
を引数に受けるから、\r -> ...
という形式になりそう。f
とg
はそれぞれr
を引数に受けるから・・・f r
はa -> b
になるg r
はa
になる
g r
の型がa
なので、f r
に渡せばb
となり、全体の型r -> b
が完成する\r -> f r (g r)
と書いて完成
というわけで(天下り的だけど) <*>
の定義は以下の通りになった。
f <*> g = \r -> f r (g r)
r
型の値を受け取り、f
にr
を部分適用して、さらにg
にr
を適用した値を引数に渡している。
ちなみに、f <*> g <*> h
のように引数を増やしていくと、次のような形になる:
f <*> g <*> h = \r -> f r (g r) (h r) f <*> g <*> h <*> k = \r -> f r (g r) (h r) (k r)
どういう働きをするか、おぼろげながら分かってきた。普通の関数適用のf g h
という形と見比べてみてほしい。
例によってApplicative則は割愛する。
Reader Applicative 完成
さてApplicativeが完成したのだけれど、これもいまいち便利さがよく分からない。どういう時に使うのだろう。
関数適用の形をしつつ、全てのf
やg
にr
を渡してくれるというのがミソのようで、
{ type Logger= String => Unit val log: Logger = println val f = (l: Logger) => { l("executing f") 42 } val g = (l: Logger) => { l("executing g") 43 } val h = (l: Logger) => { l("executing h") 44 } val F = (l: Logger) => (fx: Int) => (gx: Int) => (hx: Int) => { l("combining together") fx :: gx :: hx :: Nil } val Ffgh = F <*> f <*> g <*> h Ffgh(log) // 42 :: 43 :: 44 :: Nil }
これを実行すると、標準出力に次のように表示されつつも、Ffgh(log)
は42 :: 43 :: 44:: Nil
を返す。
executing f executing g executing h combining together
これは、F(f, g, h)
という関数適用を一般化しつつ、第一引数としてl: Logger
が添加される、というイメージである。
ちなみにF
とFfgh
は以下のようにも書くことができる。
val F2 = (fx: Int, gx: Int, hx: Int) => { fx :: gx :: hx :: Nil } val F2fgh = (f, g, h) mapN F2
ただし、mapN
を使って関数を自動的に"F2
はl: Log
を受け取る形にできない。したがってF2
自体には注入を行えない。
executing f executing g executing h
まとめ
(->) r
は、特に型クラスとして見る立場からはReaderと呼ぶ(->) r
は、fmap
を(.)
で定義することで、関手のインスタンスにできる(->) r
は、うまく定義することでApplicativeのインスタンスにできる- Applicativeは、関数適用の自然な拡張である
- Applicativeとしての
(->) r
は、関数適用の各要素にr
型の引数を添加するように拡張する
このままReaderモナドに突入してもよかったけれど、もう力尽きたので一旦Applicativeにしたところで満足しておく。