The Road to Monads: Applicativeについて

この記事は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)といってるけど、このbc -> d -> e型だった場合について考えよう。 一回<*>を適用すると、返り値はf bだけどこれは実際にはf( c -> d -> e )の型。 あれ?これってさっきと同じ形じゃない? そう、そのとおりで、cad -> ebと置き換えると、<*>をもう一回適用することでf(d -> e)がでて、結局f eの値が出ることになる。 その過程で、入力にf af bf cf 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

purefmapで書いたこれらが等価であることがわかる。 <$>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で書いたMaybeApplicative Maybeについても考えることができる (バナナのナス、バナナスみたいだ)。 Nothingが片方の項にあるならNothing、それ以外はJustを返せばいいので

instance Applicative Maybe where
  pure = Just
  (Just f) <*> (Just x) = Just (f x)
  _ <*> _ = Nothing

はい。 定義に沿った結果が返せているのでいい感じだね。

具体例についてはまたどっかで追記するかもしれない。

法則について

守らなきゃいけない法則がある。 これはHaskellというよりは、圏論からの要請っぽい。

  1. 恒等的な関数の存在
  pure id <*> x == x

これはFunctor側のfmapで確かめられていても、こっちで確かめなきゃいけない。

  1. 合成則
  pure (.) <*> u <*> v <*> w == u <*> (v <*> w)

うーんと、何が言いたいかいまいちよくわからない感じがするけど(u . v) w == u (v w)とFunctorなしの表記にするとわかりやすいのかな。 ここでuvはapplicative functorの中の関数なので、先に関数を合成して計算しても、関数の結果に関数を適用しても値が同じことを保証しなければならないことを言っている。 参照透過性とかと関係あるかは不明。

  1. 準同型
  pure f <*> pure x == pure (f x)

気持ち的には自明っていいたい!が証明は必要。 pure f <*> yとすれば、f yからf (f x)よりpure (f x)っぽい。

  1. 交換則
  f <*> pure x == pure ($ x) <*> f

割と意味わからん。ここはあとで追記。

補足

要はApplicative Functorは一つの国みたいなもので、pureはそこに連れて行くためのコンストラクタ、<*>はその中で関数を適用するための演算子として捉えるとわかりやすいのかな。

おわり。