「Applicative
とMonad
は何が違うのか?」という疑問についての自分なりのまとめ。
型制約の強さの違い
Haskellの型クラスは「特定の名前と型がついた関数が実装されているか」で型をグループ化する概念。言ってみればJavaのinterfaceのようなもので、ある型がその型クラスに属するためにはその型クラスによって指定された名前と型を持つ関数を定義する必要がある。
Applicative
とMonad
は型クラスであって、それぞれが要求する関数が異なる。
型クラスの定義を読む
GHCi(7.10.2)によると、Applicative
の定義は以下。
Prelude> :i Applicative
class Functor f => Applicative (f :: * -> *) where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
(*>) :: f a -> f b -> f b
(<*) :: f a -> f b -> f a
(ちなみに小文字で書かれたf
やa
やb
は型変数で、ある具体的な型(例えばIO
とかMaybe
とか[]
とか)が入る。同じ型変数には同じ型が代入されるので、(<*>) :: Maybe (a -> b) -> IO a -> Maybe b
のような代入は起こらない。)
(型の定義にHoge a => a
と書かれている場合、「a
は型クラスHoge
に属する」あるいは「a
は型クラスHoge
のインスタンスである」と読める。Javaでいうところの<T extends Hoge>
のようなもの。)
このApplicativeの定義は次のことを言っている。
次の条件を満たすf
がApplicative
になる:
-
f
はFunctor
に属する -
a -> f a
という型持つpure
という関数が実装されている -
f (a -> b) -> f a -> f b
という型を持つ<*>
という関数が実装されている -
f a -> f b -> f b
という型を持つ*>
という関数が実装されている -
f a -> f b -> f a
という型を持つ<*
という関数が実装されている
また、ある型m
がMonad
になるためには、以下で定義された関数を実装しないといけない。
Prelude> :i Monad
class Applicative m => Monad (m :: * -> *) where
(>>=) :: m a -> (a -> m b) -> m b
(>>) :: m a -> m b -> m b
return :: a -> m a
fail :: String -> m a
このMonadの定義は次のことを言っている。
次の条件を満たすm
がMonad
になる:
-
m
はApplicative
に属する -
m a -> (a -> m b) -> m b
という型持つ>>=
という関数が実装されている -
m a -> m b -> m b
という型を持つ>>
という関数が実装されている -
a -> m a
という型を持つreturn
という関数が実装されている -
m a
という型を持つfail
という関数が実装されている
Monad
に属すためには、型クラスApplicative
に含まれる関数も実装しないといけないし、それに加えて型クラスMonad
に含まれる関数も実装しないといけない。
型クラスの定義から、「Monad
に属する型m
は、Applicative
に属する型f
よりも強い制約を持っている」という違いがあることがわかった。
「分岐」ができるかどうか
著名なHaskellerであるkazu-yamamotoさんのブログ記事には次のような言及がある。
逐次と反復を実現するのが、Applicative 型クラスです。
分岐...を実現するのが、Monad です。
Applicative にできないことは、前のコンテナの計算結果を用いて、後のコンテナが振る舞いを変えることです。別の言い方をすれば、前のコンテナの計算結果を、後のコンテナに渡す方法がありません。これを実現するのが Monad なのです。
ここでの「分岐」とは「前のコンテナの計算結果を用いて、後のコンテナが振る舞いを変える」のことと思われるが、これは一体どういうことだろう。
>>=
の有無による違い
Applicative
とMonad
の大きな違いに、>>=
の有無がある。
Monad
は>>=
を使えるが、Applicative
は使えない。
これによって、行うことのできる計算の種類に影響がある。
>>=
の型を見る
>>=
の型宣言は(>>=) :: m a -> (a -> m b) -> m b
となっている。(ここでm
はMonad
に属する。)
引数にa -> m b
という関数を取ることができるというところがミソで、「m a
」と「m a
の実行結果a
によって次に実行するm b
を選択する関数a -> m b
」を>>=
で結合すると、m b
が得られる。
\l -> if length l > 0 then putStrLn l else putStrLn "(empty)"
例えば、こんなふうに型a
の値(ここではl
)によって、次に実行される計算(putStrLn l
かputStrLn "(empty)"
か)を切り替えることができる。
これとgetLine
を>>=
で結合して使うことで、getLine
を実行した結果によって異なる処理をするIO ()
が書ける。
Prelude> getLine >>= (\l -> if length l > 0 then putStrLn l else putStrLn "(empty)")
hoge
hoge
Prelude> getLine >>= (\l -> if length l > 0 then putStrLn l else putStrLn "(empty)")
(empty)
一方、例えばApplicativeの<*>
の型宣言は(<*>) :: f (a -> b) -> f a -> f b
で、a
によってb
を変えることはできるけど、f b
を変えることはできない。
例えば、
Prelude> (++) <$> getLine <*> getLine
foo
bar
"foobar"
こんな感じでm a
とm b
を連結させて逐次実行させることはできるけど、m a
の実行結果a
に依存してm b
を変えることはできない。
結局何が違うの?
型の持つパワーが違う。
Monadが持つ>>=
はa -> m b
という関数を取ることができるため、m a
を実行して得られるa
にm b
が依存するような処理が書ける。Applicativeにはそれが書けない。
ちなみに、Monadを使っていても前の処理の結果に後ろの処理の実行内容が依存しないのであればApplicativeが提供している関数のみでコードを書いたほうが綺麗で安全。(参考:Applicativeのススメ)
Disclaimer
筆者も勉強中のため内容については間違いがあるかもしれません。
間違いがある場合、コメント欄にてご指摘いただけると助かります。