Posted at

for文がわかるプログラマのためのモナド最速入門


はじめに

近頃の for 文は、コレクションの要素を直接参照できたり、 yield で評価結果を返せたり、多重ループを一気に回せたり、便利なものが多いですね。たとえば Scala であれば、こんなふうに書けます。

val list = for {

a <- List(100)
b <- List(10, 20)
c <- List(1, 2, 3)
} yield a + b + c

print(list) // List(111, 112, 113, 121, 122, 123)

この for 文のみを頼りに、ときおり関数型プログラマが唱える「モナド」に入門してみよー、というのが本記事の趣旨です。

なるべく簡単なコードを挙げながら、できるだけ「圏論」の各種定義やその気持ちまで掘っていくので、前提知識なしで読みすすめられると思います。


Scala の for 文

まずは Scala 使いでない方向けに先程の for 文を説明すると、次のコードと意味はざっくり同じです。

val buffer = ListBuffer[Int]()

for (a <- List(100)) {
for (b <- List(10, 20)) {
for (c <- List(1, 2, 3)) {
buffer.addOne(a + b + c)
}
}
}

val list = buffer.toList
print(list) // List(111, 112, 113, 121, 122, 123)

関数型な方には、次のコードの省略記法というとピンとくるかもしれませんね。

val list = List(100).flatMap(a => List(10, 20).flatMap(b => List(1, 2, 3).map(c => a + b + c)))

print(list) // List(111, 112, 113, 121, 122, 123)


for 文による関数合成

ここから for 文による関数の合成を考えていきます。準備として、関数を三つ用意します。

val f: String => List[Char]    = ???

val g: Char => List[Int] = ???
val h: Int => List[Boolean] = ???

fString を受けとって List[Char] を返す関数です。

実装は ??? となっている通り、今回の話では型が合えば何でもいいのですが、たとえば次のようなものを用意しておきます。

// ex. "abc"  => List('a', 'b', 'c')

val f: String => List[Char] = _.toList

// ex. 'a' => List(9, 7)
val g: Char => List[Int] = _.toInt.toString.toList.map(_.asDigit)

// ex. 9 => List(true, false, false, true)
val h: Int => List[Boolean] = _.toBinaryString.toList.map(_ == '1')

ここで _.toLista => a.toList の省略記法で、 a => a.toLista を取って a.toList を返す無名関数です。そして map は関数を引数にとって、コレクションの要素一つ一つに適用するメソッドです。

この fg を for 文で合成してみましょう。

// ex.  "abc"  => List(9, 7, 9, 8, 9, 9)

val f_g: String => List[Int] = a =>
for {
b <- f(a)
c <- g(b)
} yield c

なお fg は素直に関数合成できないことに気を留めておいてください。

// f(str) の型は List[Char] だけど g の引数の型は Char なので、コンパイルエラー!

(str: String) => g(f(str)) // g.compose(f) と書いても同じ


結合律と単位律

さて for 文による関数合成には、自然と満たしておいてほしい性質があります。三つの関数を for 文で合成してみましょう。

val f_g_h: String => List[Boolean] = a =>

for {
b <- f(a)
c <- g(b)
d <- h(c)
} yield d

これをあえて二つの合成に分解してみます。

val f_g_h2: String => List[Boolean] = a =>

for {
b <- for {
c <- f(a)
d <- g(c)
} yield d
e <- h(b)
} yield e

val f_g_h3: String => List[Boolean] = a =>

for {
b <- f(a)
c <- for {
d <- g(b)
e <- h(d)
} yield e
} yield c

このように分解方法は二通り考えられますが、 f_g_h2 にせよ f_g_h3 にせよ同じ関数であってほしい気がします。

また、次のようなコードを考えてみましょう。

val f2: String => List[Char] = a =>

for {
b <- f(a)
c <- List(b)
} yield c

ここで c <- List(b)bList に包んですぐさま c に取りだしているため、意味のないことをしているように読めますね。なので、その行を消したとすると a => for {b <- f(a)} yield b となって、これは f(a)b に取りだして yield でまた List に包んでいるため、結局この f2f と同じ関数であってほしいです。同様に、

val f3: String => List[Char] = a =>

for {
b <- List(a)
c <- f(b)
} yield c

この f3f と同じ関数であってほしいですね。

この「あってほしい」自然な性質をもう少し明らかにするため for 文による関数合成そのものを関数化してみましょう。

