モナド

このページでは,Haskell を理解するうえでカギとなる概念である,モナドについて説明します.

まず最初に,モナドを自然に理解できるように,モナドと近い概念である "ファンクタ" を紹介します. その次に,モナドを紹介します. それから,モナドの例として Maybe モナドを紹介し,モナドのための構文糖である do 記法を説明します.

1. ファンクタ

モナドを自然に理解できるように,はじめに ファンクタ(functor)とよばれる型クラスを紹介します.

モナドと同様,ファンクタも圏論に由来する概念のひとつです.しばしば "関手" とも訳されます.

リストの要素を操作する関数 map を一般化した fmap という関数があります. ファンクタはこの fmap をメンバとする型クラスであり,次のように定義されています.

class Functor t where
  fmap :: (a -> b) -> t a -> t b

Functor クラスは,値を入れるコンテナみたいな構造をもつ型を表します. 別の言い方をすれば,なんらかの型をラッピングした(包み込んだ)型を表します. インスタンスには,Maybe[] (リスト),IO などがあります.

関数 fmap は,ラッピングされた値に対する操作を可能にする関数です. いくつか使用例を見てみましょう.

f x = 2 * x

main = do print $ fmap f (Just 5)   -- 出力: Just 10
          print $ fmap f Nothing    -- 出力: Nothing
          print $ fmap f [1, 2, 3]  -- 出力: [2, 4, 6]
          print $ fmap f []         -- 出力: []

Maybe および [] のインスタンス宣言は,次のようになっています.

instance Functor Maybe where
  -- fmap :: (a -> b) -> Maybe a -> Maybe b
  fmap f Nothing  = Nothing
  fmap f (Just x) = Just (f x)

instance Functor [] where
  -- fmap :: (a -> b) -> [a] -> [b]
  fmap = map
Functor クラスのインスタンスはあくまで Maybe[] であって,Maybe a[a] ではないことに注意します. 一般に,Functor クラスのインスタンスは,具体型を 1 つとる型構成子です.

数学的には,ファンクタは次の 2 つの規則(ファンクタ則)を満たすことが要求されます.

  • 恒等射の保存: fmap id = id

  • 合成の保存: fmap (f . g) = fmap f . fmap g

実際,Maybe[] も両者を満たしています(簡単に証明できます). ただし,これらの性質が満たされているかどうかについて,コンパイラによるチェックは行われません.

関数 f の型が f :: a -> b のとき,fmap f の型は fmap f :: t a -> t b です. この関係を図にすると,次のようになります.

関数 fmap によって,普通の関数 f が,(Maybe[] で)ラッピングされた値に作用する関数 fmap f に変換されている様子がよくわかります. こうした変換のことを,関数の持ち上げ(lifting)ということがあります.

2. モナドの定義

モナドもファンクタと同じく,なんらかの型をラッピングした型を表す型クラスです. ファンクタで見たさっきの図の矢印の向きを少し変えると,モナドを説明した図ができあがります.

この図の $m$ がモナドのインスタンス,演算 $(\cdot)^*$ がモナド版の持ち上げ演算です. なお, $f^*$ というのは数学上の表記であり,実際の Haskell プログラムではこれを (>>= f) と書くことになります.

ちなみに,演算 $(\cdot)^*$ は クライスリスター(Kleisli star)と呼ばれます.

モナドの型クラスは次のように定義され,基本的な演算として >>=return を持ちます.

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

  (>>) :: m a -> m b -> m b
  m >> n = m >>= \_ ->  n

  fail :: String -> m a
  fail = error

ファンクタのときと同じく,数学的には,モナドは次の 3 つの規則(モナド則)を満たさなければなりません.

  • 左単位元律: return x >>= f = f x

  • 右単位元律: m >>= return = m

  • 結合律: (m >>= f) >>= g = m >>= (\x -> f x >>= g)

演算 >>=バインド(bind)演算と呼ばれる左結合演算子で,Functor クラスの fmap と同様に関数を持ち上げます. 演算 return は,普通の値をラッピングされた値にする関数です(C/C++ とかの return 文とは関係ありません). その他の演算 >>fail はあまり重要ではありません.

