-
Notifications
You must be signed in to change notification settings - Fork 11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ToyIO demonstration of a basic IO implementation for runtimes #1162
base: main
Are you sure you want to change the base?
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
package org.bykn.bosatsu | ||
|
||
sealed abstract class ToyIO[+E, +A] | ||
|
||
object ToyIO { | ||
case class Pure[A](get: A) extends ToyIO[Nothing, A] | ||
case class Err[E](get: E) extends ToyIO[E, Nothing] | ||
case class FlatMap[E, A, B](init: ToyIO[E, A], fn: A => ToyIO[E, B]) extends ToyIO[E, B] | ||
case class RecoverWith[E, E1, A](init: ToyIO[E, A], fn: E => ToyIO[E1, A]) extends ToyIO[E1, A] | ||
/** | ||
* fix(f) = f(fix(f)) | ||
*/ | ||
case class ApplyFix[E, A, B](arg: A, fixed: (A => ToyIO[E, B]) => (A => ToyIO[E, B])) extends ToyIO[E, B] { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this constructor can't be written in Bosatsu because |
||
def step: ToyIO[E, B] = | ||
// it is temping to simplify fixed(fix(fixed)) == fix(fixed) | ||
// but fix(af.fixed)(af.arg) == af | ||
// so that doesn't make progress. We need to apply | ||
// af.fixed at least once to create a new value | ||
fixed(Fix(fixed))(arg) | ||
} | ||
case class Fix[E, A, B](fn: (A => ToyIO[E, B]) => (A => ToyIO[E, B])) extends Function1[A, ToyIO[E, B]] { | ||
def apply(a: A): ToyIO[E,B] = ApplyFix(a, fn) | ||
} | ||
|
||
implicit class ToyIOMethods[E, A](private val io: ToyIO[E, A]) extends AnyVal { | ||
def flatMap[E1 >: E, B](fn: A => ToyIO[E1, B]): ToyIO[E1, B] = | ||
FlatMap(io, fn) | ||
|
||
def map[B](fn: A => B): ToyIO[E, B] = flatMap(a => Pure(fn(a))) | ||
def recoverWith[E1](fn: E => ToyIO[E1, A]): ToyIO[E1, A] = | ||
RecoverWith(io, fn) | ||
|
||
def run: Either[E, A] = ToyIO.run(io) | ||
} | ||
|
||
val unit: ToyIO[Nothing, Unit] = Pure(()) | ||
|
||
def pure[A](a: A): ToyIO[Nothing, A] = Pure(a) | ||
|
||
def defer[E, A](io: => ToyIO[E, A]): ToyIO[E, A] = | ||
unit.flatMap(_ => io) | ||
|
||
def delay[A](a: => A): ToyIO[Nothing, A] = | ||
defer(Pure(a)) | ||
|
||
def raiseError[E](e: E): ToyIO[E, Nothing] = Err(e) | ||
|
||
// fix(f) = f(fix(f)) | ||
def fix[E, A, B](recur: (A => ToyIO[E, B]) => (A => ToyIO[E, B])): A => ToyIO[E, B] = | ||
Fix(recur) | ||
|
||
sealed trait Stack[E, A, E1, A1] | ||
|
||
case class Done[E1, A1, E, A](ev: E1 =:= E, av: A1 =:= A) extends Stack[E1, A1, E, A] | ||
case class FMStep[E, E1, A, B, B1](fn: A => ToyIO[E, B], stack: Stack[E, B, E1, B1]) extends Stack[E, A, E1, B1] | ||
case class RecStep[E, E1, E2, A, B](fn: E => ToyIO[E1, A], stack: Stack[E1, A, E2, B]) extends Stack[E, A, E2, B] | ||
|
||
def run[E, A](io: ToyIO[E, A]): Either[E, A] = { | ||
|
||
@annotation.tailrec | ||
def loop[E1, A1](arg: ToyIO[E1, A1], stack: Stack[E1, A1, E, A]): Either[E, A] = | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this loop is the only place with explicit recursion. This loop would be implemented in the runtime or host language using a loop to avoid blowing the stack. |
||
arg match { | ||
case p @ Pure(get) => | ||
stack match { | ||
case Done(_, av) => Right(av(get)) | ||
case FMStep(fn, stack) => | ||
loop(fn(get), stack) | ||
case RecStep(_, stack) => | ||
loop(p, stack) | ||
} | ||
case e @ Err(get) => | ||
stack match { | ||
case Done(ev, _) => Left(ev(get)) | ||
case FMStep(_, stack) => | ||
// unwind the stack | ||
loop(e, stack) | ||
case RecStep(fn, stack) => | ||
loop(fn(get), stack) | ||
} | ||
case FlatMap(init, fn) => | ||
loop(init, FMStep(fn, stack)) | ||
case rw: RecoverWith[e, e1, a] => | ||
loop(rw.init, RecStep(rw.fn, stack)) | ||
case af: ApplyFix[e, a, b] => | ||
// fixed(fix(fixed)) = fix(fixed) | ||
// take a step here, | ||
// this may never terminate, because there is no | ||
// promise that a general recursive function terminates, | ||
// but it won't blow the stack | ||
loop(af.step, stack) | ||
} | ||
|
||
loop(io, Done[E, A, E, A](implicitly, implicitly)) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
package org.bykn.bosatsu | ||
|
||
import cats.Eval | ||
import cats.data.EitherT | ||
import org.scalacheck.{Gen, Cogen, Prop} | ||
import org.bykn.bosatsu.ToyIO.Pure | ||
import org.bykn.bosatsu.ToyIO.Err | ||
import org.bykn.bosatsu.ToyIO.FlatMap | ||
import org.bykn.bosatsu.ToyIO.ApplyFix | ||
import org.bykn.bosatsu.ToyIO.RecoverWith | ||
|
||
class ToyIOTest extends munit.ScalaCheckSuite { | ||
|
||
def toEvalT[E, A](toyio: ToyIO[E, A]): EitherT[Eval, E, A] = | ||
toyio match { | ||
case Pure(get) => EitherT[Eval, E, A](Eval.now(Right(get))) | ||
case Err(get) => EitherT[Eval, E, A](Eval.now(Left(get))) | ||
case fm: FlatMap[e, a1, a2] => | ||
val first = Eval.defer[Either[e, a1]](toEvalT(fm.init).value) | ||
EitherT(first.flatMap { | ||
case Right(a1) => | ||
val fn1 = fm.fn.andThen { toyio => Eval.defer(toEvalT[e, a2](toyio).value) } | ||
fn1(a1) | ||
case Left(err) => | ||
Eval.now(Left(err)) | ||
}) | ||
case rw: RecoverWith[e, e1, a] => | ||
val first = Eval.defer[Either[e, a]](toEvalT(rw.init).value) | ||
EitherT(first.flatMap { | ||
case Right(a1) => Eval.now(Right(a1)) | ||
case Left(err) => | ||
val fn1 = rw.fn.andThen { toyio => Eval.defer(toEvalT[e1, a](toyio).value) } | ||
fn1(err) | ||
}) | ||
case af: ApplyFix[e, a, b] => | ||
lazy val fix: a => ToyIO[e, b] = | ||
{ (a: a) => af.fixed(fix)(a) } | ||
|
||
EitherT(Eval.defer(toEvalT(fix(af.arg)).value)) | ||
} | ||
|
||
trait Move[A] { | ||
def notLessThan(a: A): A | ||
def notMoreThan(a: A): A | ||
} | ||
object Move { | ||
implicit val moveBoolean: Move[Boolean] = | ||
new Move[Boolean] { | ||
def notLessThan(a: Boolean): Boolean = true | ||
def notMoreThan(a: Boolean): Boolean = false | ||
} | ||
|
||
implicit val moveByte: Move[Byte] = | ||
new Move[Byte] { | ||
def notLessThan(a: Byte): Byte = | ||
if (a == Byte.MaxValue) Byte.MaxValue | ||
else (a + 1).toByte | ||
def notMoreThan(a: Byte): Byte = | ||
if (a == Byte.MinValue) Byte.MinValue | ||
else (a - 1).toByte | ||
} | ||
|
||
def apply[A](implicit m: Move[A]): Move[A] = m | ||
} | ||
|
||
def genToy[E: Cogen, A: Cogen: Ordering: Move](genE: Gen[E], genA: Gen[A]): Gen[ToyIO[E, A]] = { | ||
lazy val recur = Gen.lzy(genToy(genE, genA)) | ||
val cogenFn: Cogen[A => ToyIO[E, A]] = | ||
Cogen(_.hashCode.toLong) | ||
|
||
lazy val genFix: Gen[(A => ToyIO[E, A]) => (A => ToyIO[E, A])] = | ||
Gen.zip(genA, Gen.oneOf(true, false), genA, genE).map { case (cut, lt, result, err) => | ||
|
||
val ord = implicitly[Ordering[A]] | ||
val cmpFn = | ||
if (lt) { (a: A) => ord.lt(a, cut) } | ||
else { (a: A) => ord.gt(a, cut) } | ||
val step = | ||
if (lt) { (a: A) => Move[A].notLessThan(a) } | ||
else { (a: A) => Move[A].notMoreThan(a) } | ||
|
||
{ recur => | ||
(a: A) => { | ||
if (ord.equiv(a, cut)) ToyIO.pure(result) | ||
else if (cmpFn(a)) recur(step(a)) | ||
else ToyIO.raiseError(err) | ||
} | ||
} | ||
} | ||
Gen.function1(Gen.function1(recur)(Cogen[A]))(cogenFn) | ||
|
||
Gen.frequency( | ||
2 -> genA.map(ToyIO.pure(_)), | ||
2 -> genE.map(ToyIO.raiseError(_)), | ||
1 -> Gen.zip(recur, Gen.function1[A, ToyIO[E, A]](recur)).map { case (io, fn) => | ||
io.flatMap(fn) | ||
}, | ||
1 -> Gen.zip(recur, Gen.function1[E, ToyIO[E, A]](recur)).map { case (io, fn) => | ||
io.recoverWith(fn) | ||
}, | ||
1 -> Gen.zip(genA, genFix).map { case (a, fn) => ToyIO.fix(fn)(a) } | ||
) | ||
} | ||
|
||
val bytes = Gen.choose(Byte.MinValue, Byte.MaxValue) | ||
val bools = Gen.oneOf(true, false) | ||
|
||
property("evaluation of ToyIO via Eval matches E=Byte, A=Byte") { | ||
Prop.forAll(genToy(bytes, bytes)) { toyio => | ||
assertEquals(toyio.run, toEvalT(toyio).value.value) | ||
} | ||
} | ||
|
||
property("evaluation of ToyIO via Eval matches E=Bool, A=Bool") { | ||
Prop.forAll(genToy(bools, bools)) { toyio => | ||
assertEquals(toyio.run, toEvalT(toyio).value.value) | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Commented this because it blows up the repl