「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
筆者も勉強中のため内容については間違いがあるかもしれません。
間違いがある場合、コメント欄にてご指摘いただけると助かります。