Haskell: Monadクラスのこれまでとこれから

「`return` と `pure` って同じじゃないの？」「Monad クラスの `fail` って数学的なモナドにはなくない？」

Haskell初心者が引っかかるポイントの一つが、 Monad クラスと Applicative クラスの関係だろう。他にも、 Haskell 2010 の Monad クラスには数学的なモナドと照らし合わせると奇妙な点がいくつかある。

この記事では、近年行われている Monad クラスへの破壊的変更をまとめ、変化の途上にある Monad クラスの理解の一助としたい。

# Haskell 2010 での Monad クラスとその問題点

[Haskell 2010 Language Report](https://www.haskell.org/onlinereport/haskell2010/haskellch6.html#x13-1330006.3.6) では、 Monad クラスは次のように定義されている：

```haskell:Prelude
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 no `return` Proposal または MRP)
        - `(>>)` も Applicative クラスの `(*>)` と同一なので、 **`(>>)` も Monad クラスから取り除きたい** （これも Monad of no `return` Proposal の一部として扱われている）
- 数学的なモナドの定義と照らし合わせると、 `fail` が余計である。
    - というわけで **`fail` を別のクラスに分離したい** (`MonadFail` Proposal または MFP)
    - ちなみに、 `fail` は do 構文におけるパターンマッチの脱糖に利用される。

このほかに「`join :: Monad m => m (m a) -> m a` を Monad クラスのメソッドにしたい」という動きもあるようだが、互換性を壊す変更でもないし、ここでは扱わない[^0]。

[^0]: `join` を入れる動機としては、 1. 数学的なモナドの定義には `>>=` よりもむしろ `join` が使われる 2. モナドによっては `join` に効率的な定義を与えることができるかもしれない の2点が考えられる。しかし、 `join` を Monad クラスに入れるにあたっては、 [GeneralizedNewtypeDeriving との食い合わせが悪い](https://ghc.haskell.org/trac/ghc/wiki/Roles2#join)という問題があるらしい。

なお、 Monad クラスから削除した `return` と `(>>)` は普通の関数として `return = pure`, `(>>) = (*>)` と定義し、 Monad クラスの利用者からは従来通り使えるようにする。

## 理想の Monad クラス

というわけで、上記の問題点を解決した「理想の」 Monad クラスの定義は次のようになる：

```haskell:Prelude
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 no `return` Proposal または MRP)
- `fail` を別のクラスに分離したい (`MonadFail` Proposal または MFP)

# Functor-Applicative-Monad Proposal (AMP)

概要： Applicative を Monad のスーパークラスとする。（ついでに、 Alternative を MonadPlus のスーパークラスとする）

現状：完了している。具体的には、GHC 7.8 で移行のための警告が導入され、 GHC 7.10 （2015年3月リリース）で実際の変更が行われた。

https://wiki.haskell.org/Functor-Applicative-Monad_Proposal

## 互換性のための警告

AMP は既存のコードを壊しうる変更なので、実際に移行する前に「将来的に壊れるようなコード」に対して警告が出ると有益である。

そのため、 AMP 前夜の GHC 7.8 では `-fwarn-amp` という警告（デフォルトでON）が導入された[^2]。

`-fwarn-amp` では、 Monad のインスタンスが Applicative のインスタンスになっていない場合、および MonadPlus のインスタンスが Alternative のインスタンスになっていない場合に、警告が出る。

- [GHC 7.8 の警告オプション一覧](https://downloads.haskell.org/~ghc/7.8-latest/docs/html/users_guide/options-sanity.html)

[^2]: GHC 7.10 以降は `-fwarn-amp` は用済みとなるので、 `-fwarn-amp` オプション自体の使用に対して "it has no effect" 的な警告が出る。

## AMP 後の Monad クラスの定義

Applicative を Monad のスーパークラスにして、 return のデフォルト実装を与える[^1]：

[^1]: 関連して `MonadPlus` クラスも `Alternative` をスーパークラスとするように変更されているが、この記事では省略する。

```haskell:Prelude
-- 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 以降）の両方に通用する書き方は、次のようになる：

```haskell
-- 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` は必要ない。

```haskell
-- 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)。

https://ghc.haskell.org/trac/ghc/wiki/Proposal/MonadOfNoReturn

## 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 またはそれ以降）では、次の順番で、時間をかけて移行を促していく。

2. `return` と `(>>)` を Monad クラスから削除する。これで `return` と `(>>)` を明示的に定義した場合は基本的にエラーとなるが、例外として、 `return = pure`, `(>>) = (*>)` と定義されていた場合はエラーにしない。
3. `return = pure`, `(>>) = (*>)` と定義されている場合であっても警告を出す。
4. `return = pure`, `(>>) = (*>)` と定義されている場合であってもエラーとする。（完了）

## どういうコードを書けば良い？（Monad を実装する側）

AMPのところにも書いたように、

```haskell
-- 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` も有効にしておくと良いだろう。

## 例

修正前：

```haskell:MRPTest.hs
{-# 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 ()
```

```sh
$ 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 の証明書の期限が切れているので確認できない）。

https://prime.haskell.org/wiki/Libraries/Proposals/MonadFail

## 互換性のための警告

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` クラスをそこで提供する。

```haskell:Prelude
-- 変更なし
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
```

```haskell:Control.Monad.Fail
-- 追加
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` を実装できるモナドの場合は、次のように書く：

```haskell
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 パッケージ](https://hackage.haskell.org/package/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` のインスタンスではない[^monadfail-st]。

[^monadfail-st]: これに対し、 GHC 8.8 (base-4.13) の時点では、STモナドは MonadFail のインスタンスになっているようである。IOモナドと違ってSTモナドは定義が `fail = error` なのに！

よくわからなかったら、GHCの警告 `-Wmissing-monadfail-instances` （または `-Wcompat` でも良い）を有効にするか、あるいはGHC拡張 MonadFailDesugaring を有効にして、無事にコンパイルできるか確かめると良いだろう。

なお、筆者の私見だが、ユーザーが明示的に（do記法の脱糖以外で） `fail` を呼ぶことはなるべく避けたほうが良いように思う。そもそも `fail` は失敗に関する情報を `String` しか受け取れない。まともなエラー処理がしたかったら Control.Monad.Except の `throwError` や IO の `throwIO` など、そのモナドに適した例外送出方法を使うべきである。リストや Maybe の場合は `mzero` （もしくは `Alternative` クラスの `empty`）を使うのが適切である。

## 例

修正前：

```haskell:MFPTest.hs
{-# 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 で、警告を有効にした場合：

```sh
$ 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 拡張を有効にした場合：

```sh
$ 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)
   |   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
```

修正後：

```haskell:MFPTest.hs
{-# 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)](https://wiki.haskell.org/Foldable_Traversable_In_Prelude): 完了
- [Semigroup-Monoid Proposal (SMP)](https://prime.haskell.org/wiki/Libraries/Proposals/SemigroupMonoid): 進行中（GHC 8.4 ですでに Semigroup は Monoid のスーパークラスになっている。mappend はまだ Monoid クラスから取り除かれていない）

などがある。