Runner in the High

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

実装と処理の分離でfor-comprehensionを使う

前回の記事の続きです

izumisy.work

前の記事だと継続渡しのスタイルで処理を表現していたが、継続渡しだとシンプルにインデントがどんどん深くなっていくという微妙な点がある。

Scalaにはfor文というのが用意されているので、継続渡しスタイルよりもfor文を使うスタイル(for-comprehension)のほうが一般的に好まれる。

実際のコードで言うと、以下のようにfor文で処理の流れを書いていく感じになる。前回の記事同様、この時点では具体的な実装に関するものは何も出てこない。あくまで処理の流れだけである。

object Main extends App {
  val program: Runnable = ???

  for {
    _ <- program.output("hello!")
    _ <- program.output("what's your name?")
    name <- program.input
    _ <- program.output(s"$name, nice to meet you.")
  } yield name
}

program変数は、実際にoutputやinputを実行するためのインタプリタの実装となる。

Runnableインタフェースはこんな感じ。

trait Runnable {
  def succeed[A](value: A): Application[A]
  def input: Application[String]
  def output(line: String): Application[_]
}

上のRunnableインタフェースが依存しているApplication型の実装は以下。

sealed trait Application[A] {
  def map[B](f: A => B): Application[B] =
    flatMap(a => Return(() => f(a)))

  def flatMap[B](f: A => Application[B]): Application[B] =
    this match {
      case Return(value) =>
        f(value())
      case Output(value, next) =>
        Output(value, next.flatMap(f))
      case Input(next) =>
        Input(a => next(a).flatMap(f))
    }
}
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]

for-comprehensionを使うためにmapとflatMapを実装している。

最後にRunnableインタフェースを満たす具体的なコンソールの実装に依存したConsoleクラスを作る。

class Console extends Runnable {
  import scala.io.StdIn

  override def succeed[A](value: A): Application[A] =
    Return(() => value)

  override def input: Application[String] =
    succeed(StdIn.readLine())

  override def output(line: String): Application[_] = {
    println(line)
    Output(line, succeed(()))
  }
}

最後に、Consoleクラスを実装として差し込む。

val program: Runnable = new Console

これでfor文で処理の流れを記述しつつ処理と実装を分離することができた。

こういうのをFreeモナドって言うのかな?