この記事はThe Road to Monadsを意訳したものであり、RICORA Advent Calendarの9日目の記事です。
Applicative Functor
FunctorはWrapperで、fmapを使えば(a -> b)->(f a -> f b)
という風にFunctorの要素に対して関数を適用できたのだった。
もちろん、これはカリー化されているので結局(a -> b) -> f a -> f b
と同じことである。
でもこれだと問題があって、例えば
f :: Int -> Int -> Int
f a b = a * b
みたいな関数をMaybe Int
で置き換えて使おうと思ったときに使えない。
というか単純に一変数関数にしか使えん。
これはけっこうだるい。
そこで、その代替策がApplicativeなのだがこれもこれとして理解し難い。 まずは関数定義から見てみよう。
class functor f => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
ここで()に囲まれているのは中置関数(ここでは演算子とよぶ)であることを思い出そう。
すると、まずpure
はある型a
からFunctor a
型に移すようなものだとわかる。
pure
はfunctorのデフォルトであるべき(らしい)なので、簡単な例だとMaybe
に対して
pure a -> Just a
である。どこがpure
なのかはいまいちよくわからない。
Haskellでは関数も型なので、こういうこともできる。ある項+1を取れば、
pure (+1) -> Just (+1)
このJust(+1)
の型はMaybe(a -> a)
である。
うん。まあなんとなくはわかった感がある。
次は(<*>)
について。
なにこいつ、なまこっぽいみたいな感想が先に立つけどまあうん。
実はよく見るとfmap
に似ていることがわかる。
fmap :: (a -> b) -> f a -> f b
<*> :: f (a -> b) -> f a -> f b
ここでの違いは、もう関数がFunctorに埋め込まれているかどうかだけ。
だから実は
pure a -> f a
を思い出せば、型システムから
fmap f x = pure f <*> x
であることがわかる。
具体例を作ってみよう。
fn :: a -> b
に対して
pure fn :: f (a -> b)
なので、
pure fn <*> :: a -> f b
fmap fn :: a -> f b
がいえる。ふう。もちろん、ここでf
はFunctorの型でa
は関数の引数である。
いや、なんの役に立つねん。
うーんと、実はなまこ<*>
の重要性は関数を取ることにある。
<*>
の引数はf(a -> b)
といってるけど、このb
がc -> d -> e
型だった場合について考えよう。
一回<*>
を適用すると、返り値はf b
だけどこれは実際にはf( c -> d -> e )
の型。
あれ?これってさっきと同じ形じゃない?
そう、そのとおりで、c
をa
、d -> e
をb
と置き換えると、<*>
をもう一回適用することでf(d -> e)
がでて、結局f e
の値が出ることになる。
その過程で、入力にf a
、f b
、f c
、f d
の値が必要になる。
これって引数が多い関数じゃない?そう。
だから再帰的なアプローチを関数に対して取ることで(!?)うまく引数の多い関数を実装できるような仕組みができているのだった。
まあ、正直に言えばカリー化そのものなんだけど、カリー化はFunctorの中でも動かせるよねという話。
具体例
具体的な例を見よう。
pure (+) <*> Just a <*> Just b
== (Just (+) <*> Just a) <*> Just b
== Just (+a) <*> Just b
== Just (a+b)
ここで型を確かめておけば、Just (+a) :: Maybe (Int -> Int)
なので、<*> :: f (Int -> Int) -> f Int -> f Int
に対してf Int
であるJust (a+b)
がちゃんと返ってくれる。
これは別にfmap
でもいい気もするがそれはさておき。
また、こんな関数を考えると
quadratic :: Int -> Int -> Int -> Int -> Int
quadratic a b c x = a * x ^ 2 + b * x + c
pure
とfmap
で書いたこれらが等価であることがわかる。
<$>
はfmap
の略記。
quadratic <$> (Just 3) <*> (Just 4) <*> (Just (-2)) <*> (Just 9)
pure quadratic <*> (Just 3) <*> (Just 4) <*> (Just (-2)) <*> (Just 9)
== Just(quadratic 3 4 (-2) 9)
さらにApplicative Functorで書いたMaybe
、Applicative Maybe
についても考えることができる
(バナナのナス、バナナスみたいだ)。
Nothing
が片方の項にあるならNothing
、それ以外はJust
を返せばいいので
instance Applicative Maybe where
pure = Just
(Just f) <*> (Just x) = Just (f x)
_ <*> _ = Nothing
はい。 定義に沿った結果が返せているのでいい感じだね。
具体例についてはまたどっかで追記するかもしれない。
法則について
守らなきゃいけない法則がある。 これはHaskellというよりは、圏論からの要請っぽい。
- 恒等的な関数の存在
pure id <*> x == x
これはFunctor側のfmap
で確かめられていても、こっちで確かめなきゃいけない。
- 合成則
pure (.) <*> u <*> v <*> w == u <*> (v <*> w)
うーんと、何が言いたいかいまいちよくわからない感じがするけど(u . v) w == u (v w)
とFunctorなしの表記にするとわかりやすいのかな。
ここでu
とv
はapplicative functorの中の関数なので、先に関数を合成して計算しても、関数の結果に関数を適用しても値が同じことを保証しなければならないことを言っている。
参照透過性とかと関係あるかは不明。
- 準同型
pure f <*> pure x == pure (f x)
気持ち的には自明っていいたい!が証明は必要。
pure f <*> y
とすれば、f y
からf (f x)
よりpure (f x)
っぽい。
- 交換則
f <*> pure x == pure ($ x) <*> f
割と意味わからん。ここはあとで追記。
補足
要はApplicative Functorは一つの国みたいなもので、pure
はそこに連れて行くためのコンストラクタ、<*>
はその中で関数を適用するための演算子として捉えるとわかりやすいのかな。
おわり。