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の型表記システムの強力さというか、ここまで考えて作られてるのか、ここまでわかってこうゆうコードを書いてるのか、なんというか、途方にくれてしまうほど強力だと思ったしだいです。

ではでは。