Edited at
HaskellDay 7

HaskellのMonadお気持ちチュートリアル

堅苦しい記事ばっかりでは続かないので,今年は不真面目な記事からウォームアップしていこうと思います.あくまで個人的なチュートリアルなので,ちゃんとした知識を手に入れたい場合もっとちゃんとした文献を当たってください.


はじめに

世の中にMonadチュートリアルは溢れている.そして,それぞれみんなMonadにとっつきやすいように,様々な工夫がなされている.多くの場合,それはMonadを日常の馴染みのある何かに例えることで始まる.そういう例えが溢れすぎて,Monadはメタファではないなんて記事が出されるくらいだ.そして残念ながら記事が溢れすぎて逆に混乱を招くため,Haskell界ではMonadチュートリアルが暗黙に避けられている傾向もある.

ただ世の中にそういう記事が溢れるということは,入門者にとってこれらの概念は障壁となっており,みんな色々な記事を見て納得のいく説明を探しているということでもある.だから,選択肢を増やすことはいいことでもあると思うんだ.さすがに,象に例えるのはやりすぎだと思うけどね :wink:

だから賛否両論はあるかもしれないけれど,私なりにこの世にMonadチュートリアルを出してみようと思う.と言っても,多分このチュートリアルでMonadの本質が分かるわけではない.私もあまり分かってないしね.このチュートリアルの目的は,Monadへの畏怖をなくすことにある.ただMonadを何かに例えることから始めるチュートリアルには,みんな飽き飽きしていると思うので,ここでは少し別の側面からはじめてみることにしよう.


非決定的な計算

例えば


1から10までの合計値を計算してください.


という問題を考えてみよう.この答えは,sum関数を使えば簡単に求められる.GHCi上で以下のようにね.

>>> sum [1..10]

55

私たちがよく見る計算問題というのは,この例題のように答えが1つでしかも必ず求められる.さらに,いつ誰が見ても同じ計算値になる.でも,世の中の計算問題はそれだけではない.例えば,次のような問題だ.


$x^2 = 2$ を満たす実数 $x$ を求めなさい.


この答えは何だろう? 答えは $\sqrt{2}$ だと答えた人は正解だ.答えは $-\sqrt{2}$ と答えた人も正解だ.そう,この問題の答えは2つあり,どちらも問題の要求を満たしている.答えは $\sqrt{2}$ か $-\sqrt{2}$ だと答えた人はパーフェクトだ.150点を進呈しよう.

もう1つ,次のような問題を考えてみよう.


$x^2 = -1$ を満たす実数 $x$ を求めなさい.


残念ながらこの問題に答えられる人は,この世に存在しない.そう,この問題の要求を満たす答えは存在しないのだ1

このような問題は,もちろん私たちの世界,つまりプログラミングの世界でも登場する.そこで課題になるのが,これらの問題をどのようにプログラムで書くかだ.私たちプログラマは,問題を切り分けて小さい問題を解くプログラムを書き,その小さなプログラムを組み合わせることで最終的に問題を解くプログラムを作る.

例えば,先の例題を少し拡張して次のような問題を考えてみよう.


$x^2 = 2$ を満たす整数 $x$ を2倍した実数を求めなさい.


この問題を解くプログラムは,


$x^2 = 2$ を満たす実数 $x$ を求めなさい.


という問題を解くプログラムと,答えを2倍するプログラムを組み合わせることで作成できそうだ.ただ,単純にいかないところはこの問題の答えが複数あるところだ.では,このアイデアを具体的にどうプログラミングしたらいいだろう? 単純には,次のように書くことだ.

>>> 2 * sqrt 2

2.8284271247461903

つまり,答えが複数ある問題の中で求めやすいものを,1つだけ持ってくるということだ.そうすると,私たちはいつものプログラミング感覚で計算を行うことができる.だがこの考え方は,次のような問題の場合,困った事態を招く.


$x ^ 2 = 2$ を満たし且つ負の数となる実数 $x$ を求めなさい.


さっきと同じように, sqrt 2 を $x^2 = 2$ となる実数 $x$ を求めるプログラムだとしてしまうと,この問題にそのプログラムを応用した場合は解がなくなってしまう.だが実際は, $-\sqrt{2}$ という答えがちゃんとある.このような問題にも適用できるようにするには,もう少し工夫が必要のようだ.そこで,答えを求めるプログラムを,ありえる答え全てをリストとして出力するものとして書いてみる2

newSqrt :: Floating a => a -> [a]

newSqrt n = [sqrt n, - (sqrt n)]

こうすると,うまくプログラムを組み合わせて先ほどまでの問題に答えることができる.

>>> map (* 2) (newSqrt 2)

[2.8284271247461903,-2.8284271247461903]
>>> filter (< 0) (newSqrt 2)
[-1.4142135623730951]

では,次の問題だ.


$x ^ 2 = 2$ を満たす実数 $x$ と, $y ^ 2 = x + 2$ を満たす実数 $y$ の組 $(x, y)$ を求めなさい.


この問題を, newSqrt をうまく使って解くことはできるだろうか? できる.この問題はリスト内包表記を使えば,綺麗に簡潔に書くことができるんだ.

>>> [ (x, y) | x <- newSqrt 2, y <- newSqrt (x + 2) ]

[(1.4142135623730951,1.8477590650225735),(1.4142135623730951,-1.8477590650225735),(-1.4142135623730951,0.7653668647301795),(-1.4142135623730951,-0.7653668647301795)]

では,リスト内包表記を使わないで書くとどうなるだろう? その答えは下に書いておいた.

>>> concat $ map (\x -> map (\y -> (x, y)) $ newSqrt (x + 2)) $ newSqrt 2

[(1.4142135623730951,1.8477590650225735),(1.4142135623730951,-1.8477590650225735),(-1.4142135623730951,0.7653668647301795),(-1.4142135623730951,-0.7653668647301795)]

単純にありえる通りそれぞれに対して計算を行い,そうするとまた計算で派生が起こるので,その派生を concat で1つにまとめているだけだ.実は,この書き方にはパターンがある.ちょっとそのパターンが見やすいよう意図的に手を加えてみたのが,以下のものだ.

>>> x & f = f x

>>> newSqrt 2 & concatMap (\x -> newSqrt (x + 2) & concatMap (\y -> [(x, y)]))
...

うーんまだちょっと分かりづらいね.多くの人はパターンに気づいただろうが,少し訝しんでもいるだろう.たまたまパターンのように見えてるだけじゃない? って.だから,もう少し複雑な問題で考えてみよう.


$x ^ 2 = 2$ を満たす実数 $x$ と, $y ^ 2 = x + 2$ を満たす実数 $y$ で,$x + y < -\frac{1}{2}$ を満たす組 $(x, y)$ を求めなさい.


この答えは以下のように書ける.

>>> newSqrt 2 & concatMap (\x -> newSqrt (x + 2) & concatMap (\y -> if x + y < - 1 / 2 then [(x, y)] else []))

[(-1.4142135623730951,0.7653668647301795),(-1.4142135623730951,-0.7653668647301795)]

