Maybeモナド

 前々回の第3回で,:infoコマンドを使ってMonadクラスについて調べたときのことを覚えてるでしょうか。そこに表示されていたように,MaybeもMonadクラスのインスタンスを持ちます。標準Haskellでは,MaybeのMonadクラスのインスタンスは次のように定義されています(参考リンク)。

instance  Monad Maybe  where
    (Just x) >>= k   =  k x
    Nothing  >>= k   =  Nothing
    return           =  Just
    fail s           =  Nothing

 joinに相当する関数を介さず,直接>>=の定義を書き下しています。Just (Just x)はJust xにほかならず,NothingはMaybe (Maybe ...)のようなMaybeが何段階か入れ子になっている型としても扱えるので,当然といえば当然でしょう。

Prelude> :t Nothing::Maybe (Maybe a)
Nothing::Maybe (Maybe a) :: Maybe (Maybe a)

 パターン照合により,このような入れ子の型が発生するのを防ぐこともできます。例えばパターン照合を明示的に使う場合には,以下のように「case式(case expression)」を使って処理を入れ子にしていくことで,Maybeの入れ子を発生させずに処理を組み合わせていくことができます。

case ... of
   Nothing -> Nothing
   Just x  -> case ... of
                Nothing -> Nothing
                Just y  -> ...

 また,>>=などを使って関数越しにパターン照合を組み合わせた場合にも,同様にMaybeの入れ子を発生させずに処理を組み合わせていくことができます。

Prelude Data.List> elemIndex 3 [0..5] >>= \x -> elemIndex x [3,1,4,1]
Just 0
Prelude Data.List> elemIndex 3 [0..5] >>= \x -> elemIndex x [5..10]
Nothing
Prelude Data.List> elemIndex 3 [0..5] >>= \x -> elemIndex x [5..10]
  >>= \y -> elemIndex y [3,1,4,1]
(↑実際には1行)
Nothing

 Maybeの>>=の定義を展開してみればすぐにわかりますが,case式を入れ子にしていくことと>>=を組み合わせていくことは等価です(これまで何度か見てきたパターン照合の例はすべて糖衣構文であり,最終的には等価なcase式に変換されます。参考リンク)。

Prelude Data.List> case elemIndex 3 [0..5] of Nothing -> Nothing;
  Just x -> elemIndex x [3,1,4,1]
(↑実際には1行)
Just 0

Prelude Data.List> case elemIndex 3 [0..5] of Nothing -> Nothing;
  Just x -> case elemIndex x [5..10] of Nothing -> Nothing;
  Just y -> elemIndex y [3,1,4,1]
(↑実際には1行)
Nothing

 したがって,case式を入れ子にした上のようなコードがプログラム中に表れる場合には,モナドを使って書き換えることを考えるとよいでしょう。上のように入れ子が浅ければ直接書き下しても特に問題はないかもしれませんが,入れ子が深くなるにつれてコードを読み書きするのが大変になっていきます。モナドを使うことで,そのような煩雑さを解消できます。

 さて,何度か見てきたように,MaybeはListとは違って複数の値を保持しません。これがListモナドとMaybeモナドの大きな違いになります。

 Listは答えになりうる可能性のある値すべてを保持できるため,Listモナドでは非決定性計算を表現することができました。一方,Maybeには「答えがあるか,それとも答えはないか」の二つの状態しか存在しません。Maybeモナドの役割はもっぱら「答えがあるかどうかわからない」計算を組み合わせて,より大きな「答えがあるかどうかわかない」計算を組み立てることにあります。あるいは前回のListモナドに対する説明から言葉を借りるなら,「成功するか失敗するかわからない」計算を組み合わせてより大きな「失敗するかもしれない」計算を組み立てることにある,と言うこともできるでしょう。

 Maybe型に対してもMonadPlusクラスのインスタンスが用意されているため,無理をすればListモナドと同じく非決定性計算を表すのに使うこともできなくはないかもしれません。しかし,そうした用途での使いやすさはListモナドに比べて数段劣ります。List型とMaybe型が意図をもって明確に使い分けられているように,ListモナドとMaybeモナドも目的に応じて明確に使い分けるべきです。

 こうした違いを頭に入れたうえで,MonadPlusクラスのインスタンスの定義を見てください。現在の標準Haskellでは,Maybe型に対するMonadPlusクラスのインスタンスは次のように定義されています(参考リンク)。

