package snake import IO.* class M07IOSuite extends munit.FunSuite: private def rectAt(x: Long): Shape = Shape.Rect(x.toInt, 0, 1, 1, "#000000") private val r1 = rectAt(0) private val t1 = Shape.Text(1, 2, "hi", "#ffffff", 16, "left") private def drawOp(shape: Shape): Op = Op.Draw(shape) private val seedOp: Op = Op.RandomSeed test("trace of pure(3) has result 3 and no ops") { assertEquals(trace(pure(3), 0), Traced(3, Nil)) } test("randomSeed is answered with the seed given to trace") { assertEquals(trace(randomSeed, 9).result, 9L) } test("randomSeed records a RandomSeed op") { assertEquals(trace(randomSeed, 9).ops, List(seedOp)) } test("effect(op) is an Effect whose continuation is pure") { effect(seedOp) match case IO.Effect(op, k) => assertEquals(op, seedOp) assertEquals(k(OpResult.Seed(5)), IO.Pure(OpResult.Seed(5))) case other => fail(s"expected an Effect, got $other") } test("draw records a Draw op and produces ()") { assertEquals(trace(draw(r1), 0), Traced((), List(drawOp(r1)))) } test("drawScene draws the shapes in order") { assertEquals(trace(drawScene(List(r1, t1)), 0).ops, List(drawOp(r1), drawOp(t1))) } test("drawScene of an empty scene has no ops") { assertEquals(trace(drawScene(Nil), 0).ops, Nil) } test("sequence of nothing has no ops") { assertEquals(trace(sequence(Nil), 0), Traced((), Nil)) } test("sequence runs effects left to right") { assertEquals( trace(sequence(List(randomSeed.map(_ => ()), draw(t1), draw(r1))), 0).ops, List(seedOp, drawOp(t1), drawOp(r1)) ) } test("map transforms the result without adding ops") { assertEquals(trace(randomSeed.map(_ + 1), 41), Traced(42L, List(seedOp))) } test("flatMap passes the result of the first IO to the second") { assertEquals( trace(randomSeed.flatMap(s => draw(rectAt(s))), 7).ops, List(seedOp, drawOp(rectAt(7))) ) } test("building io.map(throwingF) does not throw") { val boom: Long => Long = _ => throw new RuntimeException("boom") randomSeed.map(boom) // only builds a description } test("tracing io.map(throwingF) runs f and throws") { val boom: Long => Long = _ => throw new RuntimeException("boom") interceptMessage[RuntimeException]("boom")(trace(randomSeed.map(boom), 0)) } // Monad laws, checked by interpreting both sides and comparing Traced(result, ops). private val programs: List[(String, () => IO[Long])] = List( "a pure value" -> (() => pure(4L)), "randomSeed" -> (() => randomSeed), "a draw followed by reading the seed" -> (() => draw(r1).flatMap(_ => randomSeed)) ) private val f: Long => IO[Long] = n => draw(rectAt(n)).flatMap(_ => pure(n * 2)) private val g: Long => IO[Long] = n => randomSeed.map(_ + n) programs.foreach { (name, program) => test(s"left identity holds for $name") { val a = trace(program(), 5).result assertEquals(trace(pure(a).flatMap(f), 5), trace(f(a), 5)) } test(s"right identity holds for $name") { assertEquals(trace(program().flatMap(pure), 5), trace(program(), 5)) } test(s"associativity holds for $name") { assertEquals( trace(program().flatMap(f).flatMap(g), 5), trace(program().flatMap(x => f(x).flatMap(g)), 5) ) } }