この例題がやっていることは分かるだろうか? 今までのように newSqrt を2回使ってまず実数の組としてありえる可能性を探すところまではいい.そこからさらに,それぞれの可能性に対して,条件を満たすものはそのまま可能性として残し,条件を満たさないものは可能性を無くす [] を返している.もう分かった.確かにあるパターンでプログラムを組み合わせることができるようだ.もう新たな例題は出さなくていいと読者の方が思っていることを祈ろう.そう,パターンとは, v & concatMap f というものだ.このパターンに handleK_nondet という名前をつけて,今までの問題を書き直してみよう.

>>> :{

handleK_nondet :: [a] -> (a -> [b]) -> [b]
handleK_nondet v f = concatMap f v
:}
>>> newSqrt 2 `handleK_nondet` \x -> newSqrt (x + 2) `handleK_nondet` \y -> if x + y < - 1 / 2 then [(x, y)] else []
...
>>> newSqrt 2 `handleK_nondet` \x -> newSqrt (x + 2) `handleK_nondet` \y -> [(x, y)]
...
>>> newSqrt 2 `handleK_nondet` \x -> [2 * x] -- map (* 2) (newSqrt 2)
...
>>> newSqrt 2 `handleK_nondet` \x -> if x < 0 then [x] else [] -- filter (< 0) (newSqrt 2)
...

見事に書き直せた.それぞれがやってることは分かるだろうか? 少し混乱しているかもしれないが,それぞれの細かい意味を捉えようとしなくていい.まず, handleK_nondet がいく通りもの可能性を持つ値に対し,その可能性1つ1つに対して計算を行なっていること,1つ1つの計算もまた派生を行うこと(例えば, filter は条件を満たすか満たさないかで可能性を1つ減らす, newSqrt は正負で2通りに分岐する)を抑えて動作を俯瞰的に見てみることだ.そして, handleK_nondet の定義を自分で展開してみるのがいいだろう.

さて,この handleK_nondet がなんだというんだ.そもそもこの名前の由来はなんなんだと読者の頭の中には様々な疑問が飛び交っていることだろう. handleK という名前の由来の解説は次節に任せるとして,ここではまず nondet の部分の由来から話そう.これは,「非決定的(nondetermistic)」という単語を略したものだ.従来の,計算値が1つに決まるものを決定的というのに対して,これまで見た計算値が1つに決まらず可能性として複数の選択肢があり得るものを非決定的計算値と呼ぶ. handleK_nondet :: [a] -> (a -> [b]) -> [b] というのは,a 型のリストを a 型の非決定的計算値と見立てて,最初に受け取った非決定的計算値の可能性それぞれに対して,次に受け取った関数を適用する.その関数はやはり非決定的計算値を返してくるので,それを全て1つの非決定的計算値にまとめるのだ.

>>> -- [] は選択肢が何も無い非決定的計算値を表すので, `handleK_nondet` は何も計算しない

>>> [] `handleK_nondet` \x -> newSqrt x
[]
>>> -- [1, 4] は1か4の選択肢のある非決定的計算値を表す
>>> -- newSqrt 1 は -1 か 1 で, newSqrt 4 は -2 か 2 の非決定的計算値を返す
>>> [1, 4] `handleK_nondet` \x -> newSqrt x
[1.0,-1.0,2.0,-2.0]

この handleK_nondet は,少し抽象的に捉えてみると,「a 型の非決定的計算値」と「a 型の値を受け取って b 型の非決定的計算値を返す関数」を繋げて,「b 型の非決定的計算値を返す関数」のように見える.実は幾つかの計算体系において, handleK_nondet と同じような型を持ち,計算値と計算値を返す関数を繋げて,1つの計算値にする関数が定義できる.これを, handleK パターンと呼ぶことにしよう.そして,その関数のことを,型はそれぞれ違うが総称して handleK と呼ぶことにしよう.


通常の計算と handleK

非決定的な計算以外にも色々表現できると言うからには,まず通常の計算を表現しておかなければ,読者の多くは納得しないだろう.あるいは,もう試している読者もいるかもしれない.実際,通常の計算について拍子抜けするほど簡単に handleK は定義できる.

handleK_pure :: a -> (a -> b) -> b

handleK_pure x k = k x -- or more simply (&)

この形の関数をどこかで見たことがある人は, handleKK とはあの K のことかと思うかもしれない.そう,その K だ.この形の関数は,継続渡しスタイルというプログラミングスタイルで馴染みのあるもので, k はcontinuation(継続)の頭文字 c の発音からきている3.継続っていうのは名前こそ仰々しいけど,ある計算の残りみたいなものだ.要は,計算の続きだね. handleK という名前は,この継続をうまく処理する(handle)という名前からきている.そう, handleK っていうのは計算値と計算の続きをくっつけて,新たな1つの計算値を作る関数なんだ.以下のようにね.

>>> 1 `handleK_pure` \x -> (x + 1) `handleK_pure` \y -> x / y

0.5

リストの時と同じように書けていることが分かるね.リストの時は,非決定的だったから計算値っていうのは何通りもあることもあれば,全くないこともあった.計算の続きにもやっぱり何通りかの派生ができるかもしれない.それをうまく組み合わせるには,全ての可能性に対してそれぞれ計算の続きを行って,出てきた答えを全てまとめるという処理を行えばよかったんだ.私はそれを, concatMap という関数で実現してみせた.今回は必ず答えは1つだ.計算値の扱い方も全然違う.でも, handleK を使えば同じように小さいプログラムを繋いで大きいプログラムを作ることができる.

もっと他の handleK を使える例を見たくなってきただろうか? でもちょっと待って. handleK っていうのは,計算値と計算の続き,つまり継続をくっつける関数だと言ったね.くっつけるという操作があった時に,私たちには考えなきゃいけないことがある.それは,単位的なもの,つまりくっつけても何も起こらないようなものはないかということさ.

まーた,天からの啓示かと読者は思うだろう. handleK が突然降ってきたときと同じように,どれほどこの概念に値打ちがあるのか全く分からないってね.でも,この概念の嬉しさは,これまでの随所に現れてたんだ.例えば次の問題を思い出してほしい.


$x^2 = 2$ を満たす実数 $x$ を2倍した実数を求めなさい.


この問題の答えを newSqrthandleK で次のようにプログラミングした.

>>> newSqrt 2 `handleK_nondet` \x -> [2 * x]

...

これは非決定的な計算専用のプログラム同士を handleK で繋いでいるけれど,何かを2倍するという動作は別に非決定的な計算特有のものという感じじゃない.例えば,今回の通常の計算でも行えるものだ.後々紹介する他の計算体系でも,この動作は実現できる.では,一体どういう動作が,計算体系に限らず行えるのだろう? それは単位的な計算だ.単位的な計算ってなんだよと思うだろう.まだ何も説明してないからね.ただ,この概念に関して納得してもらうには,もうちょっと他の計算体系を見たもらったほうがいいと思うんだ.だから,ここではくっつけても何も起こらないものと説明しておく.そして,この単位的な計算を作る unit って関数を,非決定的な計算,通常の計算に対して与えてみよう.

unit_nondet :: a -> [a]

unit_nondet x = [x]