def [A, B, C](g: B => List[C], f: A => List[B]): A => List[C] = a =>

for {
b <- f(a)
c <- g(b)
} yield c

すると、これまでの話はこう書きかえられます。

// f_g_h2 と f_g_h3 は同じ関数であってほしい

val f_g_h2 = ∙(h, ∙(g, f))
val f_g_h3 = ∙(∙(h, g), f)

// f2 も f3 は f と同じ関数であってほしい
val f2 = ∙(List(_: Char), f)
val f3 = ∙(f, List(_: String))

これは for 文による関数合成が、結合律と単位律を満たすことを表しています。

これをモナド則と呼んだりもします。なぜそう呼ぶのかは、このまま最後まで読み進めると分かるはずです。むむむ。


モノイド

こんなふうに結合律と単位律を満たすものを一般的に「モノイド」と呼びます。用語としては聞きなれないかもしれませんが、例を見ると分かりやすいです。

たとえば自然数における足し算を考えてみましょう。このような等式が自然に成り立ちますね。

\begin{align}

1 + (2 + 3) &= (1 + 2) + 3 \\
0 + 4 &= 4 \\
5 + 0 &= 5
\end{align}

自然数における足し算は、カッコの付け方によらず計算結果は同じ……すなわち結合律を満たし、単位元 $0$ を左右から足しても数字は変わらない……すなわち単位律を満たします。

他にも実数における掛け算を考えてみると、

\begin{align}

1.2 \times (2.3 \times 3.4) &= (1.2 \times 2.3) \times 3.4 \\
1.0 \times 5.6 &= 5.6 \\
-6.7 \times 1.0 &= -6.7
\end{align}

結合律を満たして、単位元 $1.0$ が存在しますね。

このように私たちが慣れ親しんでいる大抵の演算は、モノイドとして捉えることができます。

くわえて逆元が存在すると「群」になります。自然数における足し算は群ですが、実数における掛け算は 0 の逆数が存在しないので群ではないです。 このあたり突っ込んでいくと群論や環論といった代数のお話になっていきます。ふんふん。


圏の定義

そういえばモノイドって言葉の響きはモナドと似ていますね……!

これはもちろん偶然ではないのですが、その二つの関連を知るためには圏論という分野の用語が要ります。それでは「圏」とは何なのか、『圏論の歩き方』 から定義 1.2 を引用してみましょう。


(category)とは,4つ組 $\mathscr{C} = (O, A, \circ, \mathrm{id})$ ,ただし

 


  • $O$ は対象(object)の集まり.

  • 各対象 $X, Y \in O$ に対して $A(X, Y)$ は,対象 $X$ から対象 $Y$ への(morphism)の集まり.すべての
    $A(X, Y)$ をまとめて $A$ と書く.また,要素 $f \in A(X, Y)$ を $f: X \rightarrow Y$ と書く.

  • 各対象 $X, Y, Z \in O$ に対して写像 $\circ_{X, Y, Z} : A(Y, Z) \times A(X, Y) \rightarrow A(X, Z)$ が
    定まっている.これを射の合成(composition)と呼ぶ.すべての $\circ_{X, Y, Z}$ をまとめて $\circ$ と書く.

  • 各対象 $X \in O$ に対して恒等射(identity) $\mathrm{id}_X : X \rightarrow X$ が定まっている.ぜんぶまとめて $\mathrm{id}$ と書く.

であって,次のような条件を満たすものである.

 


  • 結合律(associativity)] 任意の射 $X \xrightarrow{f} Y \xrightarrow{g} Z \xrightarrow{h} W$ に対して, $h \circ (g \circ f) = (h \circ g) \circ f$ .

  • 単位律(unit law)] 任意の射 $X \xrightarrow{f} Y$ に対して, $\mathrm{id}_Y \circ f = f = f \circ \mathrm{id}_X$ .


要するに、「対象」とそれらを矢印で繋ぐ「射」の集まりであって、射がモノイドをなすものを圏と呼びます。だいぶ抽象的な定義ですが、数学の様々な分野でこのような構造が表れることが知られています。へー。

※ 引用はなるべく忠実に努めていますが、書式や図式については整合性や MathJax の制限などにより一部変更しているところがあります。


型を対象とした二つの圏

さてさて話を戻しますと、 for 文による合成によって関数はモノイドをなすのでした。それはつまり関数を射とする圏が定義できるということです!

