関数の合成の誤解
さて、先日のポストで書いたコードなんですが、
pure f <*> pure g <*> (ZipList xs)
これ、コンパイルエラーがでます。前にもこんな様なコードを書いておこられて、はっきりとした理由がわからないままあきらめていたのですが、今回理由がわかったので書いておきます…<*>の型は:
(<*>) :: (Applicative f) => f (a -> b) -> f a -> f b
なのですが、上の例を書いたときの僕のインテンションは:
f :: a -> b
g :: c -> a
であるところのfとgがうまいこと結合してくれて、(f g):: c -> bになってやくれはしないかという期待があったわけなんです…ここで、話をもう少しわかりやすくするために、
f = (+1) :: Num a => a -> a
g = (*5) :: Num a => a -> a
としてみましょう。実は、問題の種はこれらの関数の型にあるのです...<*>の右辺が定数の場合は、概念的には:
pure (+1) <*> pure 5 = zipWith (repeat (+1)) (repeat 5) = repeat ((+1) 5) = repeat 6
となるわけなんですが、右辺も関数だったりすると:
pure (+1) <*> pure (*5) = zipWith (repeat( +1 ) ) (repeat ( *5 ) ) =repeat ( ( +1 ) ( *5 ) )
までは進むのですが、問題になるのは、+1がパラメータとして期待しているのはNum型の値であって、Num->Numな関数ではないわけです。つまり、こういった関数合成はHaskellのタイプシステムでは勝手にはやってくれないということらしいです。もしこれが勝手に動くのだったら、(.) :: (b -> c) -> (a -> b) -> a -> cなんていうオペレータの出番はないですもんね…
問題の肝であるところの関数の合成がしたいところをラムダ式に書き換えてみると:
(\x -> x + 1) (\y -> y * 5)
そして、合成した結果僕が期待していたのは:
(\y -> (y * 5) + 1)
なわけですが、勝手にはやってくれないようです…
最初の例に戻って、
pure f <*> pure g <*> (ZipList xs)
が以上説明したような解釈になる理由の一つは<*>がinfixl型の中置関数だからのようです。
pure g <*> (ZipList xs)
の結果は関数ではなく値なので、(ここではg:: a->bを想定しています...)これを動かす方法としては:
pure f <*> (pure g <*> (ZipList xs))
とやるか、
pure (.) <*> pure f <*> pure g <*> (ZipList xs)
という手もあります…
つまり、
(a -> b -> c -> d) (a) (b) = (c->d)
とか、
((a -> b) -> (c -> a) -> c -> b) (a -> b) (c -> a) = c ->b
ということはできても、
(a -> b) (c -> a) = c -> a
ということは関数へパラメタを渡すという行為の範疇ではやってくれないということのようです…こうやって書いてみると、「そんなの当たり前じゃん」と思うのですが、Applicativeだの何だのとレイヤーがかぶさってしまうと、その当たり前が見えなくなっちゃったわけですね...
コメントでいただいたようにGHCにはすでにmap f $ map g $ xs -> map f.g xsと自動変換してくれるようなオプティマイザーもついているようなので、またしても、欲しいと思ったものはすでにあったという結末でした…
勉強になりました。