From 69906d4ab7860592272b80e3e29de162eb6663f9 Mon Sep 17 00:00:00 2001 From: Haemin Yoo Date: Sat, 17 Oct 2026 03:56:37 +0000 Subject: [PATCH 1/3] Evaluate retained single relations exactly once --- core/src/main/scala/decrel/syntax.scala | 16 +-- core/src/test/scala/decrel/RelationSpec.scala | 100 +----------------- .../reify/bifunctor/KeepCoherenceSpec.scala | 57 ++++++++++ .../reify/monofunctor/KeepCoherenceSpec.scala | 56 ++++++++++ 4 files changed, 125 insertions(+), 104 deletions(-) create mode 100644 core/src/test/scala/decrel/reify/bifunctor/KeepCoherenceSpec.scala create mode 100644 core/src/test/scala/decrel/reify/monofunctor/KeepCoherenceSpec.scala diff --git a/core/src/main/scala/decrel/syntax.scala b/core/src/main/scala/decrel/syntax.scala index 773eb87..8b4617b 100644 --- a/core/src/main/scala/decrel/syntax.scala +++ b/core/src/main/scala/decrel/syntax.scala @@ -34,23 +34,23 @@ trait syntax { )(implicit ev: LeftOut <:< RightIn, zippable: Zippable.Out[LeftOut, RightOut, ZippedOut] - ): Relation.Composed.Zipped[ + ): Composed.Single[ LeftTree & Relation.Single[LeftIn, LeftOut], LeftIn, LeftOut, - Relation.Composed.Single[ - LeftTree & Relation.Single[LeftIn, LeftOut], - LeftIn, + Composed.Zipped[ + Relation.Self[LeftOut], + LeftOut, LeftOut, RightTree & Relation[RightIn, RightOut], RightIn, - RightOut + RightOut, + ZippedOut ], - LeftIn, - RightOut, + LeftOut, ZippedOut ] = - Relation.Composed.Zipped(left, Relation.Composed.Single(left, right)) + Relation.Composed.Single(left, Relation.Composed.Zipped(Relation.Self[LeftOut], right)) def >>:[LeftTree, LeftIn, LeftOut]( left: LeftTree & Relation.Optional[LeftIn, LeftOut] diff --git a/core/src/test/scala/decrel/RelationSpec.scala b/core/src/test/scala/decrel/RelationSpec.scala index e4c7894..8af11c0 100644 --- a/core/src/test/scala/decrel/RelationSpec.scala +++ b/core/src/test/scala/decrel/RelationSpec.scala @@ -60,23 +60,7 @@ object RelationSpec extends ZIOSpecDefault { assertCompletes }, test("one to one, zipped") { - val composed: Composed.Zipped[ - Foo.bar.type & Relation.Single[Foo, Bar], - Foo, // Input - Bar, - Composed.Single[ - Foo.bar.type & Relation.Single[Foo, Bar], - Foo, - Bar, - Bar.baz.type & Relation.Single[Bar, Baz], - Bar, - Baz - ], - Foo, - Baz, - (Bar, Baz) // Output - ] = - Foo.bar <>: Bar.baz + val composed = Foo.bar <>: Bar.baz val _: Relation[Foo, (Bar, Baz)] = composed @@ -223,30 +207,7 @@ object RelationSpec extends ZIOSpecDefault { assertCompletes }, test("compose one zipped & a zipped relation") { - val composed: Composed.Zipped[ - Foo.baz.type & Relation.Single[Foo, Baz], - Foo, - Baz, - Composed.Single[ - Foo.baz.type & Relation.Single[Foo, Baz], - Foo, - Baz, - Composed.Zipped[ - Baz.foo.type & Relation[Baz, Option[Foo]], - Baz, - Option[Foo], - Baz.bar.type & Relation[Baz, Option[Bar]], - Baz, - Option[Bar], - (Option[Foo], Option[Bar]) - ] & Relation[Baz, (Option[Foo], Option[Bar])], - Baz, - (Option[Foo], Option[Bar]) - ], - Foo, - (Option[Foo], Option[Bar]), - (Baz, Option[Foo], Option[Bar]) - ] = + val composed = Foo.baz <>: (Baz.foo & Baz.bar) val _: Relation[Foo, (Baz, Option[Foo], Option[Bar])] = composed @@ -254,38 +215,7 @@ object RelationSpec extends ZIOSpecDefault { assertCompletes }, test("compose one compose one zipped & a zipped relation") { - val composed: Composed.Zipped[ - Foo.self.type & Relation[Foo, Foo], - Foo, - Foo, - Composed.Zipped[ - Foo.baz.type & Relation.Single[Foo, Baz], - Foo, - Baz, - Composed.Single[ - Foo.baz.type & Relation.Single[Foo, Baz], - Foo, - Baz, - Composed.Zipped[ - Baz.foo.type & Relation[Baz, Option[Foo]], - Baz, - Option[Foo], - Baz.bar.type & Relation[Baz, Option[Bar]], - Baz, - Option[Bar], - (Option[Foo], Option[Bar]) - ] & Relation[Baz, (Option[Foo], Option[Bar])], - Baz, - (Option[Foo], Option[Bar]) - ], - Foo, - (Option[Foo], Option[Bar]), - (Baz, Option[Foo], Option[Bar]) - ] & Relation[Foo, (Baz, Option[Foo], Option[Bar])], - Foo, - (Baz, Option[Foo], Option[Bar]), - (Foo, Baz, Option[Foo], Option[Bar]) - ] = + val composed = Foo.self & (Foo.baz <>: (Baz.foo & Baz.bar)) val _: Relation[Foo, (Foo, Baz, Option[Foo], Option[Bar])] = composed @@ -484,29 +414,7 @@ object RelationSpec extends ZIOSpecDefault { assertCompletes }, test("<>: and >>: has same operator precedence") { - val composed: Composed.Zipped[ - Foo.bar.type & Relation.Single[Foo, Bar], - Foo, - Bar, - Composed.Single[ - Foo.bar.type & Relation.Single[Foo, Bar], - Foo, - Bar, - Composed.Single[ - Bar.baz.type & Relation.Single[Bar, Baz], - Bar, - Baz, - Baz.foo.type & Relation[Baz, Option[Foo]], - Baz, - Option[Foo] - ] & Relation[Bar, Option[Foo]], - Bar, - Option[Foo] - ], - Foo, - Option[Foo], - (Bar, Option[Foo]) - ] = + val composed = Foo.bar <>: Bar.baz >>: Baz.foo val _: Relation[Foo, (Bar, Option[Foo])] = composed diff --git a/core/src/test/scala/decrel/reify/bifunctor/KeepCoherenceSpec.scala b/core/src/test/scala/decrel/reify/bifunctor/KeepCoherenceSpec.scala new file mode 100644 index 0000000..6b82929 --- /dev/null +++ b/core/src/test/scala/decrel/reify/bifunctor/KeepCoherenceSpec.scala @@ -0,0 +1,57 @@ +package decrel.reify.bifunctor + +import decrel.* +import scala.collection.IterableOps +import zio.test.* + +object KeepCoherenceSpec extends ZIOSpecDefault { + object next extends Relation.Single[Int, Int] + object doubled extends Relation.Single[Int, Int] + + class Interpreter extends access with proof with reifiedRelation { + type Access[+E, +A] = () => A + protected def succeed[A](a: A): Access[Nothing, A] = () => a + protected def flatMap[E, A, B](a: Access[E, A])(f: A => Access[E, B]): Access[E, B] = () => + f(a())() + protected def map[E, A, B](a: Access[E, A])(f: A => B): Access[E, B] = () => f(a()) + + var calls = 0 + def relation(f: Int => Int): ReifiedRelation[Int, Nothing, Int] = + new ReifiedRelation.Custom[Int, Nothing, Int] { + def apply(in: Int): Access[Nothing, Int] = () => f(in) + def applyMultiple[Coll[+A] <: Iterable[A] & IterableOps[A, Coll, Coll[A]]]( + in: Coll[Int] + ): Access[Nothing, Coll[Int]] = + () => in.map(f) + } + implicit val nextProof: Proof.Single[next.type, Int, Nothing, Int] = + new Proof.Single[next.type, Int, Nothing, Int] { + val reify = relation { _ => calls += 1; calls } + } + implicit val doubledProof: Proof.Single[doubled.type, Int, Nothing, Int] = + new Proof.Single[doubled.type, Int, Nothing, Int] { + val reify = relation(_ * 2) + } + } + + def spec: Spec[Environment, Any] = suite("retained single coherence")( + test("retain exactly the value consumed downstream, once per execution") { + val interpreter = new Interpreter + import interpreter.* + val composed = next <>: doubled + val reified: ReifiedRelation[Int, Nothing, (Int, Int)] = composed.reify + val run = reified.apply(0) + val first = run() + val second = run() + assertTrue(first == (1, 2), second == (2, 4), calls == 2) + }, + test("bulk execution retains each corresponding downstream input") { + val interpreter = new Interpreter + import interpreter.* + val composed = next <>: doubled + val reified: ReifiedRelation[Int, Nothing, (Int, Int)] = composed.reify + val result = reified.applyMultiple(Vector(0, 0, 0))() + assertTrue(result == Vector((1, 2), (2, 4), (3, 6)), calls == 3) + } + ) +} diff --git a/core/src/test/scala/decrel/reify/monofunctor/KeepCoherenceSpec.scala b/core/src/test/scala/decrel/reify/monofunctor/KeepCoherenceSpec.scala new file mode 100644 index 0000000..7009e35 --- /dev/null +++ b/core/src/test/scala/decrel/reify/monofunctor/KeepCoherenceSpec.scala @@ -0,0 +1,56 @@ +package decrel.reify.monofunctor + +import decrel.* +import scala.collection.IterableOps +import zio.test.* + +object KeepCoherenceSpec extends ZIOSpecDefault { + object next extends Relation.Single[Int, Int] + object doubled extends Relation.Single[Int, Int] + + class Interpreter extends access with proof with reifiedRelation { + type Access[A] = () => A + protected def succeed[A](a: A): Access[A] = () => a + protected def flatMap[A, B](a: Access[A])(f: A => Access[B]): Access[B] = () => f(a())() + protected def map[A, B](a: Access[A])(f: A => B): Access[B] = () => f(a()) + + var calls = 0 + def relation(f: Int => Int): ReifiedRelation[Int, Int] = + new ReifiedRelation.Custom[Int, Int] { + def apply(in: Int): Access[Int] = () => f(in) + def applyMultiple[Coll[+A] <: Iterable[A] & IterableOps[A, Coll, Coll[A]]]( + in: Coll[Int] + ): Access[Coll[Int]] = + () => in.map(f) + } + implicit val nextProof: Proof.Single[next.type, Int, Int] = + new Proof.Single[next.type, Int, Int] { + val reify = relation { _ => calls += 1; calls } + } + implicit val doubledProof: Proof.Single[doubled.type, Int, Int] = + new Proof.Single[doubled.type, Int, Int] { + val reify = relation(_ * 2) + } + } + + def spec: Spec[Environment, Any] = suite("retained single coherence")( + test("retain exactly the value consumed downstream, once per execution") { + val interpreter = new Interpreter + import interpreter.* + val composed = next <>: doubled + val reified: ReifiedRelation[Int, (Int, Int)] = composed.reify + val run = reified.apply(0) + val first = run() + val second = run() + assertTrue(first == (1, 2), second == (2, 4), calls == 2) + }, + test("bulk execution retains each corresponding downstream input") { + val interpreter = new Interpreter + import interpreter.* + val composed = next <>: doubled + val reified: ReifiedRelation[Int, (Int, Int)] = composed.reify + val result = reified.applyMultiple(Vector(0, 0, 0))() + assertTrue(result == Vector((1, 2), (2, 4), (3, 6)), calls == 3) + } + ) +} From a96b4fb96a06113b3b7fa04d26e04c62d3f10fbc Mon Sep 17 00:00:00 2001 From: Haemin Yoo Date: Sat, 17 Oct 2026 04:08:51 +0000 Subject: [PATCH 2/3] Exercise retained coherence through the existing deferred transform --- .../reify/bifunctor/KeepCoherenceSpec.scala | 15 ++++++--------- .../reify/monofunctor/KeepCoherenceSpec.scala | 15 ++++++--------- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/core/src/test/scala/decrel/reify/bifunctor/KeepCoherenceSpec.scala b/core/src/test/scala/decrel/reify/bifunctor/KeepCoherenceSpec.scala index 6b82929..7cb11b5 100644 --- a/core/src/test/scala/decrel/reify/bifunctor/KeepCoherenceSpec.scala +++ b/core/src/test/scala/decrel/reify/bifunctor/KeepCoherenceSpec.scala @@ -1,7 +1,6 @@ package decrel.reify.bifunctor import decrel.* -import scala.collection.IterableOps import zio.test.* object KeepCoherenceSpec extends ZIOSpecDefault { @@ -15,15 +14,13 @@ object KeepCoherenceSpec extends ZIOSpecDefault { f(a())() protected def map[E, A, B](a: Access[E, A])(f: A => B): Access[E, B] = () => f(a()) - var calls = 0 + var calls = 0 + type Id[A] = A def relation(f: Int => Int): ReifiedRelation[Int, Nothing, Int] = - new ReifiedRelation.Custom[Int, Nothing, Int] { - def apply(in: Int): Access[Nothing, Int] = () => f(in) - def applyMultiple[Coll[+A] <: Iterable[A] & IterableOps[A, Coll, Coll[A]]]( - in: Coll[Int] - ): Access[Nothing, Coll[Int]] = - () => in.map(f) - } + new ReifiedRelation.Transformed[Int, Id, Nothing, Id, Int]( + new ReifiedRelation.FromFunction[Int, Int](identity), + f + ) implicit val nextProof: Proof.Single[next.type, Int, Nothing, Int] = new Proof.Single[next.type, Int, Nothing, Int] { val reify = relation { _ => calls += 1; calls } diff --git a/core/src/test/scala/decrel/reify/monofunctor/KeepCoherenceSpec.scala b/core/src/test/scala/decrel/reify/monofunctor/KeepCoherenceSpec.scala index 7009e35..f9109ff 100644 --- a/core/src/test/scala/decrel/reify/monofunctor/KeepCoherenceSpec.scala +++ b/core/src/test/scala/decrel/reify/monofunctor/KeepCoherenceSpec.scala @@ -1,7 +1,6 @@ package decrel.reify.monofunctor import decrel.* -import scala.collection.IterableOps import zio.test.* object KeepCoherenceSpec extends ZIOSpecDefault { @@ -14,15 +13,13 @@ object KeepCoherenceSpec extends ZIOSpecDefault { protected def flatMap[A, B](a: Access[A])(f: A => Access[B]): Access[B] = () => f(a())() protected def map[A, B](a: Access[A])(f: A => B): Access[B] = () => f(a()) - var calls = 0 + var calls = 0 + type Id[A] = A def relation(f: Int => Int): ReifiedRelation[Int, Int] = - new ReifiedRelation.Custom[Int, Int] { - def apply(in: Int): Access[Int] = () => f(in) - def applyMultiple[Coll[+A] <: Iterable[A] & IterableOps[A, Coll, Coll[A]]]( - in: Coll[Int] - ): Access[Coll[Int]] = - () => in.map(f) - } + new ReifiedRelation.Transformed[Int, Id, Id, Int]( + new ReifiedRelation.FromFunction[Int, Int](identity), + f + ) implicit val nextProof: Proof.Single[next.type, Int, Int] = new Proof.Single[next.type, Int, Int] { val reify = relation { _ => calls += 1; calls } From c306492d80dd70774d3ccc498aa8ecd124892ac3 Mon Sep 17 00:00:00 2001 From: Haemin Yoo Date: Sat, 17 Oct 2026 06:42:58 +0000 Subject: [PATCH 3/3] Allow retained values to feed supertype relation inputs --- .../scala/decrel/reify/bifunctor/proof.scala | 2 +- .../decrel/reify/monofunctor/proof.scala | 2 +- .../reify/bifunctor/KeepCoherenceSpec.scala | 24 +++++++++++++++++-- .../reify/monofunctor/KeepCoherenceSpec.scala | 24 +++++++++++++++++-- 4 files changed, 46 insertions(+), 6 deletions(-) diff --git a/core/src/main/scala/decrel/reify/bifunctor/proof.scala b/core/src/main/scala/decrel/reify/bifunctor/proof.scala index 92ef4ba..c27dec0 100644 --- a/core/src/main/scala/decrel/reify/bifunctor/proof.scala +++ b/core/src/main/scala/decrel/reify/bifunctor/proof.scala @@ -430,7 +430,7 @@ trait proof { this: access & reifiedRelation => LeftOut, LeftOutRefined <: LeftOut, RightTree, - RightIn <: LeftIn, + RightIn, RightE, RightOut, ZippedOut, diff --git a/core/src/main/scala/decrel/reify/monofunctor/proof.scala b/core/src/main/scala/decrel/reify/monofunctor/proof.scala index ac7e705..8ab2408 100644 --- a/core/src/main/scala/decrel/reify/monofunctor/proof.scala +++ b/core/src/main/scala/decrel/reify/monofunctor/proof.scala @@ -390,7 +390,7 @@ trait proof { this: access & reifiedRelation => LeftOut, LeftOutRefined <: LeftOut, RightTree, - RightIn <: LeftIn, + RightIn, RightOut, ZippedOut, ZOR <: ZippedOut diff --git a/core/src/test/scala/decrel/reify/bifunctor/KeepCoherenceSpec.scala b/core/src/test/scala/decrel/reify/bifunctor/KeepCoherenceSpec.scala index 7cb11b5..68ab10e 100644 --- a/core/src/test/scala/decrel/reify/bifunctor/KeepCoherenceSpec.scala +++ b/core/src/test/scala/decrel/reify/bifunctor/KeepCoherenceSpec.scala @@ -7,6 +7,8 @@ object KeepCoherenceSpec extends ZIOSpecDefault { object next extends Relation.Single[Int, Int] object doubled extends Relation.Single[Int, Int] + object described extends Relation.Single[AnyVal, String] + class Interpreter extends access with proof with reifiedRelation { type Access[+E, +A] = () => A protected def succeed[A](a: A): Access[Nothing, A] = () => a @@ -14,6 +16,12 @@ object KeepCoherenceSpec extends ZIOSpecDefault { f(a())() protected def map[E, A, B](a: Access[E, A])(f: A => B): Access[E, B] = () => f(a()) + implicit val describedProof: Proof.Single[described.type, AnyVal, Nothing, String] = + new Proof.Single[described.type, AnyVal, Nothing, String] { + val reify: ReifiedRelation[AnyVal, Nothing, String] = + new ReifiedRelation.FromFunction[AnyVal, String](_.toString) + } + var calls = 0 type Id[A] = A def relation(f: Int => Int): ReifiedRelation[Int, Nothing, Int] = @@ -31,8 +39,20 @@ object KeepCoherenceSpec extends ZIOSpecDefault { } } + private def checked(label: String)(assertion: => TestResult) = + test(label)(Live.live(zio.ZIO.sleep(zio.Duration.fromMillis(10))) *> zio.ZIO.attempt(assertion)) + def spec: Spec[Environment, Any] = suite("retained single coherence")( - test("retain exactly the value consumed downstream, once per execution") { + checked("retained values can feed a strict supertype input") { + val interpreter = new Interpreter + import interpreter.* + val composed = next <>: described + val reified = composed.reify + val single = reified.apply(0)() + val bulk = reified.applyMultiple(Vector(0, 0))() + assertTrue(single == (1, "1"), bulk == Vector((2, "2"), (3, "3")), calls == 3) + }, + checked("retain exactly the value consumed downstream, once per execution") { val interpreter = new Interpreter import interpreter.* val composed = next <>: doubled @@ -42,7 +62,7 @@ object KeepCoherenceSpec extends ZIOSpecDefault { val second = run() assertTrue(first == (1, 2), second == (2, 4), calls == 2) }, - test("bulk execution retains each corresponding downstream input") { + checked("bulk execution retains each corresponding downstream input") { val interpreter = new Interpreter import interpreter.* val composed = next <>: doubled diff --git a/core/src/test/scala/decrel/reify/monofunctor/KeepCoherenceSpec.scala b/core/src/test/scala/decrel/reify/monofunctor/KeepCoherenceSpec.scala index f9109ff..1f79f21 100644 --- a/core/src/test/scala/decrel/reify/monofunctor/KeepCoherenceSpec.scala +++ b/core/src/test/scala/decrel/reify/monofunctor/KeepCoherenceSpec.scala @@ -7,12 +7,20 @@ object KeepCoherenceSpec extends ZIOSpecDefault { object next extends Relation.Single[Int, Int] object doubled extends Relation.Single[Int, Int] + object described extends Relation.Single[AnyVal, String] + class Interpreter extends access with proof with reifiedRelation { type Access[A] = () => A protected def succeed[A](a: A): Access[A] = () => a protected def flatMap[A, B](a: Access[A])(f: A => Access[B]): Access[B] = () => f(a())() protected def map[A, B](a: Access[A])(f: A => B): Access[B] = () => f(a()) + implicit val describedProof: Proof.Single[described.type, AnyVal, String] = + new Proof.Single[described.type, AnyVal, String] { + val reify: ReifiedRelation[AnyVal, String] = + new ReifiedRelation.FromFunction[AnyVal, String](_.toString) + } + var calls = 0 type Id[A] = A def relation(f: Int => Int): ReifiedRelation[Int, Int] = @@ -30,8 +38,20 @@ object KeepCoherenceSpec extends ZIOSpecDefault { } } + private def checked(label: String)(assertion: => TestResult) = + test(label)(Live.live(zio.ZIO.sleep(zio.Duration.fromMillis(10))) *> zio.ZIO.attempt(assertion)) + def spec: Spec[Environment, Any] = suite("retained single coherence")( - test("retain exactly the value consumed downstream, once per execution") { + checked("retained values can feed a strict supertype input") { + val interpreter = new Interpreter + import interpreter.* + val composed = next <>: described + val reified = composed.reify + val single = reified.apply(0)() + val bulk = reified.applyMultiple(Vector(0, 0))() + assertTrue(single == (1, "1"), bulk == Vector((2, "2"), (3, "3")), calls == 3) + }, + checked("retain exactly the value consumed downstream, once per execution") { val interpreter = new Interpreter import interpreter.* val composed = next <>: doubled @@ -41,7 +61,7 @@ object KeepCoherenceSpec extends ZIOSpecDefault { val second = run() assertTrue(first == (1, 2), second == (2, 4), calls == 2) }, - test("bulk execution retains each corresponding downstream input") { + checked("bulk execution retains each corresponding downstream input") { val interpreter = new Interpreter import interpreter.* val composed = next <>: doubled