val f: String => List[Char]    = ???

val g: Char => List[Int] = ???
val h: Int => List[Boolean] = ???

これらの関数 f g h を射として、そして型 String Char Int Boolean を対象とすれば、 for 文による関数合成によって、このような連なりを描くことができますね。

\mathrm{String} \xrightarrow{\mathrm{f: \ String \ \Rightarrow \ List[Char]}} \mathrm{Char} \xrightarrow{\mathrm{g: \ Char \ \Rightarrow \ List[Int]}} \mathrm{Int} \xrightarrow{\mathrm{h: \ Int \ \Rightarrow \ List[Boolean] }} \mathrm{Boolean}

なお結合律によって、次の二つの射は同一のものになります。

\mathrm{String} \xrightarrow{\mathrm{∙(h, \ ∙(g, \ f)): \ String \ \Rightarrow \ List[Boolean]}} \mathrm{Boolean} \\

\mathrm{String} \xrightarrow{\mathrm{∙(∙(h, \ g), \ f): \ String \ \Rightarrow \ List[Boolean]}} \mathrm{Boolean}

このように形作られる圏を、本記事では $\mathscr{S}_\mathrm{List}$ と呼ぶことにします。これで for 文から始めて、圏の具体例まで見ることができました。めでたしめでたし。

……いや、今の連なりはなにか違和感を覚えなかったでしょうか?

素直に考えれば、矢印はこのように描くのが自然ではないでしょうか?

\begin{align}

\mathrm{String} \xrightarrow{\mathrm{f: \ String \ \Rightarrow \ List[Char]}} &\mathrm{\mathrm{List[Char]}} \\
\mathrm{Char} \xrightarrow{\mathrm{g: \ Char \ \Rightarrow \ List[Int]}} &\mathrm{List[Int]} \\
\mathrm{Int} \xrightarrow{\mathrm{h: \ Int \ \Rightarrow \ List[Boolean]}} &\mathrm{List[Boolean]}
\end{align}

でも、これでは矢印が連なりませんね。仕方ないので、次のように型が付く関数たちを考えましょう。

val ff: String => Char    = ???

val gg: Char => Int = ???
val hh: Int => Boolean = ???

これであれば、すっきり連なります。

\mathrm{String} \xrightarrow{\mathrm{ff: \ String \ \Rightarrow \ Char}} \mathrm{Char} \xrightarrow{\mathrm{gg: \ Char \ \Rightarrow \ Int}} \mathrm{Int} \xrightarrow{\mathrm{hh: \ Int \ \Rightarrow \ Boolean}} \mathrm{Boolean}

関数 ff gg hh は素直に関数合成することができて、これも結合律を満たすので、

\mathrm{String} \xrightarrow{\mathrm{hh.compose(gg.compose(ff)): \ String \ \Rightarrow \ Boolean}} \mathrm{Boolean} \\

\mathrm{String} \xrightarrow{\mathrm{(hh.compose(gg)).compose(ff): \ String \ \Rightarrow \ Boolean}} \mathrm{Boolean}

は同一の射となります。このように形作られる圏を、本記事では圏 $\mathscr{S}$ と呼ぶことにします。

圏 $\mathscr{S}$ では、射を意味する $\rightarrow$ と、関数型を意味する $\Rightarrow$ が、矢印として一致するので分かりやすいですね。というか実のところ関数型で矢印を使うのは、型システムの理論的な支えとして圏論や論理学があるからだったりします。なるほどなー。


関手の定義

そろそろ圏と圏の橋渡しをする「関手」を導入します。ふたたび『圏論の歩き方』 から定義 7.1 を引用します。


圏 $\mathscr{C}_0$ から圏 $\mathscr{C}_1$ への関手 $F:\mathscr{C}_0 \rightarrow \mathscr{C}_1$ とは,

 


  • $\mathscr{C}_0$ の任意の対象 $A$ を $\mathscr{C}_1$ のひとつの対象 $F(A)$ に,

  • $\mathscr{C}_0$ の任意の射 $f$ を $\mathscr{C}_1$ のひとつの射 $F(f)$ に,

うつすものであって,

 


  • 任意の対象 $A$ について $F(\mathrm{id}_A) = \mathrm{id}_{F(A)}$ .

  • 合成が定義できる任意の射 $g, f$ について $F(g \circ f) = F(g) \circ F(f)$ .

をみたすものである.


