モナドとApplicativeの違い
ここまで,Applicativeをモナドの代わりに使うにはどうすればいいかを説明してきました。しかし,Applicativeはモナドのすべての役割を代替できるわけではありません。Applicativeにはjoin関数や>>=演算子に相当する機能がないからです。
またApplicativeクラスのインスタンスとMonadクラスのインスタンスの間に成り立つべき法則から,Applicativeスタイルの「f <$> x1 <*> ... <*> xn」という式では,必ずx1からxnまでのすべての計算を行われます。特定の計算を条件分岐で除去することはできません。一方,モナドでは「do {b <- mb; if b then m1 else m2}」のように,条件分岐を使って特定の計算を取り除くことができます。モナドを使って書かれた処理をApplicativeスタイルに変えるときには,条件分岐を排除する必要があります(参考リンク)。
このようにApplicativeはモナドよりも機能が貧弱です。モナドでしかできない処理は依然としてあります。
もっとも,モナドよりも機能が弱いことは必ずしも悪いことではありません。Applicativeを使うことで,「その処理にはモナドが不要である」というメッセージをソースコードの読み手に伝えられます。ソースコードの意図を明確にするために,Applicativeスタイルで表現できる処理であれば積極的に使うようにしていきましょう。
なお,モナドに渡す関数は「a -> f b」という型であるのに対し,Applicativeに渡す関数には「f (a -> b)」型が要求されます。このため,モナドを使ってApplicativeに渡す関数を定義する場合には,そうした違いを意識する必要があります(参考リンク)。
Functor,Applicative,Monadの階層化 従来は「FunctorをMonadクラスのスーパークラスにしたほうがよいのではないか」という意見がありました。しかし,Applicativeが登場したことで,この意見は「ApplicativeをMonadクラスのスーパークラス,FunctorをApplicativeクラスのスーパークラスになるよう階層構造にしたほうがよいのではないか」という意見に変わりました(参考リンク)。 class Functor f where fmap :: (a -> b) -> f a -> f b (<$) :: a -> f b -> f a (<$) = fmap . const class Functor f => Applicative f where pure :: a -> f a (<*>) :: f (a -> b) -> f a -> f b (*>) :: f a -> f b -> f b a *> b = fmap (const id) a <*> b (<*) :: f a -> f b -> f a a <* b = fmap const a <*> b class Applicative m => Monad m where (>>=) :: forall a b. m a -> (a -> m b) -> m b m >>= f = join $ fmap f m join :: m (m a) -> m a join m = m >>= id (>>) :: forall a b. m a -> m b -> m b (>>) = (*>) return :: a -> m a return = pure fail :: String -> m a fail s = error s ただし,このように階層化してしまうと,モナドを定義するユーザーはモナド則だけではなく,合計で13個もの法則を満たさなければならなくなってしまいます。内訳は,モナド則が3個,Functorが満たすべき法則が2個,MonadとFunctorの間で満たすべき法則が1個,Applicativeが満たすべき法則が4個,ApplicativeとFunctorとの間で満たすべき法則が1個,ApplicativeとMonadとの間で満たすべき法則が2個です。 ApplicativeとFunctorの間で満たすべき法則はFunctorクラスのインスタンスの定義,ApplicativeとMonadの間で満たすべき法則はApplicativeクラスのインスタンスの定義にそのまま利用できます。しかし,モナドを定義するユーザーが,FunctorクラスやApplicativeクラスのインスタンスを強制的に書かされることに変わりはありません。こうした強制は望ましくないため,サブクラスからスーパークラスの関数を定義したり,こうした定義を自動的に導出する仕組みを実装したりするための議論が行われています。 2011年には,she(the Strathclyde Haskell Enhancement)というプリプロセサに「スーパークラスに対してデフォルトのインスタンスをプログラマが定義する機能」が実装されました(参考リンク)。このプリプロセサの設計や利用から得られた知見を基にして,同様の機能をGHCに実装するための議論が行われるようになりました。型クラスの定義の中に以下のような形式で「スーパークラスに対するデフォルトのインスタンス定義」を宣言することが提案されています(参考リンク)。 class Functor f => Applicative f where return :: x -> f x (<*>) :: f (s -> t) -> f s -> f t (>>) :: f s -> f t -> f t fs >> ft = return (flip const) <*> fs <*> ft instance Functor f where fmap = (<*>) . return class Applicative f => Monad f where (>>=) :: f a -> (a -> f b) -> f b instance Applicative f where ff <*> fs = ff >>= \ f -> fs >>= \ s -> return (f s) なお,sheには現時点(2011年12月)でのGHCにはない機能がほかにも実装されています(参考リンク)。 |
著者紹介 shelarcy 本連載ではすでにモナドについて説明しているため,モナドの知識を前提にしてApplicativeクラスを説明しました。一方,Applicativeクラスの登場以降に書かれた「Typeclassopedia」(日本語訳)という記事や「Learn You a Haskell for Great Good!: A Beginner's Guide」という書籍では,先にFunctorとApplicativeを説明し,それからMonadを導入するという形式を採っています(参考リンク) Control.Applicativeモジュールでは,今回説明したもののほかに,いくつかの関数やApplicativeクラスのインスタンスである型が提供されています。また,2011年12月17日にリリースされたHaskell Platform 2011.4.0.0に同梱されているtransformersパッケージのモジュールでは,Applicativeの論文「Applicative Programming with Effects」に出てくる型などが提供されています。パーサー・ライブラリでもApplicativeクラスやAlternativeクラスのインスタンスが提供されています。 実は,Applicativeはデータ構造を走査するのにも有用です。今回は特にそうした側面については説明しませんでした。次回はTraversableクラスを取り上げる予定なので,Applicativeのそうした面にも触れたいと思います。 |