圏論的なモナドとの関係については、こちらを参照してください。このページでは、Haskellでのモナドを実用面から見てみます。

IO処理とモナド

Haskellでの計算は、原則として「どの順番で計算しても結果が同じ」です。特に、 f + g という計算をした場合に、 fg のどちらが先に評価(計算)されるかは決まっていません。そもそも、 (+) の定義によっては fg が評価されない可能性もあります。

この性質は、計算によって順番に処理を行うのには向いていません。しかし、「処理」をデータとして扱えば、計算順序に関係なく処理を扱うことができます。

「処理」をデータとして考える場合には、次のような操作が考えられます:

〈何もしない処理〉 :: 〈処理〉
〈処理の続行〉     :: 〈処理〉 -> 〈処理〉 -> 〈処理〉

この〈処理〉は、単位元と結合的な積を持つので、モノイドになります。 (実際のHaskellでは、〈処理〉は IO ()、〈何もしない処理〉は return ()、〈処理の続行〉は (>>) が相当します)

内容が最初から決まっている処理を記述するだけならモノイドで良いのですが、現実のプログラムでは「処理の結果を使って次の処理を決定する」ことが必要です。

そこで、〈処理〉に、処理結果の型を表すパラメーター a を持たせ、次の処理に反映できるようにします。なお、処理結果と呼べるものが特にない(情報量ゼロ)場合は、 unit 型 () を使います。

「前の処理の結果を使って次の処理を決める」操作は、 (>>=) という関数(演算子)で表され、次のような型を持ちます:

(>>=) :: 〈処理 a -> (a -> 〈処理 b) -> 〈処理 b

また、処理結果を指定して「何もしない処理」を作るための関数もあります:

return :: a -> 〈処理 a

(return という名前には特に深い意味はありません。「値を返す」からこの名前がついたのだと思われますが、他のプログラミング言語の return 文と違って、その場で関数の実行を中断する機能は持ちません。)

この〈処理〉のように、 (>>=)return を持つものを、モナドと呼びます。Haskellにおいては Monad クラスがモナドを表します。Monad クラスは、大雑把には次のように定義されています:

class Monad m where
  return :: a -> m a
  (>>=)  :: m a -> (a -> m b) -> m b
  (>>)   :: m a -> m b -> m b
  m >> k = m >>= \_ -> k

入出力処理(端末に文字を表示したり、ファイルの読み書きをしたり)はモナドの一例ですが、他にもモナドの例があります。

do 記法

モナドを使う計算は >>=>> のような演算子を使っても書けますが、Haskellにはそういう計算をもっと楽に書くための構文が用意されています。

do { 〈文1〉; 〈文2〉; … ;〈文n〉 }

それぞれの文として、次のいずれかを書けます:

  • 〈式〉〈式〉 の型は m a で、結果は捨てられる)
  • p <- 〈式〉〈式〉 の型は m a で、処理結果を p に代入する。p の型は a
  • let 〈宣言〉 (let 式と同様だが、 in 以下は書かない)

さらに、プログラムを適切にインデントする場合は波かっこ { } とセミコロン ; は省略できます。

例:以下の4つのプログラムは等価

-- 基本形
main = putStrLn "Type your name: " >> getLine >>= \s -> putStrLn ("Do-mo, " ++ s ++ "-san.")
-- do記法を使った場合
main = do { putStrLn "Type your name: "
          ; s <- getLine  -- 結果を s に代入
          ; putStrLn ("Do-mo, " ++ s ++ "-san.")
          }
-- 空文とlet文を使ってみた
main = do { putStrLn "Type your name: "
          ;   -- 空文
          ; s <- getLine
          ; let aisatsu = "Do-mo, " ++ s ++ "-san."  -- let 文
          ; putStrLn aisatsu
          }
-- 波かっことセミコロンを省略した
main = do putStrLn "Type your name: "
          s <- getLine
          let aisatsu = "Do-mo, " ++ s ++ "-san."
          putStrLn aisatsu

モナドの例

IO

すでに何回か書いたように、入出力はモナドになっています。

Maybe

Maybe 型は、次のように Monad のインスタンスになります:

instance Monad Maybe where
  return x      = Just x
  Just x  >>= k = k x
  Nothing >>= _ = Nothing

Maybe モナドは「失敗する可能性のある計算」と見なせます。すなわち、 Just x は「計算が成功して、値が x になった」状態で、 Nothing が「計算が失敗した」状態に対応します。

リスト [a]

リスト型は、次のように Monad のインスタンスになります:

instance Monad [] where
  return x = [x]
  xs >>= k = concatMap k xs

ただし、 concatMap 関数は concat 関数と map 関数の合成で、 concatMap k xs = concat (map k xs) です。他のプログラミング言語では flatMap と呼ばれる場合もあるようです。

このリストモナドは、「非決定的な計算」と見なせます。つまり、do記法における文 x <- [1,2,3] は、「リスト [1,2,3] からいずれかの要素を、非決定的に取り出す」文と見なせます。

例えば「リスト [1,2,3] からいずれかの要素を非決定的に取り出して、それを2倍したもの」としてあり得る値のリストを返す関数は次のように書けます:

f :: [Int]
f = do x <- [1,2,3]
       return (x * 2)
-- f = [1,2,3] >>= \x -> return (x * 2)

課題:上に書いたリストモナドの定義(return(>>=))を見ながら、この f がどのように計算されるか考えよ。

もっと複雑な例として、覆面算を総当たりで解くプログラムを見てみましょう。SEND+MORE=MONEY のそれぞれの文字に対して 0 から 9 のいずれかの数字を対応させ、覆面算が成立するかチェックします。ただし、同じ文字には同じ数字が、違う文字には違う数字が対応するようにします。また、最上位の桁は 0 ではないと言う条件もつけます。

import Data.List
import Control.Monad
sendmoremoney :: [(Int,Int,Int)]
sendmoremoney = do
  s <- [1..9]
  e <- [0..9] \\ [s]
  n <- [0..9] \\ [s,e]
  d <- [0..9] \\ [s,e,n]
  m <- [1..9] \\ [s,e,n,d]
  o <- [0..9] \\ [s,e,n,d,m]
  r <- [0..9] \\ [s,e,n,d,m,o]
  y <- [0..9] \\ [s,e,n,d,m,o,r]
  let send  =             s * 1000 + e * 100 + n * 10 + d
      more  =             m * 1000 + o * 100 + r * 10 + e
      money = m * 10000 + o * 1000 + n * 100 + e * 10 + y
  guard (send + more == money)
  return (send,more,money)

ただし、Data.List モジュールで定義されている \\ 演算子は、2つのリストの差分を取ります。つまり、左辺のリストから、右辺のリストに含まれない要素だけを取り出したリストを作ります。

また、 Control.Monad モジュールで定義されている guard 関数は、条件が成り立たない場合にリストモナドの計算を中止します。(実際には MonadPlus という型クラスで定義されており、リストモナド以外でも使えます)

課題:sendmoremoney 関数を実際に計算して、覆面算の答えを調べよ。

課題:「最上位の桁が0ではない」という条件を外した場合は、何通りの答えがあるか。

Reader

(要執筆)

State

(要執筆)