Runner in the High

技術のことをかくこころみ

継続渡しスタイルを用いた実装と処理の分離

ほぼZIOのbackgroundセクションにあるサンプルそのものだが、備忘録的に残しておく

zio.dev

いわゆるfor-comprehensionを使わずに、Applicationをミクスインした各ADTがそれぞれrestという名前で次の処理をコールバックとして取る実装(継続渡しスタイル)になっている。

sealed trait Application[A]
final case class Return[A](value: () => A) extends Application[A]
final case class Output[A](line: String, rest: Application[A]) extends Application[A]
final case class Input[A](rest: String => Application[A]) extends Application[A]

object Main extends App {

  // ここではApplicationを用いて"どのような流れで処理が行われるか"だけを実装している
  // OutputやInputは抽象化された入力と出力になっていて、ここでは実装のことを気にしない。
  val program: Application[String] =
    Output("what's your name?",
      Input(name =>
        Output(s"Nice to meet you $name",
          Return(() => name)
        )
      )
    )

  // Application型がどのように実行されるかがここに実装されている。
  // この関数はprintlnやStdIn.readLineなどコンソールの実装に依存している
  // 異なる実装に差し替える場合には、同じくApplication[A]を取るinterpreterを実装すればよい。
  def consoleInterpreter[A](program: Application[A]): A =
    program match {
      case Return(value) =>
        value()
      case Output(value, next) =>
        println(value)
        consoleInterpreter(next)
      case Input(next) =>
        consoleInterpreter(next(StdIn.readLine()))
    }

  // ここで処理と実装をがっちゃんこして初めてアプリケーションを起動する
  consoleInterpreter(program)

}

実装(consoleInterpreter)と抽象(program)が分離されていることで、差し替え可能なコードになっていることが分かる。