unit_pure :: a -> a
unit_pure x = x

unita 型の値を受け取ってそれを元にそれぞれの計算体系の a 型の単位的な計算値を作る関数だ.これを使うと,先ほどの問題を unit で書き直せる.

>>> newSqrt 2 `handleK_nondet` \x -> unit_nondet (2 * x)

そう,前の値を2倍する継続は, \x -> unit (2 * x) と書ける.ひとまずこれからは, handleK パターンには, handleKunit という関数をそれぞれ考えることにしよう.


例外を投げる計算

読者の皆さんがよく扱いたくなる計算が,例外を扱う計算だろう.どういう計算かというと,計算途中で想定外のことが起きて例外が投げられるものだ.例えば,正整数しか扱わない想定なのに負の数が来たり,リストに何か要素があって欲しいのに空のリストが来たりする場合だね.その例外を補足して,途中で復帰したりもする,普通の例外を扱う計算だ.この計算をうまく表して,さっきの handleKunit でうまく繋げられるようにしてみよう.

例外を扱う計算というのは単純に考えれば,正常な計算値がある状態と例外が投げられていて計算どころではない状態の2状態があるということだ.もっと簡単に,例外はそのエラーメッセージだけを情報として持つことにしよう.実用的なことを考えればスタックトレースとかがあるべきだけど,まあそこは目を瞑ってくれ.2状態のうちいずれかのものを表すデータ型は,Haskellではまさに Either (いずれか) という名前で提供されている.つまり,例外を扱う a 型の計算値は, Either String a で表せそうだ.これを使って次の問題を解くプログラムを作ってみよう.


実数 $x > 0$ に対して, $\frac{1}{x^2}$ を求めなさい.


単純な計算問題だけど,この問題では $a$ が負の数や $0$ の時は特に何の取り決めもない.つまり例外の出番だ.

inv2 :: (Ord a, Floating a) => a -> Either String a

inv2 x
| x > 0 = Right (1 / (x ** 2))
| otherwise = Left "Expected positive number!"

ちゃんと動作確認もしてみよう.

>>> inv2 2

Right 0.25
>>> inv2 0
Left "Expected positive number!"

うんうん,うまくプログラム化できてそうだ.では,このプログラムを使って次の問題を解いてみよう.


実数 $x > 0$ に対して, $y = \frac{1}{x^2}$ となる $y < 4$ において, $\frac{y}{y + 1}$ を求めなさい.


この問題を解くプログラムは, inv2 ともう1つのプログラムを組み合わせることで作れそうだ.それは,


実数 $y < 4$ に対して, $\frac{y}{y + 1}$ を求めなさい.


という問題だね.これを計算するプログラムは次のように書ける.

ratioDec :: (Ord a, Fractional a) => a -> Either String a

ratioDec x
| x < 4 = Right (x / (x + 1))
| otherwise = Left "Expected a number less than 4"

inv2 とこのプログラムを繋げる handleK が定義できれば,うまくいきそうだね.この2つのプログラムは両方例外状態になる場合がある.どちらかが例外になれば,最終結果も例外状態になるはずだし,もっというと inv2 で例外が発生した場合は次の計算は行われないはずだ.前の計算が正常に終了してることも期待しての続きの計算だからね.よくある例外機構と同じさ.この動作を実現する handleK を定義してみよう.

handleK_except :: Either String a -> (a -> Either String b) -> Either String b

handleK_except (Right x) f = f x
handleK_except (Left msg) _ = Left msg

この実装は,上の文章をそのまま実装している.前の計算が正常に終了すればその値を元に残りの継続を計算してしまう.でも,例外が発生していたら,継続は破棄して例外のままにする.これで,さっきの inv2a をくっつけて問題を解ける.

>>> :{

| inv2RatioDec :: (Ord a, Floating a) => a -> Either String a
| inv2RatioDec x = inv2 x `handleK_except` \y -> ratioDec y
| :}
>>> inv2RatioDec 0
Left "Expected positive number!"
>>> inv2RatioDec 1
Right 0.5
>>> inv2RatioDec 0.5
Left "Expected a number less than 4"

unit の方も定義してみよう.例外が発生する計算で単位的な計算はどういうものだろう? おそらくそれは,例外を全く発生させる可能性がない計算ではないだろうか? とりあえず,それで定義してみよう.

unit_except :: a -> Either String a

unit_except x = Right x

これがちゃんと単位的な計算なら, \x -> unit_except (2 * x) は前の値を2倍する継続になっているはずだ.ちゃんとそうなっていることが分かるかい? 前の計算で例外が発生したら handleK によってそのまま例外になるし,正常に計算できたらその結果が継続に渡ってきて単に2倍される.ちゃんとなっていそうだね.実際に計算してみよう.

>>> inv2 1 `handleK_except` \x -> unit_except (2 * x)

Right 2.0

これが例えば, unit が以下のように定義されていたらどうなるだろう?

unit_except :: a -> Either String a

unit_except _ = Left "error"

うーんこの場合,何をしても例外になってしまうので, \x -> unit_except (2 * x) の継続は,とても2倍しているとは言えない気がする.そう思わないかい? 単位的な計算って言うのは,何もしない計算だ.でもこの unit の定義は正常な状態を例外状態に変えてしまっているんだ.こういうことをすると,私たちは計算体系によって unit が突然よく分からない動作をしないか, 計算値に対して \x -> unit x といった継続を繋げたら元の計算値と異なる結果になってしまわないか,気にする必要が出てくるんだ.文字列で考えてみると,まず空の文字列で初期化して,そこにどんどん文字列を繋げていくんだけど,なぜか空の文字列に文字列を繋ぐと改行が最初に挿入されるんだ.そういう文字列の実装はあまり嬉しくないだろう? だって最初に空の文字列で初期化したのは,次の文字列を繋ぐ際に何の影響も受けない値を最初に入れておきたかったからだ.改行を勝手に挿入されてしまうと,その前提が崩れてしまう. unit も同じさ.計算体系はそれぞれ全然違うけれど, unit で作った計算値は,くっつけても計算体系独自の影響を受けることがない,気分的にはそういう感じだ.これが単位的計算値の概念だ.後で,厳密的な定義はするけどね.


状態を変えていく計算

例外を扱う計算の他にもう1つ,私たちプログラマに身近な計算がある.それは,グローバル変数を伴った計算だ.定期的に参照してその値を変更しながら計算を行うことを私たちはよくやる.要は再代入可能な変数を使ったプログラミングだ.これと同等のプログラミングを,再代入可能な変数機能がないプログラミング言語で行うにはどうすればいいだろう?

1つの方法は,変数名を新たに上書きしていくことだ.

>>> let x = 1 in let x' = 2 + x in x' / 2

1.5

残念ながらHaskellでは,同じ変数名を定義の左辺と右辺で使用すると再帰的な変数使用と認識されてしまう為,新たな変数名を使用する必要はでてくるけど,上のようにすれば再代入可能な変数とともにプログラミングしている気分を出せる.ただ,これだと xx' が同じ変数を表していることが伝わりづらい為,もう少しプログラミングスタイルを工夫することを考えよう.

