「return
と pure
って同じじゃないの?」「Monad クラスの fail
って数学的なモナドにはなくない?」
Haskell初心者が引っかかるポイントの一つが、 Monad クラスと Applicative クラスの関係だろう。他にも、 Haskell 2010 の Monad クラスには数学的なモナドと照らし合わせると奇妙な点がいくつかある。
この記事では、近年行われている Monad クラスへの破壊的変更をまとめ、変化の途上にある Monad クラスの理解の一助としたい。
Haskell 2010 での Monad クラスとその問題点
Haskell 2010 Language Report では、 Monad クラスは次のように定義されている:
class Monad m where
(>>=) :: m a -> (a -> m b) -> m b
(>>) :: m a -> m b -> m b
return :: a -> m a
fail :: String -> m a
m >> k = m >>= \_ -> k
fail s = error s
しかし、 Haskell 2010 の Monad クラスの定義には、次のような問題点がある:
- 数学的にはモナドというのは自己関手の一種であるが、 Haskell の型クラスの階層はそれを反映していない。つまり、 Monad は Functor/Applicative のサブクラスとなっているべきだが、そうなっていない。
- というわけで Applicative を Monad のスーパークラスにしたい (Functor-Applicative-Monad Proposal または AMP)
- Applicative が Monad のスーパークラスとなった暁には、
-
return
は Applicative クラスのpure
と同一なので、return
を Monad クラスから取り除きたい (Monad of noreturn
Proposal または MRP) -
(>>)
も Applicative クラスの(*>)
と同一なので、(>>)
も Monad クラスから取り除きたい (これも Monad of noreturn
Proposal の一部として扱われている)
-
- 数学的なモナドの定義と照らし合わせると、
fail
が余計である。- というわけで
fail
を別のクラスに分離したい (MonadFail
Proposal または MFP) - ちなみに、
fail
は do 構文におけるパターンマッチの脱糖に利用される。
- というわけで
このほかに「join :: Monad m => m (m a) -> m a
を Monad クラスのメソッドにしたい」という動きもあるようだが、互換性を壊す変更でもないし、ここでは扱わない1。
なお、 Monad クラスから削除した return
と (>>)
は普通の関数として return = pure
, (>>) = (*>)
と定義し、 Monad クラスの利用者からは従来通り使えるようにする。
理想の Monad クラス
というわけで、上記の問題点を解決した「理想の」 Monad クラスの定義は次のようになる:
class Functor f where
fmap :: (a -> b) -> f a -> f b
class Functor f => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
(*>) :: f a -> f b -> f b
-- 他にもメソッドがあるけど省略
-- 場合によってはより効率的な定義を与えることができる
a1 *> a2 = (id <$ a1) <*> a2
-- Applicative がスーパークラスとなる
class Applicative m => Monad m where
(>>=) :: m a -> (a -> m b) -> m b
-- (>>): クラスの外へ移動
-- return: クラスの外へ移動
-- fail: MonadFail へ移動
-- このほか、 join がデフォルト実装付きで Monad クラスに追加されるかも
class Monad m => MonadFail m where
fail :: String -> m a
return :: Applicative m => a -> m a
return = pure
(>>) :: Applicative m => m a -> m b -> m b
(>>) = (*>)
しかし、実際にこういう風に定義を変更してしまうと、今まで動いていた Monad クラスのインスタンスが動かなくなってしまう。具体的には、
- 既存の Monad インスタンスに対して Applicative のインスタンスが定義されていない場合にエラーとなる。
- Monad のインスタンスで
return
が定義されているとエラーとなる。 - Monad のインスタンスで
fail
が定義されているとエラーとなる。 - MonadFail のインスタンスでない型について
fail
を使う(ようなdo記法を使う)とエラーとなる。
したがって、一気に移行するのではなく、段階的に移行できるような措置が必要となる。
以下では、 Monad クラスの変更点を次の3点にわけ、どのように「段階的な」移行を実現するか、見ていこう。
- Applicative を Monad のスーパークラスにしたい (Functor-Applicative-Monad Proposal または AMP)
-
return
と(>>)
を Monad クラスから取り除きたい (Monad of noreturn
Proposal または MRP) -
fail
を別のクラスに分離したい (MonadFail
Proposal または MFP)
Functor-Applicative-Monad Proposal (AMP)
概要: Applicative を Monad のスーパークラスとする。(ついでに、 Alternative を MonadPlus のスーパークラスとする)
現状:完了している。具体的には、GHC 7.8 で移行のための警告が導入され、 GHC 7.10 (2015年3月リリース)で実際の変更が行われた。
互換性のための警告
AMP は既存のコードを壊しうる変更なので、実際に移行する前に「将来的に壊れるようなコード」に対して警告が出ると有益である。
そのため、 AMP 前夜の GHC 7.8 では -fwarn-amp
という警告(デフォルトでON)が導入された2。
-fwarn-amp
では、 Monad のインスタンスが Applicative のインスタンスになっていない場合、および MonadPlus のインスタンスが Alternative のインスタンスになっていない場合に、警告が出る。
AMP 後の Monad クラスの定義
Applicative を Monad のスーパークラスにして、 return のデフォルト実装を与える3:
-- GHC 7.10 (base-4.8.0.0) 以降の定義
class Functor f where
fmap :: (a -> b) -> f a -> f b
-- 他にもメソッドがあるけど省略
class Functor f => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
-- 他にもメソッドがあるけど省略
-- Applicative がスーパークラスとなる
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
m >> k = m >>= \_ -> k
return = pure -- デフォルト実装追加
fail s = error s
どういうコードを書けば良い?(Monad を実装する側)
移行前(GHC 7.8 まで)と移行後(GHC 7.10 以降)の両方に通用する書き方は、次のようになる:
-- AMP 移行前と移行後の両対応コード
import Control.Applicative
-- Functor/Applicative のインスタンスも用意する
instance Functor Foo where
fmap = ...
instance Applicative Foo where
pure = ... -- return を使わずに定義する
(<*>) = ...
instance Monad Foo where
(>>=) = ...
return = pure -- pure として定義する
Monad のインスタンス (Foo
) に対して、 Applicative のインスタンスも用意するのがポイントである。Monad のインスタンスに対しては、 return
の実装も一応用意しておく。
「pure
と return
は等価だし、 Applicative の pure
を pure = return
と定義すれば良いのでは」と思われるかもしれないが、 return
は将来的にクラスから削除したいという意図(後述の Monad of no return
Proposal)があるので、 pure = return
ではなくて return = pure
とするのが適切である。
AMP に完全に移行した後は、 return
関数のデフォルト実装があるため、 return = pure
は不要になる。また、 Applicative クラスの定義が Prelude に入るので、インスタンスを書くためだけに明示的に import Control.Applicative
をする必要はなくなる。
特に、2018年現在、AMP以前を考慮するコードを新規に書くことはないと思われるので、 return = pure
は必要ない。
-- AMP 完全移行後のコード(2018年に新規に書くならこれ)
-- Applicative(pure,(<*>),(*>),(<*)) 以外を使わないのであれば import Control.Applicative は必要ない
instance Functor Foo where
fmap = ...
instance Applicative Foo where
pure = ... -- return を使わずに定義する
(<*>) = ...
instance Monad Foo where
(>>=) = ...
-- return は必要ない
Monad of no return
Proposal (MRP)
概要:Monad クラスから return
関数を排除し、クラスの外で return = pure
と定義する。
現状:進行中。GHC 8.0〜8.8 の時点では、「将来的に壊れるコード」に対しての警告が実装されている (Phase 1)。
MRP Phase 1
Phase 1 では「将来的に壊れるコード」に対しての警告が実装される。つまり、 Monad のインスタンスが明示的に return
, (>>)
を実装しており、しかも望ましい定義 return = pure
, (>>) = (*>)
とは異なる場合に、警告を出す。
GHC 8.0 で実装された -Wnoncanonical-monad-instances
(デフォルトでOFF, -Wall
には含まれない)がこの警告オプションである。
MRP Phase 2 以降
Phase 2 以降(GHC 8.10 またはそれ以降)では、次の順番で、時間をかけて移行を促していく。
-
return
と(>>)
を Monad クラスから削除する。これでreturn
と(>>)
を明示的に定義した場合は基本的にエラーとなるが、例外として、return = pure
,(>>) = (*>)
と定義されていた場合はエラーにしない。 -
return = pure
,(>>) = (*>)
と定義されている場合であっても警告を出す。 -
return = pure
,(>>) = (*>)
と定義されている場合であってもエラーとする。(完了)
どういうコードを書けば良い?(Monad を実装する側)
AMPのところにも書いたように、
-- AMP を前提にした、 MRP 移行前と移行後の両対応コード
-- Applicative(pure,(<*>),(*>),(<*)) 以外を使わないのであれば import Control.Applicative は必要ない
instance Functor Foo where
fmap = ...
instance Applicative Foo where
pure = ... -- return を使わずに定義する
(<*>) = ...
instance Monad Foo where
(>>=) = ...
-- return は書かない
となる。
GHC オプションとして -Wnoncanonical-monad-instances
も有効にしておくと良いだろう。
例
修正前:
{-# LANGUAGE DeriveFunctor #-}
newtype Foo a = Ok a
deriving (Functor,Show)
instance Applicative Foo where
pure x = Ok x
Ok f <*> Ok x = Ok (f x)
instance Monad Foo where
Ok m >>= f = f m
return x = Ok x -- 警告
main :: IO ()
main = return ()
$ stack ghc --compiler ghc-8.8.1 -- -Wnoncanonical-monad-instances MRPTest.hs
[1 of 1] Compiling Main ( MRPTest.hs, MRPTest.o )
MRPTest.hs:12:3: warning: [-Wnoncanonical-monad-instances]
Noncanonical ‘return’ definition detected
in the instance declaration for ‘Monad Foo’.
Either remove definition for ‘return’ or define as ‘return = pure’
|
12 | return x = Ok x
| ^^^^^^^^^^^^^^^
return x = Ok x
の行を return = pure
に変えるか、あるいは完全に取り去ってしまうと、警告は出なくなる。
MonadFail Proposal (MFP)
概要:fail
を別のクラス MonadFail
に分離する。
現状:GHC 8.8 でほぼ完了。
- GHC 8.0〜8.4 の時点では、
MonadFail
クラスが導入され、do記法でMonadFail
クラスを使うためのGHC拡張 (MonadFailDesugaring) が実装されている。将来壊れるコードに対しての警告も実装されている。 - GHC 8.6 の時点では、do記法でデフォルトで
MonadFail
が使われるようになっている(MonadFailDesugaringが有効)。 - GHC 8.8 (base-4.13) では、
Monad
クラスの定義からfail
が取り除かれ、MonadFail(fail)
が Prelude からexportされるようになった。 - 将来 Control.Monad.Fail モジュールがdeprecateになるかもしれない(ただし執筆時点で prime.haskell.org の証明書の期限が切れているので確認できない)。
互換性のための警告
GHC 8.0 で -Wnoncanonical-monadfail-instances
と -Wmissing-monadfail-instances
の2つの警告が実装された。
-Wnoncanonical-monadfail-instances
は Monad を実装する側への警告で、 Monad クラスの fail
メソッドが明示的に実装されており、しかも望ましい定義 fail = Control.Monad.Fail.fail
とは異なる場合に、警告を出す。この警告は GHC 8.0〜8.6 ではデフォルトでOFFであり、 -Wall
にも含まれない。
(GHC 8.6 の時点で -Wnoncanonical-monadfail-instances
を -Wcompat
に含めるべきだったと筆者は思うのだが……)
-Wmissing-monadfail-instances
は Monad の利用者側への警告で、do記法で fail
が必要とされるのに MonadFail
のインスタンスが見つからない場合に、警告を出す。この警告は GHC 8.0〜8.4 ではデフォルトでOFFであるが、 -Wcompat
によって有効化される。
GHC 8.8 の時点では -Wnoncanonical-monadfail-instances
を指定すると警告が出るようになった。
MFP Phase 1
GHC 8.0 で実施。
Control.Monad.Fail モジュールを新規に追加し、 MonadFail
クラスをそこで提供する。
-- 変更なし
module Prelude where
class Functor f -- 略
class Functor f => Applicative f -- 略
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
m >> k = m >>= \_ -> k
return = pure
fail s = error s
-- 追加
module Control.Monad.Fail where
class Monad m => MonadFail m where
fail :: String -> m a
この時点ではdo記法の脱糖には相変わらず Monad
クラスの fail
が使用される。ただし、GHC拡張の MonadFailDesugaring が有効な場合は、do記法の脱糖に MonadFail
クラスの fail
が使われるようになる。
MFP Phase 2 以降
GHC 8.6 で、
- GHC拡張 MonadFailDesugaring がデフォルトで有効になる。つまり、do記法で
MonadFail
クラスのfail
が使用されるようになる。
GHC 8.8 で、
-
Monad
クラスからfail
が削除され、Control.Monad.Fail.fail
がPrelude.fail
となる。(MFP 完了)
なお、MFPが完了すると Control.Monad.Fail というモジュールの存在価値はなくなる。
どういうコードを書けば良い?(Monad を実装する側)
モナド実装者の側として、まともな fail
を提供できない場合(fail = error
とするしかないような場合)は、 MFP の前も後も「fail
を明示的に実装しない」。
fail
を実装できるモナドの場合は、次のように書く:
import qualified Control.Monad.Fail as Fail
instance Functor Foo -- 略
instance Applicative Foo -- 略
instance Monad Foo where
(>>=) = ...
-- GHC 8.8 以降をサポートする場合: fail を定義しない。または CPP を使って GHC 8.8 以降の場合は fail を定義しないようにする。
-- GHC 8.6 までの場合:
fail = Fail.fail
instance Fail.MonadFail Foo where
fail = ...
ただし、 GHC 8.0 よりも前のバージョンをサポートする必要がある場合は、別途 fail パッケージを利用してControl.Monad.Fail モジュールを使えるようにしておく。
ポイントは次の点である:
- GHC 8.8 以降(base 4.13 以降)では
Monad
クラスでfail
を定義しているとまずい。fail
の定義をやめる(GHC 8.6 以前でfail = error
となるのを許容する)か、 CPP を使って切り分ける。 Monad
とMonadFail
の両方に対してfail
を実装する。ただし、Monad
の方のfail
からMonadFail
のfail
へ処理を丸投げする。-
Monad
のfail
とMonadFail
のfail
は名前が同じ別物なので、区別するために qualified import を使う。
なお、モナドトランスフォーマーの類(State
モナドなど、下位のモナドの上に構築されるモナド)を書く場合は、
-
Monad
インスタンスを書く際に下位のモナドに対するMonadFail
を使えないのでfail = Fail.fail
とは書けない
ので、緩やかな移行パスは存在しない(はず)。CPP
を使おう。
どういうコードを書けば良い?(do記法の利用者)
do記法において「失敗しうるパターンマッチ」を使っているかどうか、検討する。
- do記法で「失敗しうるパターンマッチ」を使っていない場合は、問題ない。
- 使っているモナドが MonadFail のインスタンスである場合は、問題ない。
- それ以外、つまり、使っているモナドが MonadFail のインスタンスでないのに、do記法で「失敗しうるパターンマッチ」を使っている場合。
- モナドに MonadFail のインスタンスを実装できそうな場合は、実装する。(あるいは、ジェネリックな関数の場合は型制約を MonadFail に変える)
- そうでない場合は、 irrefutable pattern を使うか、明示的に
error
を呼ぶ。(暗黙のうちにerror
が呼ばれないようにするのがポイント)
なお、do記法における「失敗しうるパターンマッチ」とは、 <-
を使った do {...; <pattern> <- <expression> ;...}
の形のパターンマッチのうち、失敗しうるものである。let や case によるパターンマッチは関係ない。
よく使われるモナドの例を挙げると、リスト []
, Maybe
, IO
は MonadFail
のインスタンスで、 Either e
や Identity
は MonadFail
のインスタンスではない4。
よくわからなかったら、GHCの警告 -Wmissing-monadfail-instances
(または -Wcompat
でも良い)を有効にするか、あるいはGHC拡張 MonadFailDesugaring を有効にして、無事にコンパイルできるか確かめると良いだろう。
なお、筆者の私見だが、ユーザーが明示的に(do記法の脱糖以外で) fail
を呼ぶことはなるべく避けたほうが良いように思う。そもそも fail
は失敗に関する情報を String
しか受け取れない。まともなエラー処理がしたかったら Control.Monad.Except の throwError
や IO の throwIO
など、そのモナドに適した例外送出方法を使うべきである。リストや Maybe の場合は mzero
(もしくは Alternative
クラスの empty
)を使うのが適切である。
例
修正前:
{-# LANGUAGE DeriveFunctor #-}
data Foo a = Ok a
| Failed String
deriving (Functor,Show)
instance Applicative Foo where
pure x = Ok x
Ok f <*> Ok x = Ok (f x)
Failed s <*> _ = Failed s
_ <*> Failed s = Failed s
instance Monad Foo where
Ok m >>= f = f m
Failed s >>= _ = Failed s
fail s = Failed s -- 警告:fail = Control.Monad.Fail.fail とするべき
main :: IO ()
main = print $ do
Just x <- return (Just 123) :: Foo (Maybe Int) -- 警告/エラー:Foo に MonadFail のインスタンスがない
return x
GHC 8.4.1 で、警告を有効にした場合:
$ stack ghc --compiler ghc-8.4.1 -- -Wmissing-monadfail-instances -Wnoncanonical-monadfail-instances MFPTest.hs
[1 of 1] Compiling Main ( MFPTest.hs, MFPTest.o )
MFPTest.hs:16:3: warning: [-Wnoncanonical-monadfail-instances]
Noncanonical ‘fail’ definition detected
in the instance declaration for ‘Monad Foo’.
Either remove definition for ‘fail’ or define as ‘fail = Control.Monad.Fail.fail’
|
16 | fail s = Failed s
| ^^^^^^^^^^^^^^^^^
MFPTest.hs:20:3: warning: [-Wmissing-monadfail-instances]
• No instance for (Control.Monad.Fail.MonadFail Foo)
arising from the failable pattern ‘Just x’
(this will become an error in a future GHC release)
• In a stmt of a 'do' block:
Just x <- return (Just 123) :: Foo (Maybe Int)
In the second argument of ‘($)’, namely
‘do Just x <- return (Just 123) :: Foo (Maybe Int)
return x’
In the expression:
print
$ do Just x <- return (Just 123) :: Foo (Maybe Int)
return x
|
20 | Just x <- return (Just 123) :: Foo (Maybe Int)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
GHC 8.4.1 で、 MonadFailDesugaring 拡張を有効にした場合:
$ stack ghc --compiler ghc-8.4.1 -- -XMonadFailDesugaring MFPTest.hs
[1 of 1] Compiling Main ( MFPTest.hs, MFPTest.o ) [flags changed]
MFPTest.hs:20:3: error:
• No instance for (Control.Monad.Fail.MonadFail Foo)
arising from a do statement
with the failable pattern ‘Just x’
• In a stmt of a 'do' block:
Just x <- return (Just 123) :: Foo (Maybe Int)
In the second argument of ‘($)’, namely
‘do Just x <- return (Just 123) :: Foo (Maybe Int)
return x’
In the expression:
print
$ do Just x <- return (Just 123) :: Foo (Maybe Int)
return x
|
20 | Just x <- return (Just 123) :: Foo (Maybe Int)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
修正後:
{-# LANGUAGE DeriveFunctor #-}
import qualified Control.Monad.Fail as Fail
data Foo a = Ok a
| Failed String
deriving (Functor,Show)
instance Applicative Foo -- 略
instance Monad Foo where
Ok m >>= f = f m
Failed s >>= _ = Failed s
fail = Fail.fail -- 変更
-- MonadFail 追加
instance Fail.MonadFail Foo where
fail s = Failed s
main :: IO ()
main = print $ do
Just x <- return (Just 123) :: Foo (Maybe Int)
return x
その他
Haskell の Prelude の関数を一般化したり、スーパークラスを追加したりする変更については、この記事に書いた Monad 関連のもの以外にも
- Foldable-Traversable Proposal (FTP): 完了
- Semigroup-Monoid Proposal (SMP): 進行中(GHC 8.4 ですでに Semigroup は Monoid のスーパークラスになっている。mappend はまだ Monoid クラスから取り除かれていない)
などがある。
-
join
を入れる動機としては、 1. 数学的なモナドの定義には>>=
よりもむしろjoin
が使われる 2. モナドによってはjoin
に効率的な定義を与えることができるかもしれない の2点が考えられる。しかし、join
を Monad クラスに入れるにあたっては、 GeneralizedNewtypeDeriving との食い合わせが悪いという問題があるらしい。 ↩ -
GHC 7.10 以降は
-fwarn-amp
は用済みとなるので、-fwarn-amp
オプション自体の使用に対して "it has no effect" 的な警告が出る。 ↩ -
関連して
MonadPlus
クラスもAlternative
をスーパークラスとするように変更されているが、この記事では省略する。 ↩ -
これに対し、 GHC 8.8 (base-4.13) の時点では、STモナドは MonadFail のインスタンスになっているようである。IOモナドと違ってSTモナドは定義が
fail = error
なのに! ↩