PR

 今回は「モナド(monad)」について説明します。モナドはHaskellの重要な特徴の一つなので,名前くらいは聞いたことがある人が多いでしょう。ただ,「モナドは難しい」という声もよく聞きます。

 モナドとは一体なんでしょうか。前回,「HaskellはIOを取り扱うためにモナドと呼ばれる特別な仕組みを使用することで有名です」と書きました。Haskellは遅延評価を行うため,プログラマが処理の順番を確実に指定することができず,そのままでは入出力の処理には不向きです。モナドを使えば制御構造を導入できるため,この問題を解決できます。前回でいえば,(IO a -> IO a)にマッチする関数――finallyやprintThenAdd――を定義している部分がモナドに相当します。また,GHCiのプロンプトにもモナドが使われています。このように入出力操作を行うモナドの代表格が「IOモナド」です。ライブラリによっては,ある種のパターン化した処理の組み合わせを生成するモナドを提供していることもあります。

 ちなみに,モナド導入以前のHaskellでは,ストリーム(stream,遅延リスト(lazy list)ともいう)を利用した対話的モデル(Dialogue model)や継続(Continuation)がIO処理に使われていました(参考リンク)。ただ,これらの抽象化能力はモナドに劣ります。

 モナドの強力さは単にIOを直接扱えることだけにとどまりません。モナドの強力さは,IOだけでなく様々な計算処理を共通した枠組みで取り扱えるところにあります。こうした仕組みはどうやって実現されているのでしょうか? この点について知るために,まず最初にFunctor(函手,関手)ついて説明し,それからモナドを説明する形を取りたいと思います。少し遠回りに思えるかもしれませんが,Functorのように基本的なものを知っておくことは,今後Haskellプログラミングについて学んでいくうえでは重要だと思います。

mapを多相的に扱うためのFunctor

 Haskellのリストにはmapという関数があります。リスト内の各々の要素にある関数を適用し,その結果のリストを返します。mapの型は(a -> b) -> [a] -> [b]であり,データ構造内の各要素に関数を適用するための「高階関数(higher-order function)」として定義されています。

Prelude> map (1+) [3,1,4]
[4,2,5]
Prelude> map (: "1") ['a','b','c']
["a1","b1","c1"] 

 (: "1")の(:)はデータ構成子です。'a'というように'で囲まれた部分は,"1"とは違い,文字列リテラルではなく文字リテラルであることに注意してください。といってもピンとこないかもしれません。前回,紹介したリストの定義をもう一度見てみましょう。

Prelude> :i []
data [] a = [] | a : [a]        -- <wired into compiler>
instance Eq a => Eq [a]         -- Imported from GHC.Base
instance Monad []       -- Imported from GHC.Base
instance Functor []     -- Imported from GHC.Base
instance Ord a => Ord [a]       -- Imported from GHC.Base
instance Read a => Read [a]     -- Imported from GHC.Read
instance Show a => Show [a]     -- Imported from GHC.Show
Prelude> 

 文字列であるChar型のリスト([Char]型)を構成するには,データ構成子(:)を適用する値はChar型である文字と[Char]型である文字列でなければなりません。したがって,文字リテラルの部分を文字列リテラルにすると,リストの定義により型エラーが生じるのです。

 さて,mapは便利なので,リスト以外の自分の使ったデータ構造にも同じような「データ構造内の各要素に関数を適用するための関数」を用意したくなるかもしれません。しかし,そうする場合には以下のいずれかを選ぶ必要があります。

  1. 名前の衝突が起こることを覚悟したうえで,同じmapという関数名を使う
  2. 衝突を回避するために,mapTreeのように対象とするデータ型名(この場合はTree)などで修飾された名前を付ける(mapとは関係ない名前を付けると,機能を類推しにくくなるため)

 この問題は,mapがリストに対して定義されているために生じるものです。mapを型クラスのようなものに対して定義してあれば,この問題を回避できるのではないでしょうか?

 ここで出てくるのが冒頭に挙げたFunctorです。Haskellは,データ構造ごとに用意したmapを多相的に扱うためのFunctorというクラスを用意しています。ただし,関数名はmapではなくfmapになります。

 :infoコマンド(短縮形は:i)を使うと,クラスの定義とどんなインスタンスが宣言されているかを見ることができます。まず,PreludeのFunctorクラスを見てみましょう。

Prelude> :i Functor
class Functor (f::* -> *) where fmap :: (a -> b) -> f a -> f b
        -- Imported from GHC.Base
instance Functor []     -- Imported from GHC.Base
instance Functor IO     -- Imported from GHC.IOBase
instance Functor Maybe  -- Imported from Data.Maybe
Prelude> 

 Data.TreeモジュールやData.Queueモジュールでも,Functorクラスのインスタンスが定義されています。

Prelude> :m Data.Tree Data.Queue
Prelude Data.Queue Data.Tree> :i Functor
class Functor (f::* -> *) where fmap :: (a -> b) -> f a -> f b
        -- Imported from GHC.Base
instance Functor []     -- Imported from GHC.Base
instance Functor IO     -- Imported from GHC.IOBase
instance Functor Queue  -- Imported from Data.Queue
instance Functor Tree   -- Imported from Data.Tree
instance Functor Maybe  -- Imported from Data.Maybe
Prelude Data.Queue Data.Tree> 

 ではFunctorクラスの内容を見てみましょう。

 Functorクラスの型変数fは型構成子を意味しています。これでピンと来ない人は,前回の記事の,[a]が[] aの省略記法であることを思い出してください。Functorクラスのように型構成子に対して関数のインタフェース(関数名と関数の型)を定義したものを「型構成子クラス(type constructor class,型構築子クラス)」と呼びます。

 fの後ろについている「* -> *」という型宣言のようなものは,型の型である「種(kind,種類)」を表現したものです。種自体は標準Haskell範囲内の概念であるものの,こうした種の明示的な宣言は,GHCなどの一部の処理系が「種注釈(kind annotation)」を行っていることに起因するものです。Hugsでは,このような明示的な宣言は行いません。

 とはいえ,種注釈があった方が理解しやすくなります。また,種注釈はHaskellの次期標準――Haskell'(Haskell prime,開発コード名)――に取り入れられる可能性が高いので,知っておく価値はあります(参考リンク)。

 GHCでは,:kindコマンドで型の種を調べることができます。

Prelude>Prelude Data.Queue Data.Tree> :kind []
[] :: * -> *
Prelude Data.Queue Data.Tree> :k IO
IO :: * -> *
Prelude Data.Queue Data.Tree> :k Maybe
Maybe :: * -> *
Prelude Data.Queue Data.Tree> :k Tree
Tree :: * -> *
Prelude Data.Queue Data.Tree> :k Int
Int :: *
Prelude Data.Queue Data.Tree> :k Bool
Bool :: *
Prelude Data.Queue Data.Tree> :i Bool
data Bool = False | True        -- <wired into compiler>
instance Bounded Bool   -- Imported from GHC.Enum
instance Enum Bool      -- Imported from GHC.Enum
instance Eq Bool        -- Imported from GHC.Base
instance Ord Bool       -- Imported from GHC.Base
instance Read Bool      -- Imported from GHC.Read
instance Show Bool      -- Imported from GHC.Show
Prelude Data.Queue Data.Tree> :k IO Int
IO Int :: *
Prelude Data.Queue Data.Tree> 

 リストやTreeなどの1引数の型構成子の種が*->*である対し,IntやBoolのような無引数の型構成子の種が*であることがわかりますね。型構成子(上の例ではIO)を,種が*の型(上の例ではInt)に適用すると,種の*の数が一つ減ることもわかります。Haskellで多引数を取る関数を値に適用すると,引数が一つ減るのと同じです。

 これらのことから,Functorクラスのインスタンスを[a]や(IO a)ではなく[]やIOのように書かなければならない理由がわかると思います。「Functorクラスとそのインスタンスは,種が一致しなければならない」という制約があるのです。もし(IO a)やBoolのような種が*であるものをインスタンスにしようとすると,GHCでは"Kind error",Hugsでは"Illegal type in class constraint"というエラーが表示されます。

 Functorには他にもfmapに関する制約があります。Functorクラスのインスタンスで定義するfmapは以下の二つの法則を満たさなければなりません。

  1. fmap id == id
  2. fmap (f . g) == fmap f . fmap g

 idは恒等関数(identity functionもしくはidentity)の略であり,入力したデータをそのまま返す関数です。(.)演算子は関数合成(functional composition)です。(.)の定義は,(f . g) x = f (g x)です。

 インスタンスがこれらの法則を満たしているかどうかは,Haskellの型システムは自動的にチェックしてくれません。インスタンスを定義する人が自分で保証する必要があります。