instance  MonadPlus Maybe  where
    mzero                 = Nothing

    Nothing `mplus` ys    =  ys
    xs      `mplus` ys    =  xs

 mplusは「第1引数がゼロ(=Nothing)の時だけ,第2引数の計算を行う」よう定義されていることがわかると思います。Maybeモナドでは,mplusは「ある計算が失敗した場合,次の計算を試してみる」関数として利用できます。

Prelude Control.Monad Data.List> elemIndex 3 [0..5]
  >>= \x -> elemIndex x [5..10] `mplus` elemIndex x [5,4..0]
(↑実際には1行)
Just 2

Prelude Control.Monad Data.List> elemIndex 3 [0..5]
  >>= \x -> (guard (x<1) >> return x) `mplus` elemIndex x [3,1,4,1]
(↑実際には1行)
Just 0

Prelude Control.Monad Data.List> elemIndex 3 [0..5]
  >>= \x -> elemIndex x [5,4..0] `mplus` elemIndex x [3,1,4,1]
(↑実際には1行)
Just 2

今回のまとめ

 List型とMaybe型の違い,ListモナドとMaybeモナドの違いを理解できたでしょうか?

 ある型と別の型が似たような特徴を持ち,似たような使われ方をする場合,どこが違うのかを知ることが,その型を理解するうえで重要になってきます。何が違いどう使い分けられるのかを飲み込めば,コードはより明快になり,Haskellの型システムが「バグになりうる可能性のある部分」をより早く検出してくれることでしょう。

 型はあなたの友人です。型に親しめば親しむほど,コードはより良くなっていくはずです。

安全なリスト操作

 空リストにheadやtailを適用することによって生じる実行時エラーは,プログラマにとってあまりうれしいものではありません。未定義の動作を引き起こす心配が要らないだけマシですが,関数が積み重なっていくにつれ,どの部分が適切に処理できていないのかを発見するのが難しくなっていきます。

 このような問題の解決手段として「値をMaybe型として返す*May」「maybeのように答えがない場合にはデフォルトに設定した値を返す*Def」「与えられた文字列を使ってどこでエラーが起きたのかわかりやすくする*Note」「安全な定義を行うことが可能なものについてはデフォルトの型を返す*Safe」という四種類の代替関数を定義したSafeというライブラリが公開されています。

 SafeではPreludeの関数しかカバーしていないようですが,それでもこれらを使うことでより安全なプログラミングを行うことが可能になるでしょう。

 SafeはCabal(Haskell用のパッケージ・ツール)のパッケージになっているので,第2回のコラムで説明したData.ByteStringのインストール方法と同じやり方でインストールできます。現在のところソース・ファイルが一つだけなので,わざわざインストールする必要はないかもしれませんが。

 別のやり方としては,GHCの拡張機能である「GADT(Generalised Algebraic Data Type,一般化代数的データ型)」などを使ってListを再定義する方法があります(参考リンク)。ただ,既存のListがすでにあらゆるところで使われている以上,こちらのやり方は現実的ではありません。


著者紹介 shelarcy

 KOF 2006のHaskellセッションで,GHC 6.6の新機能のGHC as a Library(GHC API)を紹介する発表を行いました(参考リンク)。これは文字通り,GHCの機能をそのままライブラリとして利用するためのものです。

 GHC as a Libraryの登場により,GHCが行っているのと同じ仕事をする場合には,その仕事をライブラリに任せることが可能になりました。まだGHC as a LibraryやGHC本体はcabal化(cabalizeまたはcabalise,Cabalのパッケージになっていること)されていないため,残念ながらGHC自身を手軽にいろいろと改造できるようになったわけではありません。とはいえ,以前に比べれば,GHCの機能を利用する敷居はずっと下がったといえるでしょう。GHC6.8までには,GHC as a Libraryをcabal化する予定もあるようです(参考リンク)。そろそろ国際化など不満のあるところを改造してパッチを送ることを検討してもよいかもしません。