先日、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 -> aa型を受け取り、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にしたところで満足しておく。