ほぼZIOのbackgroundセクションにあるサンプルそのものだが、備忘録的に残しておく
いわゆる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
)が分離されていることで、差し替え可能なコードになっていることが分かる。