要するに、射の結合律と単位律を保存しながら、対象と射を移すものが関手です。

射も関手も同じ矢印を使って描くのでなんだか紛らわしいですが、圏論は色んなレベルの矢印が出てくるので、いちいち描き分けたりせず文脈で読みとるスタイルです。慣れましょう><


二つの圏を行き来する二つの関手

さてさてさて。我々は圏 $\mathscr{S}$ と圏 $\mathscr{S}_\mathrm{List}$ を手に入れたのでした。どちらも型を対象とし、素直な合成ないしは for 文による合成によって関数である射は結合律と単位律を満たします。

この二つの圏の関わりを調べるには $F:\mathscr{S} \rightarrow \mathscr{S}_\mathrm{List}$ とか $G:\mathscr{S}_\mathrm{List} \rightarrow \mathscr{S}$ といった関手があると良さそうです。具体的に作ってましょう。

まずは次のような関数合成に着目します。

val ff     : String => Char       = ???

val list_ff: String => List[Char] = (List(_: Char)).compose(ff)

これは圏 $\mathscr{S}$ の中で、

\mathrm{String} \xrightarrow{\mathrm{ff}} \mathrm{Char} \xrightarrow{\mathrm{List(\_)}} \mathrm{List[Char]} 

を合成すると、

\mathrm{String} \xrightarrow{\mathrm{(List(\_)).compose(ff)}} \mathrm{List[Char]}

に対応する射が圏 $\mathscr{S}_\mathrm{List}$ の中で、

\mathrm{String} \xrightarrow{\mathrm{(List(\_)).compose(ff)}} \mathrm{Char}

といふうに取れるということを示しています。ほむ。

よって関手 $F:\mathscr{S} \rightarrow \mathscr{S}_\mathrm{List}$ は、

\begin{align}

F(\mathrm{String}) &= \mathrm{String} \\
F(\mathrm{Char}) &= \mathrm{Char} \\
F(\mathrm{ff}) &= \mathrm{(List(\_:\ Char)).compose(ff)}
\end{align}

というふうに作ることができます。本当に?

関手であるためには結合律と単位律を保存しなければならないのでした。確かめてみましょう。まずは結合律から、

\begin{align}

F(\mathrm{gg.compose(ff))} &= \mathrm{(List(\_:\ Int)).compose(gg.compose(ff))} \\
&= \mathrm{((List(\_:\ Int)).compose(gg)).compose(ff)} \\
&= \mathrm{∙((List(\_:\ Int)).compose(gg), (List(\_:\ Char))).compose(ff)} \\
&= \mathrm{∙((List(\_:\ Int)).compose(gg), (List(\_:\ Char)).compose(ff))} \\
&= \mathrm{∙}(F(\mathrm{gg}), F(\mathrm{ff}))
\end{align}

そして単位律については、単位元が単位元に移ることを確認すればいいので、

F(\mathrm{(a:\ Char) => a}) = \mathrm{(List(\_:\ Char)).compose((a:\ Char) => a)} = \mathrm{(List(\_:\ Char))}

よって結合律も単位律も保存されます。やりましたね。

これで $F:\mathscr{S} \rightarrow \mathscr{S}_\mathrm{List}$ が手に入ったので、あとは対となる $G:\mathscr{S}_\mathrm{List} \rightarrow \mathscr{S}$ があれば、圏 $\mathscr{S}$ と圏 $\mathscr{S}_\mathrm{List}$ の間を自由に行き来できることになります。

単純に $G$ を $F$ の逆関手とできればいいのですが、 $G$ は射について全単射ではないので、それは叶いません。そこで次のような関数合成に着目します。

val f   : String       => List[Char] = ???

val f_id: List[String] => List[Char] = ∙(f, (a: List[String]) => a)

これは圏 $\mathscr{S}_\mathrm{List}$ の中で、

\mathrm{List[String]} \xrightarrow{\mathrm{a \ => \ a}} \mathrm{String} \xrightarrow{\mathrm{f}} \mathrm{Char}

を合成すると、

\mathrm{List[String]} \xrightarrow{\mathrm{∙(f, \ a \ => \ a)}} \mathrm{Char}

に対応する射が圏 $\mathscr{S}$ の中で、

\mathrm{List[String]} \xrightarrow{\mathrm{∙(f, \ a \ => \ a)}} \mathrm{List[Char]}

といふうに取れるということを示しています。ほむほむ。

