Edited at
ScalaDay 7

Contの合成がしたい


前置き

継続モナド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式の流れは以下のような感じです。


sourceのイメージ

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同士なら問題なく合成できるということです。


違う目標R1R2を目指す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 => RIndexedContの中に内包されています。

外から触る方法はありませんし、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 => E2runに渡される「継続」です。

E2 => EcontCの中で実行され、

E => OcontBの中で、

O => RcontAの中でそれぞれ実行されます。


Contからの変換

前述したようにCont[R, A] = IndexedCont[R, R, A]なので、imapcontramapを使用すると変換が可能です。


imap


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 => Rimapすれば型は元に戻りますが、内部で処理されなくなるわけではありません。


contramap


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


contramapimapとは逆に、Rの前の処理を追加するものです。


最初の課題へ

課題はこちら

Cont[R1, A]: A-------------------------R1

Cont[R2, B]: B---------R2

IndexedContだと考えると、imapcontramapを使えば合成できそうですね。

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って便利ですよね。

少しでも使う人が増えますように。

アドベントカレンダーなのにギリギリ更新ですみません。





  1. のつもりでしたが、日付が変わってしまいそうなためIndexedContの紹介で終わってしまいました。