モナドのマニュアル
対象読者: TypeScriptくらいなら書けるよ、という程度に多少は型に馴染んでいる人
オタク君向けの警告
この読み物では数学の話はしません
この読み物ではでは理解を優先させ、正確性を犠牲にします
だから、「教科書」とか「入門」とせず、「マニュアル」とした
これを読んだら巷のコードの雰囲気が分かる、くらいのゴール感
例示用の言語としてScalaを利用しますが、Scala独自の機能はなるだけ避けます
いつ使う?
モナドの典型的な利用例として、以下のような場面がある:
失敗しうる処理を連続させる
非同期な処理を連続させる
複数の結果を返す処理を連続させる
構文木を扱う
ひとことで言うと
モナドとは、連続する計算の文脈を扱おうとすると共通して出現する便利なパターン・構造・操作の体系である
モナドであるためにはいくつかの規則がある。言い換えると、その規則さえ守ればモナドとして共通の操作体系に載っかれる
計算の文脈
計算の文脈というとぎこちない表現だが
例えば「だいたい成功するが、たまに結果がない」ような処理について考えてほしい
失敗しうる処理、と呼ぶことにしよう
失敗しうる処理を1つ呼ぶだけなら、エラーハンドリングをするだけでいいので、特に困ることはない
code:scala
// nをmで割る計算。mが0だと例外をスローするであろう
def div(n: Int, m: Int): Int = n / m
code:scala
val n = 42
val m = 0 // ここでは固定しているが、実際は動的にどこかからやってくると想定する
// 例外を処理して進めたいので・・・
try {
div(n, m)
} catch {
case e: Exception => println("エラーになっちゃった")
}
// 何事もなく続きの処理ができる
失敗を型に載せる
ところで別の話題として、失敗するかどうかは型を見ただけで分かりたい
code:scala
div(42, 0) // これは(Int, Int) => Int だが、呼んだらヤバいかもしれない
add(42, 0) // これも(Int, Int) => Int だが、呼んでも安全
ScalaにはOptionという型が用意されている:
code:scala
// Optionは型パラメータ(ジェネリクス)で任意の型を取る
val intOption: OptionInt = ??? // Optionそのものは抽象的な型で、実際にはSomeかNoneかのどちらかの型の値をとる
// Someは値をとるが・・・
val someOption: OptionInt = Some(42) // Noneは値をとらず、唯一の値Noneがあるだけ
val noneOption: OptionInt = None 図にするとこういう関係:
code:mmd
https://scrapbox.io/files/685d3e3acc27400aa199ac10.png
Some: 成功した〜
None: 失敗しちゃった〜
これで失敗しうるかどうかが型を見ただけで判断できて嬉しい
code:scala
def div(n: Int, m: Int): OptionInt = try { Some(n / m) // Scalaでは全てが式なのでこれでよい
} catch {
case e: Exception => None
}
code:scala
scala> div(42, 42)
val res0: OptionInt = Some(1) scala> div(42, 0)
val res1: OptionInt = None 計算の連鎖
ところで、実用的にはこういう計算は連鎖させたい
「失敗しうる処理Aをやって、失敗しうる処理Bをやって、失敗しうる処理Cをやりたい」
そして、こういう一連の計算を全体として見たときもまた、失敗しうる計算である
DB接続して行う処理なんかは大抵これ
毎回取り出すと大変
code:scala
val optionalA = optionalFunctionA()
if (!optionalA.isDefined) { // isDefinedは、Someのときにのみtrueになる
return None
}
val optionalB = optionalFunctionB(optionalA.get) // getは、Someの中身を取り出す
if (!optionalB.isDefined) {
return None
}
val optionalC = optionalFunctionC(optionalB.get)
if (!optionalC.isDefined) {
return None
}
return Some(/* optionalA, optionalB, optionalCをふんだんに使った値 */)
こういう処理は、以下のように整理される
いくつかの、失敗しうる計算
それをうまいこと中継して1つの計算にまとめる
失敗しうる計算とは、以下のような形をしている
A => Option[B]
https://scrapbox.io/files/685d4315b17feb5bb5e47b4a.png
そして、それを中継して1つの計算にすることで連鎖させられる
https://scrapbox.io/files/685d556bd99f334630944a22.png
便利だなあ
計算の連鎖 その2
Optionの例だけではピンとこないかもしれないのでもう一つ例を挙げてみましょう
例えば「計算がどっかで行われて、そのうち結果が得られる」ような処理について考えてみてほしい
非同期処理、と呼ぶことにしよう
「どっか」とは、別のスレッドだったりグリーンスレッドだったりする
非同期処理を1つだけ使うなら、やはりそこで待つだけでいいので、大した仕組みはいらない
code:scala
// Scalaでは標準ライブラリにFutureがあり、非同期処理を表現できる
import scala.concurrent.Future
// 非同期処理を待機する君
import scala.concurrent.Await
// スレッドまわりの管理してくれる君
import scala.concurrent.ExecutionContext.Implicits.global
// 1.minutes といった表記を可能にする
import scala.concurrent.duration.*
// 重い計算
def tarai(x: Int, y: Int, z: Int): Int =
if x <= y then y else tarai(tarai(x-1, y, z), tarai(y-1, z, x), tarai(z-1, x, y))
val futureX = Future(tarai(15,7,0)) // どっかのスレッドで実行される
// futureXがどっかのスレッドで計算されているが、それとは無関係にプログラムは進む
// ...
Await.result(futureX, 1.minutes) // futureXの結果を最大1分間待て
// => 15
しかしやっぱり、こういう計算は連鎖させたい
「非同期処理Aをやって、非同期処理Bをやって、非同期処理Cをやりたい」
そして、こういう一連の計算を全体として見たときもまた、非同期処理である
DB接続して行う処理なんかは大抵これ
またか
毎回取り出すと大変
code:scala
def getUserIdByUserAccountId(accountId: String): FutureString = ??? def getEmailAddressById(id: Long): FutureString = ??? def sendEmail(emailAddress: String): FutureBoolean = ??? val userAccountId = "@windymelt"
Future {
val userIdFuture = getUserIdByUserAccountId(userAccountId)
val userId = Await.result(userIdFuture, 1.minutes)
val emailAddressFuture = getEmailAddressById(userId)
val emailAddress = Await.result(emailAddressFuture, 1.minutes)
val sendEmailFuture = sendEmail(emailAddress)
Await.result(sendEmailFuture, 1.minutes)
}
こういう処理は、以下のように整理される
いくつかの、非同期で行われる計算
それをうまいこと中継して1つの計算にまとめる
非同期処理とは、以下のような形をしている
A => Future[B]
https://scrapbox.io/files/685d5dbe634969de634ccf2a.png
そして、それを中継して1つの計算にすることで連鎖させられる
https://scrapbox.io/files/685d5ececc27400aa19a48ba.png
んー、なんか似てんな〜
文脈を持った計算の中継
先程の2つの節に登場したA => Option[B]とか、A => Future[B]自体にはそれほど難しさがなかった
ただの関数だからね
図でいうところの、濃い赤色の右を向いた矢印の部分
他方で、Option[B]やFuture[B]から値を取り出して、次の計算に手渡す部分は結構面倒なことになっていた
Optionの場合は
計算が成功していたら計算結果を取り出して次の計算へ
計算が失敗していたらそこで計算を中止して戻る
Futureの場合は
計算が終わっていたら計算結果を取り出して次の計算へ
計算が終わっていなかったらそこで待つ
図でいうところの、薄い青色の下向き矢印の部分
というか、この「文脈から値を取り出して、文脈に応じたなんかを行い、次の計算に手渡す」という薄い青色の部分こそが、OptionやFutureがやりたいことの本質的な部分に見える
この青い部分って簡単に書けないの?
簡単に書く:
Option[A]とA => Option[B]があるとき、簡単にOption[B]を得たい
または、A => Option[B]とB => Option[C]があるとき、簡単にA => Option[C]を得たい
こっちは今回は割愛(実は等価だったりする)
OptionでもFutureでもこの「簡単に書きたい」というモチベーションは同じなので、ここからはF[A]と書くことにする
つまり、F[A]とA => F[B]があるとき、簡単にF[B]を得たい
この面倒な部分だけメソッドかなんかに押し込んでラクできないかな〜
flatMap
ScalaではflatMapと呼ばれているが、他の言語ではbindと呼ばれていたりする
flatMapの定義: ある型Fがあるとして、F[A]とA => F[A]があるとき、F[B]を返すような関数
これともう一つpure(後述)を定義できて、モナド則(後述)というルールに従うならば、モナドを名乗ってよいことになっている
モナド則に従うかどうかはテストなどで保証する
flatMapというインターフェイスはあるが、共通の実装は存在しない。OptionやFutureといったそれぞれの型ごとに、flatMapの実装が用意される
Scalaの場合、例えばOptionのflatMap実装は以下のようになっている(無関係な箇所は割愛している):
code:scala
sealed abstract class Option+A { // ...
def flatMapB(f: A => OptionB): OptionB = if (isEmpty) None else f(this.get)
メソッドなので、F[A]を引数に取るところはthisで代替されている
「もし空ならそのままNoneを返せ。そうでなければgetした結果をfに渡した結果をそのまま返せ」
面倒な部分がflatMapという共通の名前になったことで、次のように処理を連鎖させられるようになる:
code:scala
if (n != 0) {
Some(1.0 / n)
} else {
None
}
def g(d: Double): OptionDouble = Some(d.abs) def h(d: Double): OptionDouble = if (d < 0.0) {
None
} else {
Some(math.log(d))
}
// 失敗したタイミングで計算は中止される
Some(42).flatMap(f).flatMap(g).flatMap(h)
// => Some(-3.7376696182833684)
Some(0).flatMap(f).flatMap(g).flatMap(h)
// => None
code:scala
def f(n: Int): FutureInt = Future(n*2) def g(n: Int): FutureInt = Future(n+1) def h(n: Int): FutureString = Future(n.toString) // 非同期処理が完了次第、次の非同期処理に渡されていく
val fut = Future(42).flatMap(f).flatMap(g).flatMap(h)
Await.result(fut, 1.minutes) // => "85"
以前に示した図では青い部分の仕事がflatMap
https://scrapbox.io/files/685d70295a9f4fadfb884ce5.png
pure
Haskellなどではreturnと呼ばれていたりする(TypeScriptなどで登場するreturnとは全くの別物)
ここまでは「F[A]があるとき」みたいにF[A]がある前提でシレッと話していたが・・・
flatMapがあっても、そもそもIntからどうにかしてOption[Int]といった値を作って渡せなければ実用できないじゃん
なのでモナドであるためには以下の型を持つ関数pureが必要
A => F[A]
「どんな型Aの値でも、F[A]にして返しますよ」という関数
例えば42のOptionに対するpureであればSomeに包んでやることでpureを実装できる
例えば42のFutureに対するpureであれば即座に計算が完了した状態のSuccessに包んでやることでpureを実装できる
前の節でシレッとSome(42)などのように書いているが、あれができる能力のことをpureと呼んでいるだけ