IndexedContを使ってみよう: 継続編
この記事は?
IndexedCont
便利なのでみんな使ってほしいとの思いの元、前提となる知識から解説する記事です。
前回はfor式を使ったエラーハンドリングの基本的なことを書きました。
なお、今回もIndexedCont
は 出てきません 。
継続
今回も引き続きfor
を使ってエラーハンドリングしていきます。
相変わらずソースコードはメモにスマートフォンで直書きなので、コンパイルしていません。
きっとそのうち修正されます。
Cont データ型
今回はCont
という型を使ってエラーを処理していきます。
case class Cont[R, A](run: (A => R) => R) object Cont { def point[R, A](a: A): Cont[R, A] = Cont(f => f(a)) }
あまり型パラメーターや関数型プログラミングになじみのない方には謎な表現だと思われますが、ぜひ使ってほしいので丁寧に説明します。
覚えておいてほしいのは、ほとんどの場合構造を頭で理解するよりも使って慣れていく方が簡単だということです。
Contとは
たぶんContinuation
(継続)の略です。
唯一の変数であるrun
の型は「[(A => R)
という関数を受け取ってR
を返す]関数」です。
R
: 目指す結果(Result)A
: 現在の値A => R
: 現在の値から結果までの続きの処理(継続)
scala> val a = Cont.point[String, Int](42) a: Cont[String, Int] = Cont(<function>) scala> val b = a.run(i => i.toString) b: String = 42
map
Cont
にmap
を定義します。
case class Cont[R, A](run: (A => R) => R) { def map[B](f: A => B): Cont[R, B] = Cont(g => run(f andThen g)) }
A
からR
向かう処理の途中でf: A => B
というA
からB
までの処理を受け取ったので、残りはB => R
までとなるのがmap
です。
scala> val a = Cont.point[String, Int](65) a: Cont[String, Int] = Cont(<function>) scala> val b = a.map(_.toChar) // 現在の値はCharの 'A' (たぶん) b: Cont[String, Char] = Cont(<function>) scala> val c = b.map(c => List(c, c, c)) // 現在の値はList('A', 'A', 'A') c: Cont[String, List[Char]] = Cont(<function>) scala> val d = c.run(_.mkString("-")) d: String = A-A-A
普通のプログラミングの1行ずつがすごく難しそうになっただけですね。
エラー処理(継続の破棄)
Cont
の何が便利かというと、run
の引数で渡ってきたA => R
は必ずしも使わなくても良いということです。
使い道は無いもののわかりやすい例として、数値の処理はするけれども100以上は扱わない関数を作ってみます。
def smallOnly(i: Int): Cont[String, Int] = Cont { f => if (i < 100) { f(i) } else { "大きすぎてムリです。" } }
i
が100以下ならば続きの処理をi
に対して実行するけれど、それ以外の場合は既にあきらめるCont
を作れます。
scala> val a = smallOnly(65) a: Cont[String, Int] = Cont(<function>) scala> val b = a.map(_.toChar) // 現在の値はCharの 'A' (たぶん) b: Cont[String, Char] = Cont(<function>) scala> val c = b.map(c => List(c, c, c)) // 現在の値はList('A', 'A', 'A') c: Cont[String, List[Char]] = Cont(<function>) scala> val d = c.run(_.mkString("-")) d: String = A-A-A scala> val o = smallOnly(101) // iが100以上なので Cont(_ => "大きすぎてムリです。") o: Cont[String, Int] = Cont(<function>) scala> val p = a.map(_.toChar) // Cont(f => a.run(i => f(i.toChar))) だけど、a.runには何を渡しても無視される。 p: Cont[String, Char] = Cont(<function>) scala> val q = b.map(c => List(c, c, c)) // 同上 q: Cont[String, List[Char]] = Cont(<function>) scala> val r = c.run(_.mkString("-")) r: String = 大きすぎてムリです。
なんとなくエラー処理に使えそうではないでしょうか。
flatMap
for
式と言えばflatMap
(詳しくはWebで)なので、定義していきましょう。
case class Cont[R, A](run: (A => R) => R) { // map省略 def flatMap[B](f: A => Cont[R, B]): Cont[R, B] = Cont(g =>run(a => f(a).run(g))) }
これでfor
の中でCont
が使えるようになります。
for { a <- Cont.point[String, Char]('a') b <- Cont.point[String, Int](3) } yield (a.toInt + b).toChar // => Cont[String, Char](f => f('d'))
Contによるエラー処理
前回の記事で定義したgetA
などを使って、
Either
のように失敗した位置のわかる合成をしてみましょう。
注意点としては、Either[E, A]
がエラー時の型E
と成功時の型A
を返してきていたのに対し、
Cont[R, A]
では最終的にR
の型一つしか返せないということです。
そのため、結果の型を以下のように定義します。
sealed trait Result // 失敗系 case class ANotFound(id: AID) extends Result case class BNotFound(id: BID) extends Result case class CNotFound(id: CID) extends Result // 成功 case class Succeed(a: A, b: B, c: C) extends Result
成功した場合も失敗した場合もResult
ということになります。
先ほどString
で"大きすぎて・・・"と失敗を表現していたのと同じですね。
def resolveA(id: AID): Cont[Result, A] = Cont { f => val a: Option[A] = getA(id) val b: Option[Result] = a.map(f) // getAがSomeならば継続`f`を適用 val c: Result =b.getOrElse(ANotFound(id)) // Noneならば`f`は無視してANotFound c // 一行書くならで getA(id).fold(ANotFound(id))(f) } def resolveB(id: BID): Cont[Result, B] = Cont(f =>getB(id).fold(BNotFound(id))(f)) def resolveC(id: CID): Cont[Result, C] = Cont(f =>getC(id).fold(CNotFound(id))(f)) def resolve(aID: AID): Cont[Result, Succeed] = for { a <- resolveA(aID) b <- resolveB(a.bID) c <- resolveC(b.cID) } yield Succeed(a, b, c)
実行は以下のような感じです。
scala> val a = resolve(AID(5)).run(x => x) a: Reslut = ANotFound(AID(5)) scala> val b = resolve(AID(6)).run(x => x) b: Reslut = BNotFound(BID(12)) scala> val c = resolve(AID(19)).run(x => x) c: Reslut = CNotFound(CID(11)) scala> val c = resolve(AID(5)).run(x => x) c: Reslut = Succeed((A(AID(8), BID(16)), B(BID(16), CID(8)), C(CID(8)))
Either
とCont
の使い分け
ここまで見ると中身は大きく違うものの、Either
とCont
では大体同じような用途に使えそうなことがわかります。
どちらもfor
の上から順に処理を実行していき、どこかで失敗すると続きは実行されずにエラーケースになります。
では、どのように使い分けるのでしょうか。
一番重要なポイントは戻ってくる値の型です。
Either[E, A]
では処理がすべて終了した後でもE
とA
の二つの型が返ってきます。
それに対してCont[R, A]
では、値を取り出すためにrun
する必要があり、
run
した結果返ってくるのはR
型ただ一つだけです。
これはつまりEither
が失敗した場合に失敗したことを関数の呼び出し側に伝え、エラー処理の分岐を任せているのに対し、
Cont
ではエラー処理自体も関数の中で完結していることを表します。
Cont
を返すということは「エラー処理はやり終えた」ということを伝える意思表示になっています。
もちろんmatchや正規表現などを使ってR
の中身を調べての分岐は不可能ではありませんが、やるべきではないでしょう。
よって、使い分けの目安としては以下のようになります。 - Either: 中間処理など、エラーは伝えたいけれどどう処理するかは決めたくない部分 - Cont: 外部入出力など、エラーの場合でも何か正常な処理を返さないといけない部分
次回への継続
次回はいよいよ本題であるIndexedCont
を扱いたいと思います。