LWP | 関数型プログラミングからモナドを理解する
LWP TECHNICAL ARTICLE | 172
関数型プログラミングからモナドを理解する
Maybe、List、State、IOに共通する「計算のつなぎ方」を見つける
Copyright © 2026 LWP 山中 一弘
本資料は、出典を明記いただければ、商用・非商用を問わず、ご自由に複製・改変・再配布していただけます。なお、著作権表示は改変せず、そのまま記載してご利用くださいますようお願いいたします。
記事要約
モナドは、ある種類の計算で繰り返し現れる『次の計算へのつなぎ方』を、個々の処理本体から分離して再利用する仕組みです。Maybeなら失敗時に後続を止め、Listなら各候補へ後続計算を適用し、Stateなら状態を受け渡します。
本記事では命令型と関数型の違いから出発し、Maybeの手書き処理をbindへ一般化します。最後にm a、a -> m b、pure、Monad則を確認し、『箱』『エラー処理』『副作用』だけでは足りない理由を整理します。
本記事の対象とゴール
想定読者
他言語の経験があり、Haskellは初めての方
モナドを比喩ではなく型と処理から理解したい方
Promiseや例外処理との類似と違いを整理したい方
本記事で得られること
bindが吸収する反復処理を説明できます。
Maybe、List、State、IOのつなぎ方を比較できます。
モナド則が合成の安定性に必要な理由を説明できます。
目次
Haskellを読むための最低限の記法
関数型言語を考える出発点
モナドを使わない場合から考える
Maybeモナド
Listモナド
Stateモナド
IOモナド
一般形 m a
Monad則
モナドではないもの
要点
Haskellを読むための最低限の記法
本記事ではHaskellのコードを使いますが、Haskellそのものの習得は目的ではありません。最初は次の記法だけ分かれば十分です。
| 記法 | 意味 |
|---|---|
| :: | 「~の型は」 |
| -> | 関数の入力から出力 |
| = | 名前・関数の定義 |
| Just x | Maybe で値が存在する |
| Nothing | Maybe で値が存在しない |
| \\x -> ... | ラムダ式 |
| >>= | bind。モナド計算を次の関数へつなぐ |
| do | モナド計算を手続き型風に記述する構文 |
| <- | モナド計算から得た値に名前を付ける |
| _ | 値は受け取るが使用しない |
| () | Unit型。意味のある戻り値がない場合などに使う |
| div x y | 整数除算を行う関数呼び出し |
Haskellでは関数呼び出しに通常は括弧を書きません。
double x = x * 2
は、C系の感覚なら概ね次です。
double(x) {
return x * 2;
}
呼び出しは、
double 10
です。
型宣言は、
double :: Int -> Int
と書きます。
これは、
Intを受け取り、Intを返す関数
という意味です。
Maybe を読むための最低限の知識
Maybe は「値がある場合」と「値がない場合」を一つの型で表します。
概念的には次の定義です。
data Maybe a
= Nothing
| Just a
ここで区別しておくべきものは次の4つです。
Maybe 型コンストラクタ
Maybe Int 具体的な型
Just 5 Maybe Int型の具体的な値
Nothing Maybe a型として使える値
したがって、
Maybe Int
は「整数があるかもしれないし、ないかもしれない型」です。
値がある場合:
Just 5
値がない場合:
Nothing
となります。
Maybe Int 自体がモナドなのではなく、一般化された型コンストラクタ Maybe に対して、モナドとしての合成規則が定義されます。
関数型言語を考える出発点
命令型では、状態を変更しながら手続きを進めるのが自然です。
x = 10
x = x + 1
x = x * 2
一方、関数型では値から値への変換として考えます。
10
↓ add1
11
↓ double
22
Excelでいえば、セルを For で順番に書き換えるのは命令型的です。
一方、
FILTER
MAP
REDUCE
GROUPBY
などで入力配列から新しい配列を得るのは関数型的です。
Power Queryも、
Source
↓
Filter
↓
Merge
↓
Group
↓
Output
というデータ変換の連鎖として見ることができます。
CPUは状態機械なのに、なぜ関数型が成立するのか
実際のCPUはレジスタやメモリを書き換えながら動きます。
したがって、
状態を変更しない関数型
は実機と相性が悪く見えます。
しかし、ここでは次を分ける必要があります。
ソースコード上の意味
≠
物理的な実行方法
関数型のソースコードで、
x1 = x + 1
x2 = x1 * 2
x3 = x2 - 5
と書かれていても、コンパイラが同じレジスタを使い回して実行して構いません。
重要なのは、
プログラマから見た意味として、以前の値が後から変更されない
ことです。
不変データ構造についても、毎回全コピーする必要はありません。変更されていない部分を共有する構造共有が使えます。
したがって、関数型の純粋性は実機の物理的な動作を縛るものではなく、プログラムの意味を縛るものです。
「関数型だから並列化できる」は本質ではない
関数型であっても依存関係があれば並列化できません。
A → B → C
は逐次です。
一方、
A
/ \\
B C
\\ /
D
ならBとCは並列化できます。
これは命令型でも同じです。
命令型との差は、
独立性を証明する難しさ
にあります。
命令型では、関数の内部でグローバル変数を書き換えているかもしれません。ポインタが同じ領域を指しているかもしれません。ファイル、ネットワーク、例外、ロックなどの副作用があるかもしれません。
純粋関数なら、副作用がないことが言語上保証されます。
したがって正確には、
関数型だから新しい並列計算が可能になるのではなく、安全に変形・並列化できることを証明しやすくなる。
ということです。
命令型と関数型で「計算できるもの」は基本的に同じ
チューリング完全な命令型言語と関数型言語の間に、
こちらにしか計算できない
という意味での本質的な差はありません。
違うのは、
何をプログラマに直接書かせるか
です。
命令型は、
状態変更
逐次実行
if
break
continue
return
を直接書きやすい。
関数型は、その自由を制限する代わりに、
参照透過性
局所的推論
安全な関数合成
プログラム変換のしやすさ
を得ます。
純粋関数型の実用上の弱点
人間は、
まずAをする
次にBをする
条件Cなら止める
という時間順の手順を自然に理解します。
例えば、
for
for
if 条件
break
は非常に直感的です。
一方、純粋関数型では、
集合を作る
条件で絞る
変換する
畳み込む
などへ問題を再構成する必要があります。
データ変換中心の処理では関数型が自然ですが、状態機械、探索、複雑な途中終了などでは命令型の方が自然な場合があります。
現在の主流言語が、
for
while
if
break
return
を残しつつ、
lambda
map
filter
reduce
immutable
pattern matching
などを取り込む方向へ進んだのは、この事情と整合します。
つまり実用上は、
命令型を骨格にし、局所的なデータ変換や合成に関数型を使う
というハイブリッドが強い、という見方ができます。
モナドを「値+文脈」と説明すると分かりにくい理由
モナドはよく、
値 + 文脈
と説明されます。
例えば、
Maybe Int
=
整数
+
値がないかもしれないという文脈
という説明です。
IOなら、
値
+
IOを伴う計算という文脈
とされます。
この説明は入口としては使えますが、これだけでは、
なぜbindが必要なのか
なぜMaybeとListとIOが同じMonadなのか
が見えません。
そこで、値ではなく「計算のつなぎ方」に注目します。
モナドを使わない場合から考える
これが最も重要な入口です。
失敗するかもしれない処理を3つつなぐとします。
命令型なら概念的に、
r1 = f(x)
if r1 == failure
return failure
r2 = g(r1)
if r2 == failure
return failure
r3 = h(r2)
if r3 == failure
return failure
return r3
となります。
本来やりたい処理は、
f
↓
g
↓
h
だけです。
しかし毎回、
失敗したか確認
↓
失敗なら終了
↓
成功なら値を取り出して次へ
という同じ制御処理を書いています。
この繰り返し部分を、本来の関数から外へ抜き出すことを考えます。
ここがモナド理解の出発点です。
Maybeモナド
まず失敗可能な関数を作ります。
safeHalf :: Int -> Maybe Int
safeHalf x =
if even x
then Just (x `div` 2)
else Nothing
読み方は、
safeHalfはIntを受け取る
偶数なら
半分の値を Just で返す
奇数なら
Nothing を返す
です。
例えば、
safeHalf 20
→ Just 10
safeHalf 9
→ Nothing
となります。
Maybeのbind
Maybeに対するbindの本質は次の2行です。
Nothing >>= _ = Nothing
Just x >>= f = f x
1行目:
Nothing >>= _ = Nothing
は、
前の計算がNothingなら
次の関数が何であっても実行せず
Nothingを返す
という規則です。
2行目:
Just x >>= f = f x
は、
前の計算がJust xなら
中のxを取り出して
次の関数fへ渡す
という規則です。
これによって、
Just 20
>>= safeHalf
>>= safeHalf
と書けます。
評価は、
Just 20
↓
safeHalf 20
↓
Just 10
↓
safeHalf 10
↓
Just 5
です。
一方、
Just 18
>>= safeHalf
>>= safeHalf
なら、
Just 18
↓
safeHalf 18
↓
Just 9
↓
safeHalf 9
↓
Nothing
となります。
一度 Nothing になった後は、Maybeのbind規則によって後続処理が実行されません。
Maybeで何が抽象化されたのか
モナドを使わない場合に毎回書いていた、
失敗なら終了
成功なら値を次へ渡す
を、次の2行へ切り出しました。
Nothing >>= _ = Nothing
Just x >>= f = f x
本体には、
safeHalf
↓
safeHalf
だけが残ります。
したがって、Maybeモナドについては、
「失敗なら停止、成功なら次へ」という反復する制御をbindへ抽出したもの
と理解できます。
bindの型
Maybeに限定すると、bindは次の形です。
Maybe A
+
(A -> Maybe B)
----------------
Maybe B
Haskell風に書けば、
(>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
です。
左側は Maybe a。
右側は、
aを受け取り
次のMaybe bを返す関数
です。
bindが両者をつなぎ、最終的に Maybe b を作ります。
Listモナド
Maybeだけを見ると、
モナド = エラー処理
と誤解しやすいため、Listを見ることが重要です。
次のコードを考えます。
[1, 2] >>= \x -> [x, x * 10]
結果は、
[1, 10, 2, 20]
です。
計算イメージは、
1
→ [1, 10]
2
→ [2, 20]
最後に結合
→ [1, 10, 2, 20]
です。
Listのbindは概念的には、
xs >>= f = concatMap f xs
と考えられます。
つまり、
各要素について次の計算を行い
複数の結果を得て
最後に一つのListへ平坦化する
という合成規則です。
Maybeとは全く違う動作ですが、
m a
+
(a -> m b)
↓
m b
という構造は同じです。
ここで初めて、
モナドの共通点は「失敗」ではなく、「その計算世界で次へどうつなぐか」
であることが見えます。
Stateモナド
状態を純粋関数だけで扱うこと自体は可能です。
例えば概念的に、
S0
↓ f
(value1, S1)
↓ g
(value2, S2)
と、状態を毎回引数と戻り値で渡せばよいからです。
Stateは概念的には、
State s a
≈
s -> (a, s)
と考えられます。
つまり、
現在状態sを受け取り
値aと新しい状態sを返す計算
です。
モナドを使わなければ、各処理のたびに、
前の状態を取り出す
↓
次の関数へ渡す
↓
新状態を受け取る
必要があります。
Stateのbindは、この状態受け渡しを自動化します。
したがってStateでは、
「前の計算が返した状態を次の計算へ渡す」
ことが、その世界の合成規則になります。
IOモナド
Haskellでは、
getLine :: IO String
です。
これは単なる String ではありません。
Stringを結果として得るIOアクション
です。
一方、
putStrLn :: String -> IO ()
は、
Stringを受け取り
出力を行うIOアクションを返す関数
です。
したがって、
getLine >>= putStrLn
と接続できます。
型としては、
IO String
+
(String -> IO ())
-----------------
IO ()
です。
意味は、
getLineというIOアクション
↓
結果のString
↓
そのStringをputStrLnへ渡す
↓
次のIOアクション
です。
重要なのは、>>= が単独で「今すぐIOを起こす命令」だと考えないことです。
概念的には、
前のIOアクションの結果を次のIOアクションへ渡す、より大きなIOアクションを構成する
と考える方が正確です。
Haskellのプログラムでは、そのように構成された main :: IO () が実行対象になります。
do 記法
IOなどでは、bindをそのまま書くと読みにくくなります。
getLine >>= \s -> putStrLn s
Haskellではこれを、
main = do
s <- getLine
putStrLn s
と書けます。
<- は可変変数への代入ではありません。
getLineというモナド計算から得られた値に
sという名前を付けて
後続計算で利用する
という意味です。
見た目は手続き型ですが、背後ではbindによる計算合成になっています。
Promiseとの類似
JavaScriptのPromiseでは、
非同期処理
↓
完了したら次
↓
失敗なら失敗経路
という反復する制御を .then()、.catch()、await などが担当します。
そのためPromiseはモナドを理解するための良い類比になります。
ただし、
Promise = HaskellのMonadそのもの
と単純に同一視する必要はありません。
重要なのは、
本来の計算とは別に繰り返し現れる「次へどう進むか」を、別の合成規則へ切り出している
という共通した発想です。
オブジェクト指向との違い
オブジェクト指向は概念的に、
データ
+
そのデータに対する操作
をまとめます。
モナドは、
ある種類の計算
+
その計算を次へつなぐ規則
を扱います。
したがって、単なる型の継承やオブジェクトの拡張とは異なります。
Maybe、List、IO などの型によって、
今どの種類の計算を扱っているか
を型として表し、
>>= によって、
その種類の計算を次へどうつなぐか
を定義します。
AOP・例外処理との類似と違い
モナドにはAOPや例外処理と似た側面があります。
例えばMaybeでは、
失敗確認
失敗時の中断
という、本筋とは別の処理を本体から外へ移します。
これは、
ログ
トランザクション
例外処理
のような横断的関心事を本体から分離するAOPと発想上の共通点があります。
ただし仕組みは異なります。
AOPは一般に、特定のポイントへ横から処理を差し込みます。
モナドは、
m a
という型と、
>=
という明示的な合成によって、計算のつなぎ方を構造化します。
したがって、
似ている点
本筋以外の反復処理を分離する
違う点
モナドは型と合成規則として明示的に構成する
と整理できます。
ラムダ計算にifやループがないからモナドが必要なのか
これは違います。
純粋ラムダ計算でも条件分岐は表現できます。
Church Booleanでは、
TRUE = λx.λy.x
FALSE = λx.λy.y
と定義でき、
IF = λb.λx.λy. b x y
とすれば、
IF TRUE A B → A
IF FALSE A B → B
となります。
再帰も固定点コンビネータなどで表現できます。
したがってモナドは、
ラムダ計算にifがないから追加する仕掛け
ではありません。
モナドは計算能力を増やさない
Maybeモナドがなくても、
失敗したか?
を毎回書けば失敗処理はできます。
Stateモナドがなくても、状態を引数と戻り値で明示的に渡せます。
Listモナドがなくても、ループして結果を集めて平坦化できます。
純粋性を捨てれば、IOを直接実行する言語も作れます。
したがって、
モナドは「できなかった計算を可能にする仕組み」ではない。
モナドが行うのは、
繰り返し現れる計算制御・受け渡し・分岐・順序づけを抽象化して、計算を合成可能にすること
です。
一般形 m a
Maybeだけを見ると、
Maybe a
+
(a -> Maybe b)
↓
Maybe b
でした。
Listなら、
List a
+
(a -> List b)
↓
List b
です。
IOなら、
IO a
+
(a -> IO b)
↓
IO b
です。
これらを一般化すると、
m a
+
(a -> m b)
↓
m b
になります。
Haskellの型として書けば、
(>>=) :: m a -> (a -> m b) -> m b
です。
ここで、
m
は、
Maybe
List
IO
State ...
のような「計算の種類」を表します。
a と b は、その計算で扱う通常の値の型です。
この一般形が見えると、
Maybe
List
State
IO
が同じMonadという枠で扱われる理由が見えてきます。
pure
モナド的な計算世界へ、通常の値を入れる操作も必要です。
概念的には、
pure :: a -> m a
です。
例えばMaybeなら、
pure 5
→ Just 5
Listなら、
pure 5
→ [5]
と考えられます。
IOなら、
値5を結果として返すが
外部作用そのものは追加しないIOアクション
を作ると考えられます。
pure は、通常の値をその計算世界の値へ持ち上げる操作です。
Monad則
>>= と pure が存在するだけでは、好き勝手な合成規則をMonadと呼んでよいわけではありません。
少なくとも、合成が一貫して振る舞うためのMonad則を満たす必要があります。
左単位元
pure a >>= f
==
f a
通常の値 a をいったんモナドへ入れてから f へ渡しても、直接 f a とするのと同じです。
右単位元
m >>= pure
==
m
計算結果をそのままモナドへ戻すだけなら、元の計算と同じです。
結合則
(m >>= f) >>= g
==
m >>= (\x -> f x >>= g)
計算をどこで括るかによって意味が変わってはいけません。
この3つによって、モナド的な計算を長く連結しても合成規則が破綻しないことが保証されます。
「値+文脈」「計算世界」「計算合成規則」の関係
モナドには複数の説明方法があります。
値+文脈
Maybe Int
=
Int
+
値がないかもしれない
という見方です。
型を見るときには便利ですが、bindの意味が見えにくいという弱点があります。
小さな計算世界
Maybe世界
Nothingなら停止
Justなら次へ
List世界
全候補へ分岐して結果を結合
State世界
状態を受け渡す
IO世界
IOアクションを順序づける
という見方です。
意味論を理解するには有効ですが、実際に別の仮想CPUが動いているという意味ではありません。
計算合成規則
最もプログラマ向けに本質を捉えやすいのは、
通常の関数合成では扱いにくい計算について、「次へどう進むか」を共通規則として抽出する
という見方です。
この規則を実装する中心がbindです。
プログラマ向けの最終定義
実務的な理解としては、次の定義が最も使いやすいです。
モナドとは、ある種類の計算で繰り返し現れる「計算のつなぎ方」を本来の処理から分離し、そのつなぎ方を共通規則として再利用できるようにする仕組みである。
bindについては、
bindとは、その計算固有の規則を適用しながら、前の計算結果を次の計算へつなぐ演算である。
と考えます。
代表例を並べると次のようになります。
| 種類 | 本体から切り離される反復処理 | bindが担当すること |
|---|---|---|
| Maybe | 失敗確認 | Nothingなら停止、Justなら次へ |
| List | 分岐・結果結合 | 各候補へ適用し、結果を平坦化 |
| State | 状態受け渡し | 新状態を次の計算へ渡す |
| IO | IOの順序づけ | 前のIO結果を次のIOアクションへ渡す |
モナドではないもの
理解を固定するために、モナドではないものも明確にしておきます。
モナドは単なる箱ではない
Maybe a や IO a を「値を包む箱」と見るだけでは、なぜbindが必要なのか説明できません。
モナドは単なるエラー処理ではない
Maybeは失敗処理に見えますが、ListやStateやIOも同じMonad構造を持ちます。
モナドは遅延評価そのものではない
IOや非同期処理を見ていると遅延実行と混同しやすいですが、Monad一般が遅延評価を意味するわけではありません。
モナドは計算能力を追加しない
チューリング完全性を増やす仕組みではありません。
モナドは物理的に実行環境を分裂させる機構ではない
「小さな計算世界」は理解のためのモデルです。
モナドはオブジェクト指向の継承ではない
データ構造そのものを拡張するというより、計算を合成する方法を型と演算で表します。
Haskellの記法をもう一度まとめる
関数定義
double x = x * 2
型宣言
double :: Int -> Int
Maybeを返す関数
safeHalf :: Int -> Maybe Int
パターンマッチ
showMaybe Nothing = "値なし"
showMaybe (Just x) = "値あり"
ラムダ式
\x -> x * 2
JavaScriptなら、
x => x * 2
に相当します。
bind
ma >>= f
do記法
main = do
s <- getLine
putStrLn s
概念的には、
getLine >>= \s -> putStrLn s
です。
理解の確認
ここまで読んだ後、次の説明が理解できれば、モナドの基本概念は掴めています。
Maybe
Maybe a
は、値がある場合とない場合を表せる。
bindは、
Nothingなら後続を実行しない
Just xならxを次の関数へ渡す
という計算合成規則。
List
bindは、
各候補について次の計算を実行
↓
得られた複数結果を結合
という規則。
State
bindは、
前の計算が返した新状態
↓
次の計算へ渡す
という規則。
IO
bindは、
前のIOアクション
↓
結果
↓
次のIOアクションへ渡す
という規則。
一般化
全部、
m a
+
(a -> m b)
↓
m b
として表せる。
これが、
(>>=) :: m a -> (a -> m b) -> m b
です。
さらに先で検討する論点
ここから先は、モナドの基本理解とは分けて考えた方がよい論点です。
FunctorとMonadの違い
ApplicativeとMonadの違い
fmap、<*>、>>= の能力差
Monad Transformer
MaybeT、StateT などによる効果の合成
Effect System
Algebraic Effects / Effect Handlers
Rustの Result と ?
JavaScript / C# / Pythonなどの async/await
「モナドを知らなくてもモナド的な恩恵を使える」言語設計
純粋関数型を表面構文にどこまで見せるべきか
AI支援下での言語設計と、人間がデバッグできる構文の重要性
要点
関数型と命令型で、計算可能性そのものに大きな差があるわけではない。
関数型は状態変更や副作用を制限し、計算の意味を解析・変形しやすくする。
純粋関数型では、失敗・状態・IOなどの受け渡しを明示的に書くと冗長になる。
モナドを使わなくても同じ計算は書ける。
モナドは、そのとき毎回現れる「面倒な計算のつなぎ方」を抽出する。
bindは、その計算固有の規則を適用しながら次の計算へつなぐ。
Maybe、List、State、IOは、つなぎ方は違うが同じ一般形を持つ。
一般形は m a -> (a -> m b) -> m b。
pure とMonad則によって、合成が一貫して振る舞う。
モナドの本質は「箱」よりも「計算の合成規則」と考える方が、プログラマには理解しやすい。
出典メモ
Haskell Wiki『Typeclassopedia』 https://www.haskell.org/wikiupload/e/e9/Typeclassopedia.pdf
Haskell 2010 Language Report https://www.haskell.org/onlinereport/haskell2010/