演算 >>=return の関係を図にすると,次のようになります.

演算 >>=return は,例えば次のように使われます.

f x = return (2 * x)

main = do print $ Just 5 >>= f     -- 出力: Just 10
          print $ Nothing >>= f    -- 出力: Nothing
          print $ [1, 2, 3] >>= f  -- 出力: [2, 4, 6]
          print $ [] >>= f         -- 出力: []

ファンクタを理解できていれば,モナドも自然に理解できますね(少なくとも Maybe モナドみたいな簡単なモナドなら).

Haskell で定義されている代表的なモナドには,次のようなものがあります.

  • Identity

  • Maybe

  • Error

  • [] (リスト)

  • IO

  • State

  • ST

  • Reader

  • Writer

ご存じのように,この中の IO モナドが,現実世界とのやりとり(入出力処理)に使われるモナドです. 入出力の値を IO モナドというベールで隠蔽してしまうことにより,"(参照透過性はそのままで)副作用のある処理を行う" という側面を実現しています.

3. Maybe モナド

定義が簡単な Maybe モナドを例にとって,バインド演算の動きを追ってみます.

Maybe モナドのインスタンス宣言は次のように書かれます.

instance Monad Maybe where
  Nothing >>= f = Nothing
  Just x  >>= f = f x
  return x = Just x
  fail _ = Nothing

Maybe モナドのバインド演算は,失敗しうる計算を連鎖させて,失敗しうる大きな計算を作ります. 次のプログラムはその好例です.

-- x が 2 で割り切れない場合には失敗する
div2 :: Int -> Maybe Int
div2 x = if even x then Just (x `div` 2)
                   else Nothing

-- x が 8 で割り切れない場合には失敗する
div8 :: Int -> Maybe Int
div8 x = return x >>= div2 >>= div2 >>= div2

main = do print $ div8 32  -- 出力: Just 4
          print $ div8 50  -- 出力: Nothing

バインド演算の定義を展開して書くと,関数 div8 は次のように書けます.

div8 :: Int -> Maybe Int
div8 x =
  case return x of
    Nothing -> Nothing
    Just y ->
      case div2 y of
        Nothing -> Nothing
        Just z ->
          case div2 z of
            Nothing -> Nothing
            Just w -> div2 w

これを見て一目瞭然なように,バインド演算は左から右へ順番に式を評価する性質をもちます. IO モナドでも同じ理屈で,"(遅延評価はそのままで)処理を逐次的に実行する" という仕組みが実現されています.

4. do 記法

モナドのバインド演算の構文糖として,do 式(do 記法)が利用できます. この do 式の存在が,モナドを Haskell において特別な存在にしています.

do { 1 ; ...; n }

do 式の中に書ける式は,次の 3 通りです.

(a) モナド式  (すなわち,Monad m => m a 型の式)
(b) パターン <- モナド式
(c) let { 宣言1 ; …​ ; 宣言n }

式 (b) は,右辺が m a 型であれば,a 型の値を左辺のパターンに束縛します. 式 (c) は,普通の変数や補助関数を宣言します. 式 (b),(c) で宣言された名前は,その do 式の中だけで有効です.

do 式の中の最後の式は (a) でなければなりません. この式の値が,do 式全体の値となります.

先の div8 関数は,do 式を用いると次のように書けます.

div8 :: Int -> Maybe Int
div8 x = do y <- div2 x
            z <- div2 y
            div2 z

すこし別の例を見てみましょう. 次のプログラムは,入力された文字列を 1 行ごとに大文字に変換して出力するものです. 空行が入力された時に終了します.

import Data.Char

main :: IO ()
main = do s <- getLine
          let t = map toUpper s
          if null s then return ()
                    else do { putStrLn t ; main }

このプログラムの do 記法を脱糖してみると,次のようになります(簡単のために,言語仕様での定義に基づく展開から少し変更しています).

import Data.Char

main :: IO ()
main =
  getLine >>= (\s ->
    let t = map toUpper s in
    if null s then return ()
              else putStrLn t >> main)

実行例は次のようになります($ はプロンプト,太字は入力した文字列).

$ runghc Sample.hs
hello
HELLO
hello, Haskell
HELLO, HASKELL

$