再代入可能な変数でのプログラミングとは,最初に何らかの値で初期化された変数が用意され,その変数の中身を随時上書きしていくことでプログラミングを行う.これは,実は関数で模倣できる.

>>> (1, ()) & \(x, _) -> (2 + x, ()) & \(x, _) -> (x, x / 2)

(3,1.5)

元のプログラムと比べ歪ではあるけど,このプログラムは,再代入可能な変数を表す値と計算結果を組として表し,この組を入出力とする関数を1つのプログラムと見ることで,再代入可能な変数とともにプログラミングする気分を味わおうというものだ.最初に与えられる (1, ()) は変数 x1 で初期化されたことを表し,初期化計算の結果は () であることを表す.次に, (2 + x, ())x に前の x の値を使って 2 + x を再代入し,再代入計算の結果は () だと言うことを表す.最後の (x, x) は, x に前の x を再代入する,つまり x の値は特に変えないことを表し,その x の値を計算結果として返すこと,つまり x の値を単純に参照する計算を表す.どうだい,なんとなく意味は理解できたかい? 再代入可能な変数っていうのは,つまり共通のどんどん変わっていくような入力,こういうのを状態っていうけど,前の状態を受け取って次の状態を返す,変数で言えば前の変数の値を受け取って変えても変わらなくてもいいけど次の変数の値を返すような関数で模倣できるんだ.後は最初の状態,変数で言えば初期化する値を決めてその関数を適用するだけでいい.ところで,かんのいい人はもう気づいてるかもしれないけど,この形今まで何度か出てきたものと似てるような気はしないかい?

共通の状態があって,それが計算するたびに変わっていくようなものは,前の状態を受け取って次の状態と計算結果を返す関数で表現できる.つまり,実数言い換えれば Double 型の状態を持つ計算値というのは, Double -> (Double, a) という型の関数で表現される.この計算値は,計算がどの状態で行われるか,つまり前の状態が何だったかで値が変わるということを表現している.とりあえずどういう計算ができるのか具体例を見てみよう.今回は状態を持つ計算値の型がちょっと分かりにくいので, WithX a という名前で,状態を持つ a 型の計算値を表現する型エイリアスを作っている.

type WithX a = Double -> (Double, a)

-- X を参照する
refX :: WithX Double
refX = \s -> (s, s)

-- X に n を再代入する
assignX :: Double -> WithX ()
assignX n = \_ -> (n, ())

-- 状態に関数を適用し,その結果を新たな状態とする
mapX :: (Double -> Double) -> WithX ()
mapX f = \s -> (f s, ())

-- 状態に 2 を加算した結果を新たな状態とする
add2ToX :: WithX ()
add2ToX = mapX (2 +) -- \s -> (2 + s, ())

関数が計算値というのは,なんとも不思議な感覚かもしれないけど,イメージとしては前の状態の値を使って色々するもの,もっというと前の状態によって値が確定する計算値だと思えばいい.じゃあ,この状態を変えていく計算を行う小さなプログラムがあった時,それを組み合わせて大きなプログラムを作るにはどうすればいいだろうか? これもやっぱり handleK パターンで表現できる.

handleK_withX :: WithX a -> (a -> WithX b) -> WithX b

