前置き
継続モナドCont[R, A]
は「どんな事があっても値を返したい」というシーンでのエラーハンドリングで非常に便利です。
ですが、R
が同一のCont
同士でしか合成できないため、時々型が合わなくて困ることがありますよね。
そういった場合の解決策を考えていこうと思います。1
なお、ContT[F[_], R, A]
でもほとんどの内容は同じなため、すべてCont[R, A]
で記述します。
Cont[R, A]
のおさらい
Cont[R, A]
のmap
, flatMap
の型定義は以下のような感じです。
class Cont[R, A](run: (A => R) => R) {
def map[B](f: A => B): Cont[R, B] = ...
def flatMap[B](f: A => Cont[R, B]): Cont[R, B] = ...
}
このため、for
式の流れは以下のような感じです。
def contA: Cont[R, A]
def contB(a: A): Cont[R, B]
...
val result: Cont[R, D] = for {
a <- contA
b <- contB(a)
c <- contC(b)
d <- contD(c)
} yield d
result.run(d => dToR(d)) // => R
contA : A----------------------------R
contB(a) : B--------------------------R
contC(b) : C------------------------R
contD(c) : D----------------------R
ある終了地点Rに向かって、flatMap
するたびに少しずつ近付いていき、最後にrun
で終着点に到達するイメージですね。
同一の目標R
を目指しているCont
同士なら問題なく合成できるということです。
違う目標R1
とR2
を目指すCont
は合成できないのか
昔のソースコードと戯れるときなど、ちょっと違うCont
同士を合成できないものかと悩むシーンはそれなりにあると思います。
いくつかの場合に分けて方法を考えてみたいと思います。
開始点・終着点が他方のCont
に内包される場合
Cont[R1, A]
とCont[R2, B]
の関係が以下のようになっている場合です。
Cont[R1, A]: A-------------------------R1
Cont[R2, B]: B---------R2
外部入力のString(A
)からID(B
)を作って、そのIDからEntity(R2
)を生成しResult(R1
)を返却するような処理を想定すると、下のように定義できます。
cont1: Cont[Result, String]
cont2: Cont[ID, Entity]
こういった場合は処理は単純で、IndexedCont[R, O, A]
型を使用すると簡単にflatMap
できます。
IndexedCont[R, O, A]
なじみの薄い方向けに、定義を記述しておきます。
定義
class IndexedCont[R, O, A](run: (A => O) => R) {
def map[B](f: A => B): IndexedCont[R, O, B] =
new IndexedCont[R, O, B] { (g: B => O) =>
run(f.andThen(g))
}
def flatMap[B](f: A => IndexedCont[O, E, B]): IndexedCont[R, E, B] =
new IndexedCont[R, E, B] { (g: B => E) =>
run(a => f(a).run(g))
}
def imap[E](f: R => E): IndexedCont[E, O, A] =
new IndexedCont[E, O, A] { (g: A => O) =>
f(run(g))
}
def contramap[I](f: I => O): IndexedCont[R, I, A] =
new IndexedCont[R, I, A] { (g: A => I) =>
run(g.andThen(f))
}
}
意味
基本的にはCont[R, A]
と同様のものですが、run
が受け取る関数がA => R
ではなくA => O
になっています。
現在の値A
からR
に向かう途中の型O
までの処理(継続)を渡してくれたら、O => R
はこのオブジェクトの中で勝手にやるよという型です。
cont1: IndexedCont[R, O, A]: A-------------O-----R
cont2: IndexedCont[O, E, B]: B--------E--O
Cont[R, A]
はIndexedCont[R, O, A]
のR == O
という特殊形なので、型エイリアスを使って以下のように定義できます。
type Cont[R, A] = IndexedCont[R, R, A]
scalazの中では実際にこのような感じで定義されています。
flatMap
IndexedCont[R, O, A]
には以下のような型が登場します。
R: 最終的なゴール
O: 中間地点
A: 現在の値
A => O: runが受け取る「継続」
上記「継続」を受け取りなんらかの方法でO
ができた後、そこからR
を作る方法O => R
はIndexedCont
の中に内包されています。
外から触る方法はありませんし、O
を使ってくれているかどうかもそのインスタンス次第です。
def contA: IndexedCont[R, O, A]
def contB(a: A): IndexedCont[O, E, B]
def contC(b: B): indexedCont[E, E2, C]
val result: IndexedCont[R, E2, C] = for {
a <- contA
b <- contB(a)
c <- contC(b)
} yield c
result.run(c => cToE2(c)) // => R
contA : A-------------------------O--R
contB(a) : B--------------------E--O
contC(b) : C-------------E2---E
result : C-------------E2---------R
A
からC
までの処理はflatMap
の中で実行されます。
C => E2
はrun
に渡される「継続」です。
E2 => E
はcontC
の中で実行され、
E => O
はcontB
の中で、
O => R
はcontA
の中でそれぞれ実行されます。
Contからの変換
前述したようにCont[R, A] = IndexedCont[R, R, A]
なので、imap
やcontramap
を使用すると変換が可能です。
imap
def rToI(r: R): I = ...
val cont1: Cont[R, A] = ...
val cont2: IndexedCont[I, R, A] = cont1.imap(rToI)
cont1 : A-----------------R
cont2 : A-----------------R---I
imap
は上記のように、R
が出来上がった後の処理を足す事ができます。
一度作成したcont2
からcont1
を作り出す方法は無いので注意してください。
もちろんR => I
をキャンセルするようなI => R
でimap
すれば型は元に戻りますが、内部で処理されなくなるわけではありません。
contramap
def oToR(o: O): R = ...
val cont1: Cont[R, A] = ...
val cont2: IndexedCont[R, O, A] = cont1.contramap(oToR)
cont1 : A-----------------R
cont2 : A--------------O--R
contramap
はimap
とは逆に、R
の前の処理を追加するものです。
最初の課題へ
課題はこちら
Cont[R1, A]: A-------------------------R1
Cont[R2, B]: B---------R2
IndexedCont
だと考えると、imap
かcontramap
を使えば合成できそうですね。
IndexedCont[R1, R1, A]: A-------------------------R1
IndexedCont[R2, R2, B]: B---------R2
def contA: Cont[R1, A] = ...
def contB(a: A): Cont[R2, B] = ...
def r2ToR1(r2: R2): R1 = ...
val result: IndexedCont[R1, R2, B] = for {
a <- contA.contramap(r2ToR1)
b <- contB(b)
} yield b
IndexedCont[R1, R2, A]: A----------------R2-------R1
IndexedCont[R2, R2, B]: B---------R2
// または
val result: IndexedCont[R1, R2, B] = for {
a <- contA
b <- contB(b).imap(r2ToR1)
} yield b
IndexedCont[R1, R1, A]: A-------------------------R1
IndexedCont[R1, R2, B]: B---------R2-------R1
開始点・終着点が他方のCont
に内包されない場合
この場合は処理の組み合わせによって、どちらかのContをrunしたうえでつなげていくことになります。
ですが、アドベントカレンダーなのに日付が変わってしまいそうなのでまたの機会に記述したいと思います。
おわりに
Contって便利ですよね。
少しでも使う人が増えますように。
アドベントカレンダーなのにギリギリ更新ですみません。
-
のつもりでしたが、日付が変わってしまいそうなため
IndexedCont
の紹介で終わってしまいました。 ↩