来世から頑張る!!

技術ブログを目指して

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

Contmapを定義します。

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)))

EitherContの使い分け

ここまで見ると中身は大きく違うものの、EitherContでは大体同じような用途に使えそうなことがわかります。
どちらもforの上から順に処理を実行していき、どこかで失敗すると続きは実行されずにエラーケースになります。
では、どのように使い分けるのでしょうか。

一番重要なポイントは戻ってくる値の型です。 Either[E, A]では処理がすべて終了した後でもEAの二つの型が返ってきます。

それに対してCont[R, A]では、値を取り出すためにrunする必要があり、 runした結果返ってくるのはR型ただ一つだけです。

これはつまりEitherが失敗した場合に失敗したことを関数の呼び出し側に伝え、エラー処理の分岐を任せているのに対し、 Contではエラー処理自体も関数の中で完結していることを表します。

Contを返すということは「エラー処理はやり終えた」ということを伝える意思表示になっています。

もちろんmatchや正規表現などを使ってRの中身を調べての分岐は不可能ではありませんが、やるべきではないでしょう。

よって、使い分けの目安としては以下のようになります。 - Either: 中間処理など、エラーは伝えたいけれどどう処理するかは決めたくない部分 - Cont: 外部入出力など、エラーの場合でも何か正常な処理を返さないといけない部分

次回への継続

次回はいよいよ本題であるIndexedContを扱いたいと思います。