handleK_withX x k = \s -> let (s', r) = x s in k r s'

unit_withX :: a -> WithX a
unit_withX x = \s -> (s, x)

今までよりちょっと複雑な定義で,混乱が起きそうだ.でも,落ちついて考えたら結構簡単なはずさ.今回状態(State)を表す変数に ss' という名前を振っている.私たちが作らなければいけない,まず handleK からだ.最終的に私たちが作らなければいけない計算値は, WithX b ,つまり Double 型の前の状態を受け取って, Double 型の次の状態と b 型の値を返す Double -> (Double, b) 型の関数だということを抑えよう. handleK_withX が返してくるものは,まず前の状態 sx に渡して返ってきた次の状態 s' と計算結果 r を,さらに継続 k に渡して,最終的な状態と計算結果を得る.これを1つの状態を伴う計算値とするって感じだ. unit の方はまだ簡単だね.単純に状態を変えずに結果だけを埋め込む感じだ.これは,次のように使うことができる.

-- mapX の refX と assignX だけを使った再実装

mapXByRA :: (Double -> Double) -> WithX ()
mapXByRA f =
refX `handleK_withX` \x ->
assignX (f x)

-- 状態を参照して,その値を 2 で割った値を計算値とする
div2OfX :: WithX Double
div2OfX =
refX `handleK_withX` \x ->
unit_withX (x / 2)

-- 最初の問題である,状態を 2 加算した値で更新して, 2 で割った値を結果とする
add2ToXDiv2OfX :: WithX Double
add2ToXDiv2OfX =
add2ToX `handleK_withX` \() ->
div2OfX

-- add2ToXDiv2OfX の refX と assignX だけを使った再実装
add2ToXDiv2OfXByRA :: WithX Double
add2ToXDiv2OfXByRA =
refX `handleK_withX` \x ->
assignX (2 + x) `handleK_withX` \() ->
refX `handleK_withX` \x ->
unit_withX (x / 2)

まずは,気分を掴もう.例えば最初の mapXByRA の実装は, refX で状態を参照してその結果を x に入れ, assignXf x の値を再代入している.いわば,よくある let x = ref global.X; global.X := f(x); みたいなプログラムを表している感じだ4.さて,実際には何をしているかというと, mapX を思い出してみるとどうやらこのプログラムは \s -> (f s, ()) という関数を生成するようだ. refX = \s -> (s, s)assignX n = \s -> (n, ()) ということを思い出すと,まず状態 srefX に入れたところで次の状態はそのままで結果として s の値が返ってくる. handleK_withX はこれを継続 \x -> assignX (f x) に渡す.すると今度は状態が f s に更新され結果自体は () な計算値が返ってくるわけだ.これに状態を引き継がせると,最終状態は f s で結果が () という結果が返ってくる.これは mapX f = \s -> (f s, ()) と同じ動作になっていることが分かる.

まあ,動作を正確に追うのはかなり混乱するけど,幸運なことに私たちはさっきの


よくある let x = ref global.X; global.X := f(x); みたいなプログラムを表している感じだ


という感覚で,他のプログラムを捉えられることに気づくはずだ.そうなると,どう実装されているかを推測するのは容易い.また, unit_withX が重要な働きをしていることにも気づくはずだ.実際に動かしてみよう.

>>> add2ToXDiv2OfX 1

(3.0,1.5)
>>> add2ToXDiv2OfXByRA 1
(3.0,1.5)

WithX a 型の値は最初に global.X を初期化する値を指定してあげなければいけないことに注意だ.さて,unit_withX は,通常の計算を計算体系の計算値へと昇格させ,うまく辻褄を合わせるのに非常に便利だ.そして, handleK パターンで小さなプログラムを組み合わせて大きなプログラムを作る際,共通のユーティリティとして有用なものになっている.


究極の疑問の答え

この世で2番目に価値のある計算は,究極の疑問に対する答えを出すことだ.もちろん,1番目は究極の疑問を考えることだ.みなさんは,ランクウィルとフックが設計したかの有名なスーパーコンピュータ,ディープ・ソートを知っているだろうか5? そしてそのコンピュータが750万年かかって出した究極の疑問に対する答えを知っているだろうか? その答えは,42だ.

ディープ・ソートは750万年かけて答えを出したが,私たちはいまやその答えを知っている.そして,この答え以外に計算する価値のあるものなどないだろう.なので,常に42を出力するような計算を考えてみよう.究極の答えを表す計算値を次のように作る.

data TheAnswer a = AnswerIs42

deriving (Eq, Show)

このような計算に対しても,私たちは handleK パターンを考えることができる.

handleK_theAnswer :: TheAnswer a -> (a -> TheAnswer b) -> TheAnswer b

handleK_theAnswer x f = AnswerIs42

unit_theAnswer :: a -> TheAnswer a
unit_theAnswer x = AnswerIs42

handleKunit も今回はやってることは単純だ.究極の疑問への答えは42で,それ以外にはなりようがない.つまり, handleK でつなげる時,継続を計算する必要はない.継続が出してくる答えは42だからだ. unit も全てを無視して42を出力すればいい.というか,それ以外にやりようがない.この計算体系で,まるで何か有意な計算をしているような以下のプログラムを書くことができる.

deepThought :: TheAnswer Int

deepThought =
unit_theAnswer 1 `handleK_theAnswer` \x ->
unit_theAnswer (x * x) `handleK_theAnswer` \y ->
unit_theAnswer (y + x)

実際には,このプログラムは見かけ上の計算を全て無視して,単に42を返す.

>>> deepThought

AnswerIs42


抽象化の時間だ

どうやら, handleKunit というのは色々なものに適用できるようだということは,分かってもらえただろうか? それぞれの計算の勝手は全然違うけれど,計算値と継続をくっつけて1つの計算値にでき,くっつけても何も起こらない計算値があれば,私たちは様々な計算で小さなプログラムから大きなプログラムを作る操作をうまく共通化できるようだ.

実は,それがHaskellで大仰に語られている Monad というものの正体なのだ.Haskellの Monad とは以下のものだ6

class Monad m where

(>>=) :: m a -> (a -> m b) -> m b
return :: a -> m a

(>>=)handleK に対応し,returnunit と似ているということに注目してほしい.今まで型が違うために,handleKunitをそれぞれの計算体系に対して定義してきたが,この型クラスの定義によって違いはうまく吸収できるんだ.例えば,リストを Monad だと思いたかったら,以下のように書けばいい.

注意: 実は以降の定義はコンパイルエラーになる.なぜならインスタンス定義をするのに,大事な部分を飛ばしてしまっているからだ.心配しなくても,最後に私は読者の方にMonadを作ってもらう機会を与えるつもりだ.だから今は安心して,このように書けるのかとしげしげ眺めるのに留めておいてほしい.

instance Monad [] where

m >>= k = concatMap k m -- m `handleK_nondet` k
return x = [x] -- unit_nondet x

[a] という型はHaskellでは, [] a と書くのと等しいということに注意だ.つまり, [a] -> (a -> [b]) -> [b] という型は [] a -> (a -> [] b) -> [] b という型と同じだ.実際に既に,同じインスタンスが Prelude モジュール内で定義されている.

では, handleK_pure :: a -> (a -> b) -> bunit_pure :: a -> a をインスタンスとして書けるだろうか? うーん,これは m a -> (a -> m b) -> m ba -> m a とマッチしそうにない.似てはいるんだけど.こういう場合には少し視点を変えてみることが大事だ.以下のような型シノニムを考えてみよう.

type Identity a = a

そして,無駄に見えるかもしれないけど,これを使って handleKunit の型を書き直してみる. a -> (a -> b) -> b っていうのは, Identity a -> (a -> Identity b) -> Identity b だ.とうぜんだね.だって, Identity a = a だし, Identity b = b だからさ.でもこの無駄に見える書き換えをしてみると,見事に Monad の形にマッチする.でも残念ながら,型エイリアスは型クラスのインスタンスとして書けないんだ.こういう場合は, newtype の登場だ. Identitynewtype にしてみよう.

newtype Identity a = Identity

{ runIdentity :: a
}

Haskellの newtype は妥協の産物みたいなものさ.型エイリアスは型としては少し扱いづらいんだけど7,逆に data 宣言で新しく作った型コンストラクタは扱いやすい. newtype はこの型エイリアスの扱いづらさを払拭するために, datatype の中間をとったみたいなものだ.newtype は型の上では data と同じ扱いを受けるけど,実行時には type と同じように単にエイリアスとして扱われるようになる.まあ,この辺のことは,有用な解説記事があると思うから調べてみてほしい8

さて, newtype を使うと型クラスのインスタンスとして扱えるようになる.

instance Monad Identity where

Identity x >>= f = f x -- Identity (x `handleK_pure` (runIdentity . f))
return x = Identity x -- Identity (unit_pure x)

Identity っていう余計なコンストラクタや runIdentity っていう余計なデコンストラクタが付くけど,これは実行時には消えて無くなるから,無いものとして扱ってほしい. newtype によって一見 m a の形になっていないものを強制的に m a の形にすることができる.こうやって, Monad で色々なものが共通化できる. Identity は実際にbaseパッケージのData.Functor.Identityモジュールで提供されていて, Monad の実装もちゃんと提供されている.

handleK_exceptunit_except はどうだろう? これはそのまま定義できる.

instance Monad (Either String) where

m >>= k = case m of
Right x -> f x
Left msg -> Left msg
-- m `handleK_except` k
return x = Right x -- unit_except x

実際Preludeモジュールではもっと汎用的にインスタンスが定義されている. Either ee の部分がどんな型でも, handleK_exceptunit_except を実装できることに気づいた人はいるだろうか? 実際のHaskellでは eString じゃなくてもどんなものでも handleK パターンもとい Monad が使えるんだ.だから独自に例外用のデータを実装したりもできる.

handleK_withXunit_withX はどうだろう? これは WithX という型エイリアスで既に m a という形にマッチするようになってるけど,やっぱり型エイリアスの制約で Monad のインスタンスにできない.だから,ここでも newtype を使おう.

newtype NewWithX a = NewWithX

{ runNewWithX :: Double -> (Double, a) -- WithX a
}

instance Monad NewWithX where
NewWithX m >>= k = NewWithX (\s -> let (s', r) = m s in runNewWithX (k r) s')
-- NewWithX (m `handleK_withX` (runNewWithX . k))
return x = NewWithX (\s -> (s, x)) -- NewWithX (unit_withX x)

やっぱり NewWithXrunNewWithX を余分につける必要があるけど,これで定義はできる.ところで,状態っていうのは別に Double 型じゃなくてもいい.今回は,実数の状態を考えていたけど,別に整数でもブール値でも,何かユーザが定義したものでもいいわけさ.実は状態がどんな型であろうと,やっぱり Monad のインスタンスを定義できる. transformersパッケージのControl.Monad.Trans.State.Strictモジュールには,このもう少し汎用的なバージョンが定義されていて,もちろん Monad のインスタンスにもなっている9

究極の疑問の答えも Monad にできる.

instance Monad TheAnswer where

m >>= k = AnswerIs42 -- m `handleK_theAnswer` k
return x = AnswerIs42 -- unit_theAnswer x

実は, TheAnswer と同じようなデータ型が,Haskellにはある.baseモジュールのData.Proxyモジュールで提供されている Proxy というデータ型だ(もちろん究極の疑問の答えを表すために,導入されたわけでは無い).もちろん Monad のインスタンスも提供されている.ただ,このデータ型は少々特殊な使い方をするので,ここではどういうものかについて解説しない.もし興味があれば調べてみてほしい10

どうやら今まで登場した, handleK パターンは全部 Monad クラスのインスタンスとして統一できた.これで, handleK_~unit_~~ の部分を明示しないで, >>=return を使うことでプログラムを繋ぐことができるんだ.でもそれだけじゃない.Haskellでは,この抽象化を行うことでよりプログラミングを便利にすることができる.


抽象化して得られるもの

1990年代ごろの偉大な先人たちは,私が保育園で積み木を組み立ててる時に,この handleK パターンもとい Monad を使ったプログラミングが,色々な計算体系に適用できることを発見していた.そして,小さいプログラムを >>=return を駆使して組み合わせ,プログラミングをしていたんだ. WithX で私がやっていたように適度に改行を入れながらインデントを揃えて,なるべく綺麗にどうプログラムを組み合わせたかが分かりやすいようにね.ただ,同時に多くのHaskellerはこの作業に嫌気もさしていた.不満のいくつかは時折挿入しなければいけない括弧にあり,もう一つ大きな不満は理不尽に強制される >>= \x ->>>= \() -> というタイプ数の多い接尾語にあった.このためこの問題を解決するため,幾つかの提案がなされることになる.

この提案の中で脚光を浴びることになったのが,do記法だ.今では,Haskellで当たり前に使われているけどね.do記法の面白さは,まるで普通の命令型言語でプログラムを書いてるかのように,プログラムを書けるところさ.例えば, WithX をdo記法を使って書き直してみよう.

refX :: NewWithX Double

refX = NewWithX (\s -> (s, s))

assignX :: Double -> NewWithX ()
assignX n = NewWithX (\_ -> (n, ()))

add2ToX :: NewWithX ()
add2ToX = do
x <- refX
assignX (2 + x)

div2OfX :: NewWithX Double
div2OfX = do
x <- refX
return (x / 2)

add2ToXDiv2OfX :: NewWithX Double
add2ToXDiv2OfX = do
add2ToX
div2OfX

add2ToXDiv2OfXByRA :: NewWithX Double
add2ToXDiv2OfXByRA = do
x <- refX
assignX (2 + x)
x <- refX
return (x / 2)

何が起こったか分からないって顔をして無いかい? この記法はさっきの NewWithX に対する Monad のインスタンスを定義するだけで使える.不思議に思えるかもしれないけど,じっくり前の handleK を使った実装と見比べてほしい.もしかしたら,こっちの方が見比べる時はいいかもしれないね.

add2ToXDiv2OfX :: NewWithX Double

add2ToXDiv2OfX = do
() <- add2ToX
div2OfX

add2ToXDiv2OfXByRA :: NewWithX Double
add2ToXDiv2OfXByRA = do
x <- refX
() <- assignX (2 + x)
x <- refX
return (x / 2)

これはさっきと同じプログラムだ.どうだい? そうこの記法は一見魔法のように見えるけど,単に x <- mm >>= \x -> に置き換えるだけで変換ができるんだ.さらに少し手を加えてあって, _ <- m と書く代わりに単に m と書くだけでいいようになってる11.本質的には以下の図のような感じに,継続が繋がってるだけだ.

Do Notation Structure of add2ToXDiv2OfXByRA

つまり handleK パターンとやってることは変わらないが,ずっと見やすくなったということだ.これは素晴らしいことだね.また,今まで計算値と継続を繋げるということを感覚で説明したけど,do記法によってスッキリしたおかげで,列を並べてそれを1つの計算値にしている感じがより分かりやすくなったと思う.上の図で言えば, $\leftarrow$ で計算値と継続を繋げているんだ.この $\leftarrow$ が計算結果を変数に束縛(bind)してるように見えるところから, >>= をbindと言ったりする.ところで,実は Monad のインスタンスにするためには,重要な暗黙のルールがあるんだ.そしてこれは,この計算値と継続を「繋げる」という感覚にとても密接な関係があるんだ.

私たちは add2ToXDiv2OfX を,add2ToXdiv2OfX という部品に分けて,これを繋げることで実装した.これは言うなれば,上の図を以下のようにグループ分けした感じだ.

Split to add2ToX and div2OfX

そして,それぞれのグループをまず1つにして,それから2つをくっつけて1つにした感じだ.でも,分かりやすさを無視すれば次のようなグループ分けもできるはずだ.

Split to last and other

具体的には,

m = do

x <- refX
_ <- assignX (2 + x)
refX

k x = do
return (x / 2)

add2ToXDiv2OfXByRA = do
x <- m
k x

こういう実装だ.このグループ分けにどういう意味があるか分からないけど,でもプログラミングをしてる時に共通化の方法は色々あって,状況によって共通化の仕方が異なるといったことはよくあることだ.もっと言うと,元々の add2ToXDiv2OfXByRA の実装だと,下からどんどんグループ分けして,最後に一番先頭の refX と残り全部をくっつけてできた継続をさらにくっつけて1つにしているんだ.もし,このグループ分けによって最終的に出来上がる計算値が違ったらどうなるだろう?その場合,列の共通化方法に細心の注意を払わなきゃいけなくなるだろう.繋ぎ方を抽象化してdo記法まで作って分かりやすくしたのに,私たちはこの記法に嫌気がさすに違いない.

実は,今までの実装はどこを先に共通化しても上手くいくように出来ていた.そして,Haskellでのいかなる Monad インスタンスも,グループ分けをどのようにしても上手くいくようにしなければいけないという決まりがあるんだ.それは,Monad 則と呼ばれるもので,以下のように表される.

> (m >>= k1) >>= k2 = m >>= \x -> k1 x >>= k2

何やら複雑な式に,目眩を覚えそうになる人もいるだろう.そういう人(大半の人がそうだと思うけど)は,とりあえずパッと見てそれから目をそらして,続きの話を読んでほしい.それぞれの式をdo記法で書き直してみよう.

-- 単に f = \x -> f x なことを利用してるだけ!

-- (m >>= k1) >>= k2 = (m >>= \x -> k1 x) >>= \y -> k2 y なことに注意!
leftM = do
y <- do
x <- m
k1 x
k2 y

-- m >>= \x -> k1 x >>= k2 = m >>= \x -> k1 x >>= \y -> k2 y なことに注意!
rightM = do
x <- m
y <- k1 x
k2 y

これは,さっきのグループ分けの図で言えば,次が成り立つって言ってるのと同じだ.

Associativity of Monad Law

この法則さえ成り立てば,実はさっきの add2ToXDiv2OfX はどんなグループ分けをしても成り立つことが証明できる.要はグループ分けされたそれぞれのグループも,やっぱり2つのグループをくっつけてできたもののはずで,そうすると上の法則を使ってグループの範囲が変えられるってわけだ.そうしたら上でも下でもいいけど,最小単位までどちらかを分解していってグループの範囲を変えていったらどちらも同じグループ分けになる.多分パズル感覚でできるから,一度グループ分けを2種類作ってみて,本当にどちらも同じグループ分けにできるか試してみると面白いかもしれない.

もう一つ, unit つまり return は単位的計算値を作るという話を覚えているかい? 実は Monad 則のフルバージョンには,どういうものが単位的計算なのかも含めて,以下の2つの規則が追加であるんだ.

> return x >>= k = k x

> m >>= return = m

これももちろん,どんな xkm についても成り立たなければいけない.でもこれは言ってることは単純で,左に return で作った計算値があってそれを継続とつなげる時に何かしら影響を受けてはダメ,右に return があってそれをくっつけた場合も同じく何かしら影響を受けてはダメと言ってるだけだ.これが守られないと,例外を投げる計算で話したように, return によって突然例外が発生したり状態が変わったりといったことが起こってしまうかもしれない.でも,それは多分誰も望んでいないことだ.この法則はそういうことが無いということをうまく表している.空文字列も左から繋げても右から繋げても特に何も影響を与えないだろ? この法則はそれとだいたい同じさ.

さて,これらの法則を今までの Monad がちゃんと満たしているかどうか,確かめてみたい人もいるだろう.また,どういうものはこの法則を満たして,どういうものはこの法則を満たさないのか試してみたい人もいるかもしれない.次節で,今度は私たちだけの Monad を作るやり方を教えるつもりだけど,そういう人のために休憩時間を設けよう.一旦ここまでの話を整理して,いまいち分からないところがあったら,色々調べ物をしてみることをオススメするよ.特に何か頭がボーとして,今はとにかくコードを見たく無いと思っていたら,1日寝てそれからまた読み進めるのがいいだろう.準備万端になったと思ったら次の節に進んで欲しい.


Haskellに新たなMonadを増やしてみる

今まで紹介した計算体系は,残念ながら既にHaskellにあるものばかりだった! どこかに,Haskellに追加されていない私たちだけの Monad はないだろうか? 何か,私たちだけの handleKunit は定義できないだろうか?

悲しいことに,そのような楽しいことは1990年代辺りに先人たちが済ませてしまって,私たちにはほとんど何も残してくれなかった12.ただそれでも,画期的でエレガントであることを諦めるなら,私たちだけの Monad を作ることはできる.それは,言語内DSLに対する Monad だ.

まず,DSLで使える命令を考えてみよう.例えば,今まで :clap: した回数を入手できる howManyClaps と指定した回数 :clap: を行う clapMyHand という命令を持つDSLを考えてみる.ここから Monad のインスタンスになるようなDSLのためのデータ型を作りたかったら,以下の手順に従うといい.



  1. まず,DSLの型名と命令の型を考える.

    data ClapDSL a

    howManyClaps :: ClapDSL Int

    clapMyHand :: Int -> ClapDSL ()




  2. DSLの命令に対応するコンストラクタを,命令の引数と (返り値の型 -> DSLの型) をパラメータに持つように追加する.

    data ClapDSL a
    
    = HowManyClaps (Int -> ClapDSL a)
    | ClapMyHand Int (() -> ClapDSL a)



  3. return に対応するコンストラクタを次のように追加する.

    data ClapDSL a
    
    = ...
    | ClapDSLReturn a



  4. DSLの命令を,引数は対応するパラメータに入れ,最後に return に対応するコンストラクタを渡す.

    howManyClaps :: ClapDSL Int
    
    howManyClaps = HowManyClaps ClapDSLReturn

    clapMyHand :: Int -> ClapDSL ()
    clapMyHand n = ClapMyHand n ClapDSLReturn



この不思議なレシピに従うと,なんと Monad が何も考えなくても実装できる.ただ, Monad を実装するにはもう少し手順が必要だ.実は,今まで私が Monad を実装できると言って示してきたコードは,それだけではHaskellでは動かない.理解を優先して, Monad を実装するために大事な手順を幾つかすとっばしてきたんだ.何をすっ飛ばしてきたかというと,実は Monad のインスタンスにするためには, FunctorApplicative という2つの不思議な型クラスのインスタンスも定義する必要があるんだ.それぞれは Monad の親クラスになっている.でも,心配しないでほしい.この2つのインスタンスはすぐに実装できる.

instance Functor ClapDSL where

fmap f = \m -> do
x <- m
return (f x)

instance Applicative ClapDSL where
pure = return
mf <*> mx = do
f <- mf
x <- mx
return (f x)

このインスタンスは, Monad のインスタンスを定義して初めて,効力を発揮するけど,どんな Monad もこれだけ書けば FunctorApplicative のインスタンスを定義したことになる.今までのだってね.この2つの型クラスがどういうものかは,他の解説書に譲ろう.とにかくいっぱいあるし,自分の気に入ったものを見てみるといいと思うよ.

さて,いよいよ Monad の実装だ.ここまで長かったね. Monad を実装するには, return は対応するコンストラクタで実装して, >>= は以下のように実装する.命令に対応するコンストラクタは保持している関数を実行した後,それを継続とつなげていくんだ. return に対応するコンストラクタに関しては,保持している内容をそのまま継続に渡せばいい.

instance Monad ClapDSL where

return x = ClapDSLReturn x

HowManyClaps k1 >>= k2 = HowManyClaps (\x -> k1 x >>= k2)
ClapMyHand n k1 >>= k2 = ClapMyHand n (\x -> k1 x >>= k2)
ClapDSLReturn x >>= k = k x

この手順に従えば Monad を定義できる.狐につままれた気分かい? でも中にはなぜ Monad がこれで定義できるか,答えを掴みかけている人もいるかもしれない.ポイントは,それぞれの命令に対応するコンストラクタが,最後に保持している (返り値の型 -> DSLの型) という型の値だ.これは,実はDSLの命令を実行し終わった後に実行するべき継続を入れる,格納スペースなんだ.継続は最初 return という特に何も行わないものから始まる.これが徐々に >>= で継続をくっつけられるごとに大きくなっていく.そして,最終的なDSLによるプログラムが完成したころには,それ以降に実行するべき計算の続きが全部入ってることになる.今までのはこの継続を入れるためのスペースを,うまく扱うための下準備だったわけさ.それに継続をつなぐ作業は >>= で計算値と継続をくっつける操作にそのまま対応している.イメージとしては,一つ一つ命令を紐解いて,最初の継続が return まで行きついたら次の継続に移るみたいな感じさ.ところでこの定義は, Monad 則もちゃんと満たせるようになってるんだ.これは格納した継続をどんどん紐解いていくと,最終的に return を継続として持つ命令列が >>= でくっつけられたものと同じになることが示せるからさ13.さて,これを実際どう使うかっていうと,次のように使う.まず,このDSLの実行関数を定義する.

runClapDSL :: ClapDSL a -> (Int, a)

runClapDSL = go 0 where
go n dsl = case dsl of
HowManyClaps k -> go n (k n)
ClapMyHand m k -> go (n + m) (k ())
ClapDSLReturn x -> (n, x)

さっき,各命令に対応するコンストラクタには,以降の計算の続きが全部入ってるという話をしたね.この関数は,その命令1つ1つを紐解いて引数となるパラメータを受け取って処理を行った後結果をその継続に渡すんだ.そうすると,継続はまたDSLの命令を計算して返してくる.こういう風にして簡単に自分だけのDSLを作れるんだ.後は,実際にDSLでプログラムを作って実行関数にかけるだけだ.

>>> :{

| sampleProg = do
| clapMyHand 5
| n <- howManyClaps
| clapMyHand n
| n <- howManyClaps
| return (n `mod` 2 == 0)
| :}
>>> runClapDSL sampleProg
(10,True)

そう,DSLでプログラムを書くにはdo記法が使える.私たちは独自の言語を Monad にするだけで,あたかも言語内でサポートされているかのように,自然にプログラムを書くことができるんだ.これが多くのHaskellerが憑りつかれている Monad の魅力ってわけだよ.


さいごに

どうだろう? このチュートリアルが,少しでも役に立ったのなら嬉しい. Monad っていうのは,そこまで難しい概念でもないし,特別な技能を持ってる人だけが提案できるものでもない.単なるデザインパターンみたいなもので,簡単に作れるものだということが分かってもらえたら嬉しい.

ところで, Monad のこととなると多くの人が「なぜこれでうまくいくのか」という問いを考える傾向にあるんだけど(おそらくこの概念が数学発祥というところが強く影響しているのだろうね), Monad 自体そこまで高尚なことが考えられた結果うまくいったかというと,それは怪しいんじゃないかな.オブジェクト指向のクラスやダックタイピングと同じで,私は,結果的に皆に受け入れられるプログラミングスタイルと相性が良かったからという,元も子もない回答が正解だと思うんだ.なので私から言える Monad と付き合う上でのワンポイントアドバイスは,その疑問を考えることは重要なことなんだけど,その疑問に囚われ過ぎるのもよくないということだ.別に,「なぜこれでうまくいくのか」に答えられなくても, Monad を使ったプログラミングはできるからね14

そうだ,忘れるところだった.最後にこの言葉を送ろう.


継続は、人生の大切なすべての事がつまってるんだよ[要出典]


実はこの言葉が使いたくて,この記事を書いたと言っても過言ではない.

では,メリークリスマス & よいお年を!





  1. 答えが存在しないことは,背理法を使うことで簡単に証明できる.もしこの答えが存在するとしよう.その整数を $i$ とおく.もし, $i \ge 0$ なら $i \times i \ge 0$ で,これは $i^2 = -1$ を満たさない.$i < 0$ なら $i = -j$ を満たす整数 $j > 0$ が存在する.この時, $i \times i = -j \times -j = j \times j > 0$ で,やっぱり $i^2 = -1$を満たさない.よって,答えとなる整数は存在しない.最も多くの人は,複素数の概念を知ってると思うので,ここまでして証明することではないと思うけど. 



  2. ここでリストを選択することは正しいだろうか? 実際には,これでは表せない場合もある.例えば, $0$ 以上 $1$ 以下の実数を求めよという問題の答えを,リストで表すのは難しい.代替表現として,述語( p :: a -> Bool )を使うという方法があるが,これはプログラミングと少し相性が悪い.例えば,答えに関数 $f$ を適用したものを求めよという問題が出た時,この表現では答えを出すのが困難なことに気づくだろう.一般に,プログラミングでは集合のような概念をうまく表現することが難しい.ここでは,ひとまず完全な表現は諦めて,多くの場合はうまくいくリストを採用する. 



  3. 実のところなぜ c を使わず, k を使うのか私にも分からないのだが,プログラミング業界では継続を表すのに k を使う慣習がある. c だとconstant(定数)やcapability(容量)などと混じってややこしいからとかかな? それに日本語のKeizokuのkとも被ってて一石二鳥(?)だしね. 



  4. 現実にこんな記法のプログラミング言語は無いと思うけど,ここでは概要が掴めたらいい. global.XX という名前の再代入可能なグローバル変数でその値にアクセスするのに ref という操作が必要だ.そして,値 v を再代入するには global.X := v という記法を使う.そういう感じの擬似的なプログラムと思ってほしい. 



  5. みなさんが知らなくても無理もない話だ.このコンピュータは,イギリスの脚本家ダグラス・アダムスの「銀河ヒッチハイク・ガイド」という作品に登場する架空のコンピュータだからだ.もちろん,ランクウィルとフックも架空の人物だ. 



  6. 実は私は話をややこしくしないために,実際の定義とは異なる定義を出している.実際の定義については,次の章で説明しよう.もし,とても気になるならフライングしてもいいかもしれない. 



  7. この原因は簡単に言ってしまえば,インスタンス解決ができない,または非常に時間がかかる場合が出てきてしまうからだ.専門的には,決定不能(undecidable)という. 



  8. newtype の解説については,「すごいHaskellたのしく学ぼう!」という本の,11章でも触れられている.ただ少し分かりにくい概念ではあるけどね.幾つか解説を当たってみて分からなかったら,周辺にいるHaskellerを捕まえて聞いてみるのがいいかもしれないね. 



  9. 実際には,この定義は Monad Transformer というものとして定義されてしまっている.本質としては, newtype State s a = State { runState :: s -> (s, a) } という感じの定義に Monad Transformer という設計パターンを適用しただけだ. Monad Transformer については, Maybe と IO を一緒に使いたくなったら という記事が入門用としておすすめだ.もし,興味があれば読んでみるといいかもしれない. 



  10. どういう用途で使うのかについては,Stack OverflowのWhy use Proxy?という質問に対する答えを見てみるのがいいかもしれない.ただその前に基礎知識として,幽霊型という概念を紹介している記事を読んでみてからの方がいいかもしれない.幽霊型について知ることは,Proxy型への良い導入になってると思う. 



  11. より正確には,do記法は他に let 文というものが使えるようになっている.実は複雑な handleK パターンでは let-in 構文も合わせて使われることが多かった.ただ,do記法では let-in を使うとネストが深くなってしまう.この改善策として let 文は導入された.詳しいdo記法の仕様については,Haskellの入門書ならどれでも書いてあるだろうからそちらを当たって欲しい.do記法については,「すごいHaskellたのしく学ぼう!」という本の,12章でも触れられている. 



  12. もちろん,まだ誰も思いついていない面白い Monad が残っているかもしれない.そして,それを読者の誰かが見つけてしまうかもしれない.一度挑戦してみてもいいだろう. 



  13. ほんとに? と思った人はぜひ確かめてみると面白いと思うよ.その作業は多分,この Monad の作り方を理解するうえでも参考になるはずさ. 



  14. 実際,多分私はその答えにちゃんと回答できないし,ちゃんと答えられるHaskellerにあまり心当たりもない.もし,満足のいく回答が得られたら,こっそり私にも教えて欲しい :yum: