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)})#L
をS
のみ返す実装にするときなどに使える。
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でも戻り値の型が変わるぐらいで同様の事が可能。