モナド (プログラミング)
関数型プログラミングにおいて、モナドはプログラムを構造化するための汎用的な抽象概念である。対応したプログラム言語では、ボイラープレート的なコードでもモナドを使って除去することが可能となる。これはモナドが、特定の形をした計算を表すデータ型と、それに関する生成と合成の2つの手続きを提供することによって実現されている。生成は任意の基本型の値をモナドに包んでモナド値を生成する手続きであり、合成はモナド値を返す関数(モナド関数)たちを合成する手続きである。[1]
広い範囲の問題をモナドを使うことで単純化できる。例えば、Maybe
モナドを使えば未定義値への対処が簡単になり、List
モナドを使えばリストに入った値を柔軟に扱うことができる。複雑に組み合わさった関数は、モナドを使えば、補助データの管理や制御構造や副作用を除去した簡単なパイプライン構造に置き換えることができる[1][2]。
モナドの概念や用語は圏論から来ている。圏論においては、モナドは追加の構造を持つ関手として定義されている[注釈 1]。1980年代の後半に研究され始め、1990年代前半には、一見無関係な計算機科学上の問題がモナドによって統一的で関数的にモデル化できることが分かってきた。圏論はモナドの満たすべき形式的な条件を与え、これはモナド則と呼ばれている。モナド則はモナド的なコードの形式的検証にも使用可能である。[3][4]
ある種の計算では、その計算の意味をモナドで明確に表現できることを利用して、プログラム言語の便利機能の実装にモナドを使うことができる。Haskellのように、標準ライブラリですでに汎用のモナド構造といくつかのインスタンスを定義している言語もある[1][5]。
歴史
プログラミングにおけるモナドの概念は1980年代のプログラム言語Opal (プログラミング言語)に見られるが、このときは「コマンド」と呼ばれ形式化はされなかった[要出典]。
1991年にEugenio Moggiはプログラムの構造化でモナドを初めて汎用的に使用した[6]。この成果を元に、Philip WadlerやSimon Peyton Jonesといったプログラム言語の研究者(二人ともHaskellの言語仕様に取り組んでいた)により実装が行われた。初期のバージョンのHaskellは問題の多い「遅延リスト」を用いた入出力を使っていたが、Haskell 1.3からは遅延評価により柔軟に合成可能なモナドによる入出力が導入された。
入出力に加えて、プログラム言語研究者とHaskellのライブラリ設計者は構文解析器やインタープリタにモナドを適用することに成功した。Haskellのdo記法に現れる性質から、モナドの概念はapplicative functorとarrowへと一般化された。
長い間、Haskellとその派生だけがプログラミングにおけるモナドの主な利用者であった。最近では、F#がコンピュテーション式または「ワークフロー」と呼ばれる機能を導入した。これは、命令型プログラムしか経験のないプログラマにもなじみやすい構文を使ったモナド的構成を提供しようという試みである[7]。
動機付けと例
プログラム言語Haskellはモナドを多用する関数型の言語であり、簡単にモナドの合成ができるような構文糖衣を持っている。特に指定がない限り、この記事のコード例はHaskellで書かれている。
モナドの紹介として一般的なふたつの例 Maybe モナドと I/O モナドを取り上げる。もちろん、モナドはHaskellに限られたものではないので、JavaScriptでの Writer モナドの例もその次に扱う。
Maybeモナド
オプション型 Maybe a を考えよう。これは型 a の値をひとつ持つか、全く持たないかのふたつの状態を表現する型である。これらを区別するために、二種類の代数的データ型構成子を持つ。Just t
はひとつの値t
を保持し、Nothing
は値を何も保持しない。
data Maybe t = Just t | Nothing
この型を使って検査例外の簡単なものを作ってみたいとする。計算の途中で失敗した場合は、残りの計算は飛ばして結果Nothing
を返し、すべての計算が成功した場合は何か値x
を保持したJust x
を返すことにする。
以下の例では、add
はMaybe Int型のふたつの引数を受け取って、同じ型の結果を返す。mx
とmy
がともにJust
である場合は、これらの値の和をJust
に入れて返したい。もし、mx
とmy
のどちらかがNone
だった場合は、Nothing
を返したい。こういった振る舞いをする関数を単純に実装しようとしたら、「if Nothing
then Nothing
else Just x
のx
に何かする」の形の場合わけを複数ネストすることになり、すぐやっかいなものになる[1]。
add :: Maybe Int -> Maybe Int -> Maybe Int
add mx my =
case mx of
Nothing -> Nothing
Just x -> case my of
Nothing -> Nothing
Just y -> Just (x + y)
これを楽にするための、各計算を繋げる演算子を定義することができる。二項演算子 bind (>>=
) は失敗する可能性のある計算の結果を、もうひとつの失敗する可能性のある計算へ連鎖する。最初の引数が Nothing
だった場合は、二番目の引数(関数引数)は無視されて、単に計算全体が失敗する。もし、最初の引数が Just x
だった場合は、この値 x
は二番目の関数引数に渡されて、この関数の結果として新しい Maybe の値が返される。これは Just
な値か失敗かのどちらかになる。
(>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
Nothing >>= _ = Nothing -- 失敗した計算は Nothing を返す
(Just x) >>= f = f x -- 値 x に関数 f を適用する
計算の状態に影響を与えずに値を投入する構築子は、Just
がすでにそうなっている。
return :: a -> Maybe a
return x = Just x -- 値 x を包んで、型 (Maybe a) の値として返す
これらを使うことで、上の例は次のように書ける。
add :: Maybe Int -> Maybe Int -> Maybe Int
add mx my = -- (Maybe Int) 型のふたつの値を足す関数。各入力は Nothing の場合がある
mx >>= (\x -> -- mx が Nothing でない場合に、値 x を抽出する
my >>= (\y -> -- my が Nothing でない場合に、値 y を抽出する
return (x + y))) -- 値(x+y)を包んで、(Maybe Int)型の値として和を返す
さらにdo記法という構文糖衣を使うことで、次のように書くことができる。
add :: Maybe Int -> Maybe Int -> Maybe Int
add mx my = do
x <- mx
y <- my
return (x + y)
この種の操作は頻出なので、Haskellの標準関数(liftM2
)が用意されている。これはふたつのモナド的な値(ここでは、ふたつのMaybe)と、その中身(ふたつの数値)を組み合わせる関数(加算)を受け取る関数であり、上の例は次のように書ける。
add :: Maybe Int -> Maybe Int -> Maybe Int
add = liftM2 (+)
(上記のdo記法を使ってliftM2の定義を書いてみよう)
I/Oモナド
Haskellのような純粋関数型言語では関数が外部から観測可能な作用をもつことは、関数の意味から外れることになるため許されない。しかし、関数が直接副作用を起こすのではなく、求める副作用を記述する値を構築して返し、呼び出した側が適当な時に実行することは可能である[8]。Haskellの記法では、型 IO a の値がこのようなアクションを表現していて、実行されたときには、型 a の何らかの値を生成する。
IO
型の値は「現在の世界の状態を引数に取り、関数の返り値として変化した状態を持つ新しい世界を返す」という意味でアクションとみなすことができる。例えば、関数 doesFileExist
と removeFile
は次の型を持つ関数である。
doesFileExist :: FilePath -> IO Bool
removeFile :: FilePath -> IO ()
removeFile
は関数としては、FilePath
を引数にとり、ある IO
アクションを返す。このアクションが実行されると世界を、この場合はファイルシステムを FilePath
という名前のファイルの存在しないものに変更する。この場合は、IO
の持っている値は ()
型の値なので、なにも結果は生成されず、呼び出し側はこれを気にする必要はない。一方で、doesFileExist
の場合は、関数の返す IO
アクションはブール型の値、True
または False
を包んでいて、概念的には、アクションが実行されたときのファイルシステムに FilePath
という名前のファイルが存在したかどうかを呼び出し側が知っているという世界の状態を表現している。アクションからアクションへと世界の状態を渡して管理するこの方法により、決まった順序で状態を変化させていくアクションの列を定めることが可能となる。この過程は時相論理において宣言的な命題のみで時間の経過を表現する手法と似ている。以下の例ではプログラム内のアクションを連鎖する方法の詳細を再び Haskell を使って明らかにする。
基本的な種類の入出力操作、標準出力に文字列を書く、標準入力から文字列を読む、ファイルの読み書き、ネットワーク越しのデータ送信等をすべて記述できるようにしたい。さらに、これらのプリミティブを組み合わせて大きなプログラムを作れる必要もある。例えば次のように書けることが望ましい。
main :: IO ()
main = do
putStrLn "What is your name?"
name <- getLine
putStrLn ("Nice to meet you, " ++ name ++ "!")
この直観的な記法はどのように形式化できるだろうか?このためには各入出力アクションに対して、いくつかの基本的な操作を実行できる必要がある。
- ふたつの入出力アクションをひとつにできなければならない。Haskellでは、中置演算子
>>
を用いて、putStrLn "abc" >> putStrLn "def"
と書き、コンソールに二行の文字列を表示するひとつのアクションである。演算子>>
の型は IO a → IO b → IO b であり、ふたつの入出力アクションを引数に取り、順番に実行するひとつのアクションにまとめて返す。 - 何もしないという入出力アクションも必要である。これは値を返すがなんの副作用も持たない。Haskellではこのアクションは
return
で構築され、この型は a → IO a である。 - さらに、最初のアクションの結果に基づいて次に実行するアクションを決める必要がある場合がある。Haskellでは、演算子
>>=
(bindと読む)を使い、型は IO a → (a → IO b) → IO b である。左側のオペランドは型a
の値を返す入出力アクションであり、右側のオペランドは左側のアクションの生成した値に応じて次に実行する入出力アクションを選択する関数である。結果は、最初のアクションを実行しその値を関数に渡した評価結果をアクションとして実行し、最後にこのアクションの結果を返す合成されたひとつのアクションとなる。
- Haskellのこの演算子を使った例は
getLine >>= putStrLn
であり、これは標準入力から一行文字列を読み込み、それをそのまま標準出力に書き出す。最初の演算子>>
は単にこの演算子の特別な場合である。最初のアクションの結果は無視され、二番目のアクションは常に同じである。
これらの3つの演算子をプリミティブな入出力操作として「いかなるプログラムでも」書けるかというのは、データ変換(ラムダ式使う)やif式やループを含めたとしても自明であるとは言いがたい。上の例は長い式を使って次のように書ける。
main =
putStrLn "What is your name?" >>
getLine >>= \name ->
putStrLn ("Nice to meet you, " ++ name ++ "!")
bind 演算子の持つパイプライン構造により、getLine と putStrLn の命令はそれぞれ一度だけ、書いた順序で評価され、入力ストリームから文字列を取り出して、出力ストリームに出力する副作用は関数型パイプラインの中でも正しく実行される。これは言語が関数をアウト・オブ・オーダー実行や遅延評価する場合でも成立する。
明らかに入出力の定義とMaybeの定義は共通の構造を持っているが、異なっている部分も多い。上述した構造や他のよくある似たような多くの構造の抽象化である。モナドの一般化された概念は、計算の一部が副作用を発生させるにもかかわらず、純粋関数的な計算として実行したくなるようなあらゆる状況を包含している。
形式的定義
モナドは、元となる型システムが与えられたとき、その内部に対応する型システム(モナド的型システムと呼ぶ)を埋め込む構成である(つまり、各モナド的型は元のシステムの型でもある)。このモナド的型システムは元の型システムの重要な点は全て保存するが、モナドに固有の特徴を追加する[注釈 2]。
モナドは複数の方法で定義できる。そのうちの1つは「モナドとは、ある法則(モナド則)を満たす三つ組みである」[9] で表される定義である。すなわち圏論のクライスリ圏におけるクライスリトリプルである。
三つ組
三つ組は以下の要素からなる。
名前 | 別名 | 定義式 | 説明 |
---|---|---|---|
型構築子M | 型 x から派生した別の型を得る(型から型を作る)演算子
| ||
unit関数 | return | unit :: x -> M x
|
型 x の値を型 M x の値へうつす多相な関数
|
bind関数 | >>=
|
bind :: M x -> (x -> M y) -> M y
|
型 M x の値、型 x の値を型 M y の値へ写す関数の2つから M y を得る関数
|
型構築子M
class Monad M where
...
value :: M x
型構築子Mは、ある型 x
から派生した型を得る方法を定める[10]。得られる型はしばしば M x
で表記される。
unit 関数
unit関数( return
とも[注釈 3])は型 x
の引数を型 M x
の返り値にうつす関数である。すなわちunit 関数は型 x
の値をモナド型 M x
の値へうつす多相な関数である。引数値変換の有無は定義されない(一般には値を変換せずに保持する(例:入力 1
=>出力 Maybe(1)
)。
bind 関数
bind関数( >>=
とも[11])は「モナド的値」と「値をモナド的値へうつす関数」を引数にとり、第1引数のモナド的値を第2引数関数の値域へうつす。
モナド則
三つ組 {M, unit(return), bind(>>=)} をモナドと成す規則をモナド則という[12]。以下がモナド則である。
- return は >>= に対して、ほとんど単位元である。
(return x) >>= f ≡ f x
m >>= return ≡ m
- ふたつの関数を続けて bind するのは、これらの関数から決まるひとつの関数を bind することに等しい。
(m >>= f) >>= g ≡ m >>= ( \x -> (f x >>= g) )
公理を do 記法で表現することもできる。
do { f x } ≡ do { v <- return x; f v }
do { m } ≡ do { v <- m; return v }
do { x <- m; y <- f x; g y } ≡ do { y <- do { x <- m; f x }; g y }
また、モナド合成演算子
(f >=> g) x = (f x) >>= g
を使うと
return >=> g ≡ g
f >=> return ≡ f
(f >=> g) >=> h ≡ f >=> (g >=> h)
fmap と join
Haskellではモナドを return と bind 関数を用いて定義するが、return にふたつの演算子 join と fmap を加えることでも定義可能である。この定式化は圏論における元のモナドの定義により近くなる。fmap 演算子は型 (t→u) → M t→M u[13] を持ち、ふたつの型間の関数を受け取って、モナドの中の値に「同じこと」をする関数を返す。join 演算子は型 M (M t)→M t を持ち、二層になったモナド的な情報を平らにしてひとつにする。
ふたつの定式化は以下の関係を持つ。
fmap f m = m >>= (return . f)
join n = n >>= id
m >>= g ≡ join (fmap g m)
ここで、m は M t、n は M (M r)、f は t → u、g は t → M v の型を持つ。t、r、u、v は元の型である。
モナドでなくても、型と関数からなる圏では fmap 関数は任意の函手に対して定義できる。函手は次の法則を満たす。
fmap id ≡ id
fmap (f . g) ≡ (fmap f) . (fmap g)
同じ圏で、return は基点付き函手を特徴付ける。これは値を函手の中に「持ち上げる」ことによる。満たすべき法則は次の通りである。
return . f ≡ fmap f . return
さらに、join によってモナドが特徴付けられる。
join . fmap join ≡ join . join
join . fmap return ≡ join . return = id
join . fmap (fmap f) ≡ fmap f . join
加法的モナド
加法的モナドは、モノイド則を満たす二項演算子 mplusとその単位元としてモナド的な値 mzero を備えたモナドである。演算子 mplus の型は M t → M t → M t であり(ここで、M はモナドの構築子であり t は元の型である)、結合律と左右からの単位元律を満たす。
(a `mplus` b) `mplus` c = a `mplus` (b `mplus` c)
m `mplus` mzero ≡ mzero `mplus` m ≡ m
このことから、加法的モナドはモノイドでもある。>>=
に対しては、mzeroはヌル要素として振舞う。ゼロを掛けるとゼロになるように、mzero と関数を bind すると結果はゼロとなる。
mzero >>= f ≡ mzero
同様に、モナド的な値 m とつねにゼロを返す関数を bind すると、結果はゼロである。
m >>= (\x -> mzero) ≡ mzero
直観的には、モナド的値としてのゼロはモナドに関連した構造だけを持ち元の型の値を持たない。Maybe モナドでは、「Nothing」がゼロである。リストモナドでは、空リストがゼロである。
構文糖衣: do記法
bind 演算子 >>=
を直接使ってプログラムを書くのがよい場合もあるが、典型的にはdo記法 (OCamlでは perform記法、F#ではコンピュテーション式)を使用し、命令型言語のような見た目にできる。コンパイラはdo記法の式を >>=
を使ったものに変換する。例えば
a = do x <- [3..4]
[1..2]
return (x, 42)
を変換した結果は以下となる。
a = [3..4] >>= (\x -> [1..2] >>= (\_ -> return (x, 42)))
リストモナドの実装を見てみよう。リストにマップを行い結合を行う(平らにする)この関数は concatMap とも呼ばれる。
instance Monad [] where
m >>= f = concat (map f m)
return x = [x]
fail s = []
よって、以下のような変形が行われて、それぞれの式はすべて等価である。
a = [3..4] >>= (\x -> [1..2] >>= (\_ -> return (x, 42)))
a = [3..4] >>= (\x -> concatMap (\_ -> return (x, 42)) [1..2] )
a = [3..4] >>= (\x -> [(x,42),(x,42)] )
a = concatMap (\x -> [(x,42),(x,42)] ) [3..4]
a = [(3,42),(3,42),(4,42),(4,42)]
リスト[1..2]は使われないことに注意しよう。左矢印をつけないことで、連鎖した関数は引数を無視するように変換されて、値ではなくモナド的な構造だけが問題になることを示すことができる。例えば、状態モナドで値を生成せずに状態だけを変化させるようなときに使われる。do記法は >>=
の単なる構文糖衣であるので、どんなモナドに対しても使うことができる。
Maybeモナドの値を安全に割り算する以下の定義も等価である。
x // y = do
a <- x -- xとyの中に値がある場合は取り出す
b <- y
if b == 0 then Nothing else Just (a / b)
x // y = x >>= (\a -> y >>= (\b -> if b == 0 then Nothing else Just (a / b)))
同様にF#ではコンピュテーション式を使うことができる。
let readNum () =
let s = Console.ReadLine()
let succ,v = Int32.TryParse(s)
if (succ) then Some(v) else None
let secure_div =
maybe {
let! x = readNum()
let! y = readNum()
if (y = 0)
then None
else return (x / y)
}
このmaybeブロックは構文糖衣であり、以下の式に変換される。
maybe.Delay(fun () ->
maybe.Bind(readNum(), fun x ->
maybe.Bind(readNum(), fun y ->
if (y=0) then None else maybe.Return(x / y))))
モナド的な汎用関数
安全な割り算の結果は、値が Nothing であるかを素直に確認せずに(つまり割り算の結果があるかを確認せずに)そのまま計算に使えたほうが便利なことがある。これは「持ち上げ」関数を使うことで可能である。この関数は Maybe に限定されず、任意のモナドに対して定義される。Haskellでは LiftM2 と呼ばれる。
liftM2 :: Monad m => (a -> b -> c) -> m a -> m b -> m c
liftM2 op mx my = do
x <- mx
y <- my
return (op x y)
関数型を示す矢印が右結合であることから、liftM2は2引数の関数を引数に取って、さらなる2引数の関数を返す。型シグネチャは m がモナドのとき、任意の2引数関数を「持ち上げ」ることができることを示している。例えば、
(.*.) :: (Monad m, Num a) => m a -> m a -> m a
x .*. y = liftM2 (*) x y
は演算子 (.*.) を定義し、それは引数がどちらも Nothing でないときに、掛け算した結果を返す(どちらかが Nothing の場合は Nothing を返す)。この方法の利点はモナドの実装に深入りする必要がないことである。他の同じような関数やモナドを使う場合でも、liftM2 を使うことで、やりたいことが鮮明になる(コードの再利用を見よ)。
数学的には、liftM2 は次のように定義される。
Haskellのモナドチュートリアル
- Monad Tutorials Timeline Probably the most comprehensive collection of links to monad tutorials, ordered by date.
- Piponi, Dan (2006年8月7日). “You Could Have Invented Monads! (And Maybe You Already Have.)”. A Neighborhood of Infinity. 2015年2月21日閲覧。 — The most famous "blog post" tutorial.
- Yorgey, Brent (12 March 2009). “The Typeclassopedia”. The Monad.Reader (13): 17–68 . — An attempt to explain all of the leading typeclasses in Haskell in an elementary way, with monadic functors considered as only one form, best understood by comparison with others: e.g., the more general idea of a "Functor" as something you can map over; "Applicative" functors, and so forth; contains an extensive bibliography.
- Yorgey, Brent (2009年1月12日). “Abstraction, intuition, and the "monad tutorial fallacy"”. blog :: Brent -> [String]. WordPress.com. 2015年2月21日閲覧。 — Opposes the idea of making a tutorial about monads in particular.
- What a Monad is not deals with common misconceptions and oversimplifications in a humorous way.
- beelsebob (2009年3月31日). “How you should(n’t) use Monad”. No Ordering. WordPress.com. 2015年2月21日閲覧。 — Takes a similar point of view, locating monads in a much wider array of Haskell functor classes, of use only in special circumstances.
- Vanier, Mike (2010年7月25日). “Yet Another Monad Tutorial (part 1: basics)”. Mike's World-O-Programming. LiveJournal. 2015年2月21日閲覧。 — An extremely detailed set of tutorials, deriving monads from first principles.
- “A Fistful of Monads”. 2015年2月21日閲覧。 An explanation of Monads, building on the concepts of Functors, Applicative Functors and Monoids discussed in the previous chapter.
- Functors, Applicatives and Monads in Pictures. A humorous beginner's guide to monads.
古いチュートリアル
- All About Monads
- Haskell Wiki: Monads as Computation
- Haskell Wiki: Monads as Containers
- Norvell, Theodore. “Monads for the Working Haskell Programmer”. Memorial University of Newfoundland. 2015年2月21日閲覧。
- Klinger, Stefan (15 December 2005). The Haskell Programmer’s Guide to the IO Monad — Don’t Panic. Centre for Telematics and Information Technology, University of Twente. ISSN 1381-3625 .
- Turoff, Adam (2007年8月2日). “Introduction to Haskell, Part 3: Monads”. ONLamp. O'Reilly Media. 2015年2月21日閲覧。
- Monads A monad tutorial providing examples of non-trivial monads apart from the conventional IO/Maybe/List/State monads.
他の文書
- van Tuyl, Henk-Jan (2010年2月27日). “A tour of the Haskell monad functions”. 2015年2月21日閲覧。
- Moggi, Eugenio. Notions of computation and monads . — The original paper suggesting use of monads for programming
- Wadler, Philip (August 2001). Monads for functional programming. University of Glasgow . — Describes monads in Haskell (before they were implemented)
Scala のモナドチュートリアル
- League, Chris (12 July 2010). Monadologie: Professional Help for Type Anxiety (flv) (Tech talk). New York City: New York Scala Enthusiasts.
- Morris, Tony (2010年6月22日). “Understanding Monads using Scala (Part 1)”. λ Tony’s blog λ. 2015年2月21日閲覧。
- “Monads are Elephants” (2007年9月18日). 2015年2月21日閲覧。
- “Pro Scala: Monadic Design Patterns for the Web”. pp. 300 (2010年4月25日). 2015年2月21日閲覧。