来世から頑張る!!

技術ブログを目指して

FreeのRunnerを抽象化する

モチベーション

差し替え時の変更忘れなどを防ぐために、Runner部分を抽象化してConstructor Injectionしたい。

Free

ここは今回重要ではないので、Functorの要らない単純な実装を用意。

sealed trait Free[F[_], A] {
  def map[B](f: A => B): Free[F, B] = flatMap(a => Free.pure(f(a))
  def flatMap[B](f: A => Free[F, B]): Free[F, B] = this match {
    case Free.Pure(a) => f(a)
    case Free.Impure(fi, g) => Free.impure(fi, g.andThen(_.flatMap(f)))
  }
}

object Free {
  final case class Pure[F[_], A](a: A) extends Free[F, A]
  final case class Impure[F[_], I, A](fi: F[I], f: I => F[A]): extends Free[F, A]

  def pure[F[_], A](a: A): Free[F, A] = Pure(a)
  def impure[F[_], I, A](fi: F[I], f: I => F[A]): Free[F, A] = Impure(fi, f)
  def liftF[F[_], A])(fa: F[A]): Free[F, A] = impure(fa, pure)
}

Reader

簡単な何か。

object Reader {
  sealed abstract case class Ask[I, A](run: I => A)

  def ask[I](i: I): Free[Ask[I, ?], I] =
    Free.liftF(new Ask(identity[I]) { })
}

ビジネスロジック

何か適当に。

def powers(times: Int): Free[Reader.Ask[Int, ?], List[Int]] = {
  def pow(i: Int, j: Int, acc: Int = 1): Int = {
    if (j <= 0) acc
    else pow(i, j - 1, acc * i)
  }

  def loop(i: Int, acc: Free[Reader.Ask[Int, ?], List[Int]]): Free[Reader.Ask[Int, ?], List[Int]] = {
    if (times < i) acc.map(_.reverse)
    else {
      val next = for {
        list <- acc
        j <- Reader.ask[Int]
      } yield pow(j, i) +: list
      loop(i + 1, next)
    }
  }

  loop(1, Free.pure(List.empty))
}

Runner

抽象化していない実装。

object ReaderRunner {
  @tailrec
  def run[I, A](fa: Free[Reader.Ask[I, ?], A])(i: I): A = fa match {
    case Free.Pure(a) => a
    case Free.Impure(ask, f) => run(f(ask.run(i)))(i)
  }
}
ReaderRunner.run(powers(3))(2) // => List(2, 4, 8)

Runnerを抽象化する。

変更したい部分は入力と戻り値の2か所。
型パラメーターにこの部分を取れば、抽象化できる。

trait ReaderRunner[I[_], O[_]] {
  def run[P, A](fa: Free[Reader.Ask[P, ?], A])(p: I[P]): O[A]
}

実装を複数定義可能に。

object ReaderRunner {
  type Id[A] = A
  object Simple extends ReaderRunner[Id, Id] {
    @tailrec
    def run[P, A](fa: Free[Reader.Ask[P, ?], A])(p: P): A = fa match {
      case Free.Pure(a) => a
      case Free.Impure(ask, f) => run(f(ask.run(p)))(p)
    }
  }

  object ListInput extends ReaderRunner[List, Option] {
    @tailrec
    def run[P, A](fa: Free[Reader.Ask[P, ?], A])(p: List[P]): Option[A] =
      fa match {
        case Free.Pure(a) => Some(a)
        case Free.Impure(ask, f) =>
          if (p.length == 0) None
          else run(f(ask.run(p.head)))(p.tail)
      }
  }
}
ReaderRunner.Simple.run(powers(3))(2)
// => List(2, 4, 8)
ReaderRunner.ListInput.run(powers(3))(List(1, 2, 3, 4, 5))
// => Some(List(1, 4, 27))

おまけ

({type L[A] = String})#Lみたいに定義すると、元の戻り値を無視することが可能。 State[S, A]({type L[A] = (S, A)})#LSのみ返す実装にするときなどに使える。

enrich my library

.runみたいにしたい場合、rmlでなんとかできる。

implicit class RunReader[P, A](val fa: Free[Reader.Ask[P, ?], A]) extends AnyVal {
  def runReader[I[_], O[_]](p: I[P])(implicit RR: ReaderRunner[I, O]): O[A] = RR.run(fa)(p)
}
implicit val rr: ReaderRunner[List, Option] = ReaderRunner.ListInput
powers(11).runReader(List(1, 3, 5))

さらなる抽象化?

Free[F[_], A]Fの部分を型パラメーターに取れば、FreeRunnerのようなものも作成はできる。 ただ、引数の数などを固定することにあまり旨味が見えないので、過度な抽象化のように思う。

なお、EffのRunnerでも戻り値の型が変わるぐらいで同様の事が可能。