Applicative勉強中
ちょっと前から、時間を見つけてはApplicativeについての論文を読んでいます。まだいくつか(も)疑問点が残っているので、まだまだ説明はできないのですが、いくつか面白いことが出ていたので...小出しに書いていこうと思います。
概要としては、ApplicativeはFunctorとMonadの間にあるもので、全てのApplicativeはFunctorだし、全てのMonadはApplicativeだけど、その逆は成り立たないようです。つまり
Functor > Applicative > Monad :ここでA > B = B is subset of A
という存在らしいです。そしてMonadクラスをApplicativeとしてみるときにカギになるのがap関数というもので:
ap :: Monad m => m (a -> b) -> m a -> m b ap :: (Monad m) => m (a -> b) -> m a -> m b ap = liftM2 id liftM2 :: (Monad m) => (a1 -> a2 -> r) -> m a1 -> m a2 -> m r liftM2 f m1 m2 = do { x1 <- m1; x2 <- m2; return (f x1 x2) }
となっています。ここで、僕が引っかかっているのは、何でliftM2 idが許されるのかということです。
id :: a -> a
なので、liftM2の第一引数であるところの(a1 -> a2 -> r)を満たすはずがないと思うのですが…
ということで、試しにliftM2のサンプルを書いてみました。まずはまとも版:
module Main where import Char import Control.Monad getInputNum :: IO Int getInputNum = do putStrLn "Type a number" input <- getLine return $ digitToInt $ head $ filter (isDigit) (input ++ "0") main = (liftM2 (+) getInputNum getInputNum) >>= putStrLn.show
ここでは+演算子:
(+) :: Num t => t -> t -> t
をIOモナド型にリフトしています。数字を2つコマンドプロンプトからもらってきて、その合計を返します。+は入力を2つ取るのでliftM2の第一パラメタとして適格ですね。
ここで、liftM2のap的用法をするようにサンプルを変えてみます。
module Main where import Char import Control.Monad getInputNum :: IO Int getInputNum = do putStrLn "Type a number" input <- getLine return $ digitToInt $ head $ filter (isDigit) (input ++ "0") ioPlus :: IO (Int -> Int) ioPlus = return (+ 1) main = (liftM2 (id) ioPlus getInputNum) >>= putStrLn.show
liftM2の第一引数をapと同じようにidにして、第二引数をIO(Int -> Int)を返すioPlusに変えてみました。今度はコマンドプロンプトから数字を一つもらって、それに1を足したものを表示します...これが、コンパイルしてちゃんと動きます。不思議です。
ここでidを(+1)に変えてみました。両方引数を一つとって値を返す関数です。これはどういうわけかコンパイルしません。どうやらidに何か隠れているようです。と思ってソースを見ると、これがあっけないほどシンプルです:
id :: a -> a id x = x
(+1)との違いらしい違いといえば型がNumに制限されていないところでしょうか…
ところで、apを理解しようとしてやってみたことが、apを展開してみることです。以下のようになります:
ap mf my= liftM2 id mf mx = mf >>= \f -> mx >>= \x -> return (id f x)
ここで、id:: a -> aなので、
ap mf my= liftM2 id mf mx = mf >>= \f -> mx >>= \x -> return ( f x)
と書き換えられ、型の問題を除けば、コード自身は目的を達成していることがわかります…
このことから考えられるのは、
id fというのはつまりa->aを返す関数だとも取れるということで、liftM2 idをHaskellコンパイラが見てidの型として(a -> a)を選択したとすれば、
id :: (a -> a) -> a -> aで、パラメタを2つとる関数と見えるので、liftM2の第一パラメタとして適格とみなされ、id:: (a->a)->(a->a)なので、id::a->aというid自身の定義にもコンプライアントであるといえると…
型推論の強力さというか、Haskellの型表記システムの強力さというか、ここまで考えて作られてるのか、ここまでわかってこうゆうコードを書いてるのか、なんというか、途方にくれてしまうほど強力だと思ったしだいです。
ではでは。