よって関手 $G:\mathscr{S}_\mathrm{List} \rightarrow \mathscr{S}$ は、

\begin{align}

G(\mathrm{String}) &= \mathrm{List[String]} \\
G(\mathrm{Char}) &= \mathrm{List[Char]} \\
G(\mathrm{f}) &= \mathrm{∙(f, \ (a: List[String]) => a)}
\end{align}

というふうに作ることができます。できるのです。

さて $G$ は $F$ の逆関手ではないので、行って帰って元には戻らないのですが、

\begin{align}

G(F(\mathrm{String})) &= \mathrm{List[String]} \\
G(F(\mathrm{Char})) &= \mathrm{List[Char]} \\
G(F(\mathrm{ff})) &= \mathrm{∙((List(\_:\ Char)).compose(ff), \ (a: List[String]) => a)} \\
&= \mathrm{(\_:\ List[String]).map(ff)}
\end{align}

といふうに $F$ と $G$ を合成した関手 $GF:\mathscr{S} \rightarrow \mathscr{S}$ は対象も射も List に持ちあげる綺麗な自己関手になるので、これはこれで対になっているのですね。ということで、この行って帰って生まれるズレを捉えるために「自然変換」というものを導入します。

まぁ行きて帰りし物語で、主人公が何も変わらなかったらつまらないですしね。


自然変換の定義

ふたたびふたたび『圏論の歩き方』 から定義 5.2 を引用します。

ここで可換図式とは、どの経路で合成しても同じ射になるような図式のことです。


$\mathscr{C}, \mathscr{D}$ を圏, $F,G:\mathscr{C} \rightarrow \mathscr{D}$ を関手とする. $F$ から $G$ への自然変換とは, $\mathscr{C}$ の対象で添字づけられた射の族

\left\{ \alpha_C: FC \rightarrow GC \mid C \in \mathrm{Ob}_\mathscr{C} \right\}

で,任意の射 $f \in \mathrm{Home}_\mathscr{C}(A,B)$ に対して以下の図式を可換にするものである.

\require{AMScd}

\begin{CD}
FA @>\alpha_A>> GA \\
@VFfVV @VVGfV \\
FB @>>\alpha_B> GB
\end{CD}

以降,$F$ から $G$ への自然変換 $\alpha$ を $\alpha: F \rightarrow G$ と書く.


また矢印が増えました。対象から対象への矢印が射で、それらを一纏めにした圏から圏への矢印が関手で、関手から関手への矢印が自然変換というわけです。

ちなみに、ややこしめの豆知識を書いておくと。関手を対象とみなし、自然変換を射とみなすことで、新たな圏を作ることができます。これを関手圏と呼びます。このあたりの話もあって、圏論はあえて同じ矢印記号を色んなところに使うのですね。


二つの圏を行き来する二つの関手を繋ぐ二つの自然変換

さてさてさてさて $F:\mathscr{S} \rightarrow \mathscr{S}_\mathrm{List}$ と $G:\mathscr{S}_\mathrm{List} \rightarrow \mathscr{S}$ を繋ぐ自然変換を考えたいのですが、この二つは矢印の元と先となる圏が一致していません。そこで合成した関手 $GF:\mathscr{S} \rightarrow \mathscr{S}$ と、 $\mathscr{S}$ の対象と射どちらも同じものを返す関手……すなわち恒等関手 $\mathrm{id}_\mathscr{S} :\mathscr{S} \rightarrow \mathscr{S}$ を繋ぐ自然変換 $\eta: \mathrm{id}_\mathscr{S} \rightarrow GF$ を考えてみましょう。つまり、

\require{AMScd}

\begin{CD}
\mathrm{String} @>\eta_\mathrm{String}>> \mathrm{List[String]} \\
@V\mathrm{ff}VV @VV\mathrm{\_.map(ff)}V \\
\mathrm{Char} @>>\eta_\mathrm{Char}> \mathrm{List[Char]}
\end{CD}

という図式を書いて $\eta_A = \mathrm{List(\_:\ A)}$ とすると、これは経路によらず射の合成が同じに、すなわち可換になります。

そして、やはり $\eta$ の逆自然変換 $GF \rightarrow \mathrm{id}_\mathscr{S}$ はうまく作れないのですが、なんと一つ持ちあげて $\mu: GFGF \rightarrow GF$ は作ることができます。先程の図式を大きくして、

\require{AMScd}

\begin{CD}
\mathrm{String} @>\eta_\mathrm{String}>> \mathrm{List[String]} @<\mu_\mathrm{String}<< \mathrm{List[List[String]]} \\
@V\mathrm{ff}VV @VV\mathrm{\_.map(ff)}V @VV\mathrm{\_.map(\_.map(ff))}V \\
\mathrm{Char} @>>\eta_\mathrm{Char}> \mathrm{List[Char]} @<<\mu_\mathrm{String}< \mathrm{List[List[String]]}
\end{CD}

$\mu_A = \mathrm{∙((a: List[A]) => a , (a: List[List[A]]) => a)}$ とすれば、これも可換になります。

Scala では、これを表すメソッドがあって $\mu_A = \mathrm{(\_: List[List[A]]).flatten}$ と書くことができます。二重リストをフラットな一重リストに潰すためのものですね。

はい、お疲れ様でした。なんだか色々と定義してきましたが、この $(GF,\eta,\mu)$ という三つ組こそが、圏論で「モナド」と呼ばれている概念そのものになります。やったぜ。


モナドの定義

圏論の歩き方』 から定義 5.3 を引用します。もう、ゴールしてもいいよね……。


圏 $\mathscr{C}$ 上のモナド(monad)とは $(T,\eta,\mu)$ という組で, $T:\mathscr{C} \rightarrow \mathscr{C}$ は関手, $\eta : \mathrm{id} \rightarrow T$ , $\mu : T \circ T \rightarrow T$ は自然変換であり,任意の $\mathscr{C}$ の対象 $A$ に対して以下の図式を可換にするものである.

\require{AMScd}

\begin{CD}
TA @>T(\eta_A)>> T(TA) @<\eta_{TA}<< TA \\
@V\mathrm{id}_{TA}VV @VV\mu_AV @VV\mathrm{id}_{TA}V \\
TA @>>\mathrm{id}_{TA}> TA @<<\mathrm{id}_{TA}< TA
\end{CD}

\require{AMScd}

\begin{CD}
T(T(TA)) @>T(\mu_A)>> T(TA) \\
@V\mu_{TA}VV @VV\mu_AV \\
T(TA) @>>\mu_A> TA
\end{CD}

なお, $\eta$ を単位元, $\mu$ を乗法と呼ぶ.


ということで「Scala の for 文はモナドを扱うのための構文」などといわれた時に、いきなり圏論のモナドの定義を見ても「日本語でおk」という気持ちにしかならないと思うのですが、その間にはこういう話が横たわっているのです。慣れれば「それはそう」という感じになってしまうのですけれどね。

そして Scala の for 文は List だけでなく Option や Future などにも使うことができます。これらはみな自然に for 文による関数合成において結合律と単位律……すなわちモナド則を満たすので、そこから本記事の話の流れを追っていけば、 map による自己関手、コンストラクタ、 flatten 、の三つ組によって正しくモナドが導けるというわけです。


おわりに

いかがでしたでしょうか。 for 文がわかるプログラマのためのモナド入門としては、多分この流れが一番速いと思います。

やー、実のところモナドの解説記事はネットに溢れているので今更感はあったのですが。

Scala 使いの私は一年に一回くらい「なんでプログラミングと圏論が関係あるんだっけ?」となって、とりあえずググると Haskell のモナド則の記事が出てくるので、 Scala の for と Haskell の do を脳内マッピングしながらプログラミングのモナド則と圏論のモナドをクライスリ圏経由して繋げる、ということに半日くらい時間かけてしまいます。ただ、そういう読み方はけっこう本質的でないところに頭使ってしまって、あまり記憶に残らず。。。

もちろん Scala のモナド記事もあるのですが Haskell ほど充実していないのと、たいてい型クラス前提で implicit のトリックを使って実装することになるので、まずはモナドについて知りたい初心者向きではないと思っています。ただ、型クラスについては Scala 3 で文法整理される予定なので、じきに分かりやすくなりそうです。

ということで、一年後の自分のためにコード付きの自己完結的な記事が欲しくて、本記事を書きました。そうしてみたら Scala 固有の知識にもほぼ依存しなくなったと思うので、対象読者広めのタイトルにしてみたというわけです。モナドはふつうのプログラマが感じている「自然さ」をそのまま抽象化したもの、という気持ちが本記事で伝えらていたらなぁと祈っています。