Skip to content

Commit

Permalink
prelude: Aspect improvements (#752)
Browse files Browse the repository at this point in the history
  • Loading branch information
fwbrasil authored Oct 14, 2024
1 parent 937ad51 commit 7434c55
Show file tree
Hide file tree
Showing 3 changed files with 212 additions and 34 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1391,8 +1391,8 @@ val countPlusOne =
new Aspect.Cut[Database, Int, IO]:
// The first param is the input of the computation and the second is
// the computation being handled
def apply[S](v: Database < S)(f: Database => Int < IO) =
v.map(db => f(db).map(_ + 1))
def apply(db: Database)(f: Database => Int < IO) =
f(db).map(_ + 1)

// Bind the 'Cut' to a computation with the 'let' method.
// The first param is the 'Cut' and the second is the computation
Expand All @@ -1409,8 +1409,8 @@ def example(db: Database): Int < IO =
// Another 'Cut' implementation
val countTimesTen =
new Aspect.Cut[Database, Int, IO]:
def apply[S](v: Database < S)(f: Database => Int < IO) =
v.map(db => f(db).map(_ * 10))
def apply(db: Database)(f: Database => Int < IO) =
f(db).map(_ * 10)

// First bind 'countPlusOne' then 'countTimesTen'
// the result will be (db.count + 1) * 10
Expand Down
10 changes: 5 additions & 5 deletions kyo-prelude/shared/src/main/scala/kyo/Aspect.scala
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ final class Aspect[A, B, S] private[kyo] (default: Cut[A, B, S])(using Frame) ex
* @return
* The result of applying the aspect and then the function
*/
def apply[S2](v: A < S2)(f: A => B < S) =
def apply(v: A)(f: A => B < S) =
local.use { map =>
map.get(this) match
case Some(a: Cut[A, B, S] @unchecked) =>
Expand Down Expand Up @@ -115,7 +115,7 @@ object Aspect:
* @return
* The result of applying the cut and then the function
*/
def apply[S2](v: A < S2)(f: A => B < S): B < (S & S2)
def apply(v: A)(f: A => B < S): B < S

/** Chains this cut with another cut.
*
Expand All @@ -126,7 +126,7 @@ object Aspect:
*/
def andThen(other: Cut[A, B, S])(using Frame): Cut[A, B, S] =
new Cut[A, B, S]:
def apply[S2](v: A < S2)(f: A => B < S) =
def apply(v: A)(f: A => B < S) =
Cut.this(v)(other(_)(f))
end Cut

Expand All @@ -143,8 +143,8 @@ object Aspect:
*/
def init[A, B, S](using Frame): Aspect[A, B, S] =
init(new Cut[A, B, S]:
def apply[S2](v: A < S2)(f: A => B < S) =
v.map(f)
def apply(v: A)(f: A => B < S) =
f(v)
)

/** Initializes a new Aspect with a custom default cut.
Expand Down
228 changes: 203 additions & 25 deletions kyo-prelude/shared/src/test/scala/kyo/AspectTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,17 @@ class AspectTest extends Test:

"with cut" in run {
val cut = new Cut[Int, Int, Any]:
def apply[S](v: Int < S)(f: Int => Int < Any) =
v.map(v => f(v + 1))
def apply(v: Int)(f: Int => Int < Any) =
f(v + 1)
aspect.let[Assertion, Any](cut) {
test(1).map(v => assert(v == 3))
}
}

"sandboxed" in run {
val cut = new Cut[Int, Int, Any]:
def apply[S](v: Int < S)(f: Int => Int < Any) =
v.map(v => f(v + 1))
def apply(v: Int)(f: Int => Int < Any) =
f(v + 1)
aspect.let[Assertion, Any](cut) {
aspect.sandbox {
test(1)
Expand All @@ -34,11 +34,11 @@ class AspectTest extends Test:

"nested cuts" in run {
val cut1 = new Cut[Int, Int, Any]:
def apply[S](v: Int < S)(f: Int => Int < Any) =
v.map(v => f(v * 3))
def apply(v: Int)(f: Int => Int < Any) =
f(v * 3)
val cut2 = new Cut[Int, Int, Any]:
def apply[S](v: Int < S)(f: Int => Int < Any) =
v.map(v => f(v + 5))
def apply(v: Int)(f: Int => Int < Any) =
f(v + 5)
aspect.let[Assertion, Any](cut1) {
aspect.let[Assertion, Any](cut2) {
test(2).map(v => assert(v == 2 * 3 + 5 + 1))
Expand All @@ -47,7 +47,57 @@ class AspectTest extends Test:
}
}

"multiple aspects" in run {
"multiple aspects" - {
"independent aspects" in run {
val aspect1 = Aspect.init[Int, Int, Any]
val aspect2 = Aspect.init[Int, Int, Any]

def test(v: Int) =
for
v1 <- aspect1(v)(_ + 1)
v2 <- aspect2(v)(_ + 1)
yield (v1, v2)

val cut1 = new Cut[Int, Int, Any]:
def apply(v: Int)(f: Int => Int < Any) =
f(v * 3)
val cut2 = new Cut[Int, Int, Any]:
def apply(v: Int)(f: Int => Int < Any) =
f(v + 5)
aspect1.let[Assertion, Any](cut1) {
aspect2.let[Assertion, Any](cut2) {
test(2).map(v => assert(v == (2 * 3 + 1, 2 + 5 + 1)))
}
}
}

"chained aspects" in run {
val aspect1 = Aspect.init[Int, Int, Any]
val aspect2 = Aspect.init[Int, Int, Any]

def test(v: Int) =
aspect1(v) { v1 =>
aspect2(v1) { v2 =>
v2 + 1
}
}

val cut1 = new Cut[Int, Int, Any]:
def apply(v: Int)(f: Int => Int < Any) =
f(v * 2)
val cut2 = new Cut[Int, Int, Any]:
def apply(v: Int)(f: Int => Int < Any) =
f(v + 3)

aspect1.let[Assertion, Any](cut1) {
aspect2.let[Assertion, Any](cut2) {
test(2).map(v => assert(v == ((2 * 2) + 3) + 1))
}
}
}
}

"use aspect as a cut" in run {
val aspect1 = Aspect.init[Int, Int, Any]
val aspect2 = Aspect.init[Int, Int, Any]

Expand All @@ -57,36 +107,164 @@ class AspectTest extends Test:
v2 <- aspect2(v)(_ + 1)
yield (v1, v2)

val cut = new Cut[Int, Int, Any]:
def apply(v: Int)(f: Int => Int < Any) =
f(v * 3)
aspect1.let[Assertion, Any](cut) {
aspect2.let[Assertion, Any](aspect1) {
test(2).map(v => assert(v == (2 * 3 + 1, 2 * 3 + 1)))
}
}
}

"aspect chain" in run {
val aspect = Aspect.init[Int, Int, Any]

val cut1 = new Cut[Int, Int, Any]:
def apply[S](v: Int < S)(f: Int => Int < Any) =
v.map(v => f(v * 3))
def apply(v: Int)(f: Int => Int < Any) =
f(v * 2)
val cut2 = new Cut[Int, Int, Any]:
def apply[S](v: Int < S)(f: Int => Int < Any) =
v.map(v => f(v + 5))
aspect1.let[Assertion, Any](cut1) {
aspect2.let[Assertion, Any](cut2) {
test(2).map(v => assert(v == (2 * 3 + 1, 2 + 5 + 1)))
}
def apply(v: Int)(f: Int => Int < Any) =
f(v + 3)
val cut3 = new Cut[Int, Int, Any]:
def apply(v: Int)(f: Int => Int < Any) =
f(v - 1)

val chainedCut = Aspect.chain(cut1, Seq(cut2, cut3))

aspect.let[Assertion, Any](chainedCut) {
aspect(5) { v => v }.map(v => assert(v == ((5 * 2 + 3) - 1)))
}
}

"use aspect as a cut" in run {
"aspect sandbox with multiple aspects" in run {
val aspect1 = Aspect.init[Int, Int, Any]
val aspect2 = Aspect.init[Int, Int, Any]

def test(v: Int) =
for
v1 <- aspect1(v)(_ + 1)
v2 <- aspect2(v)(_ + 1)
yield (v1, v2)
v2 <- aspect2(v1)(_ * 2)
yield v2

val cut1 = new Cut[Int, Int, Any]:
def apply(v: Int)(f: Int => Int < Any) =
f(v * 3)
val cut2 = new Cut[Int, Int, Any]:
def apply(v: Int)(f: Int => Int < Any) =
f(v + 5)
aspect1.let[Assertion, Any](cut1) {
aspect2.let[Assertion, Any](cut2) {
aspect1.sandbox {
test(2)
}.map(v => assert(v == (2 + 5 + 1) * 2))
}
}
}

"nested aspect lets" in run {
val aspect = Aspect.init[Int, Int, Any]

def test(v: Int) = aspect(v)(_ + 1)

val cut1 = new Cut[Int, Int, Any]:
def apply(v: Int)(f: Int => Int < Any) =
f(v * 2)
val cut2 = new Cut[Int, Int, Any]:
def apply(v: Int)(f: Int => Int < Any) =
f(v + 3)

aspect.let[Assertion, Any](cut1) {
aspect.let[Assertion, Any](cut2) {
test(2).map(v => assert(v == (2 * 2 + 3) + 1))
}
}
}

"aspect order of application" in run {
val aspect = Aspect.init[Int, Int, Any]

def test(v: Int) = aspect(v)(_ + 1)

val cut1 = new Cut[Int, Int, Any]:
def apply(v: Int)(f: Int => Int < Any) =
f(v * 2)
val cut2 = new Cut[Int, Int, Any]:
def apply(v: Int)(f: Int => Int < Any) =
f(v + 3)

aspect.let[Assertion, Any](cut1) {
aspect.let[Assertion, Any](cut2) {
test(2).map(v => assert(v == (2 * 2 + 3) + 1))
}
}

aspect.let[Assertion, Any](cut2) {
aspect.let[Assertion, Any](cut1) {
test(2).map(v => assert(v == (2 + 3) * 2 + 1))
}
}
}

"aspect reuse after let" in run {
val aspect = Aspect.init[Int, Int, Any]

def test(v: Int) = aspect(v)(_ + 1)

val cut = new Cut[Int, Int, Any]:
def apply[S](v: Int < S)(f: Int => Int < Any) =
v.map(v => f(v * 3))
aspect1.let[Assertion, Any](cut) {
aspect2.let[Assertion, Any](aspect1) {
test(2).map(v => assert(v == (2 * 3 + 1, 2 * 3 + 1)))
def apply(v: Int)(f: Int => Int < Any) =
f(v * 2)

aspect.let[Assertion, Any](cut) {
test(2).map(v => assert(v == 2 * 2 + 1))
}

test(2).map(v => assert(v == 3))
}

"aspect chain with identity cut" in run {
val aspect = Aspect.init[Int, Int, Any]

val cut1 = new Cut[Int, Int, Any]:
def apply(v: Int)(f: Int => Int < Any) =
f(v * 2)

val identityCut = new Cut[Int, Int, Any]:
def apply(v: Int)(f: Int => Int < Any) =
f(v)

val chainedCut = Aspect.chain(cut1, Seq(identityCut))

aspect.let[Assertion, Any](chainedCut) {
aspect(5) { v => v }.map(v => assert(v == 5 * 2))
}
}

"aspect interaction with effects" in run {
val aspect = Aspect.init[Int, Int, Var[Int]]

def test(v: Int) =
for
_ <- Var.update[Int](_ + v)
result <- aspect(v)(_ + 1)
s <- Var.get[Int]
yield (result, s)

val cut = new Cut[Int, Int, Var[Int]]:
def apply(v: Int)(f: Int => Int < Var[Int]) =
for
_ <- Var.update[Int](_ * 2)
result <- f(v * 2)
yield result

Var.run(0) {
aspect.let(cut) {
test(3).map { case (result, s) =>
assert(result == 3 * 2 + 1)
assert(s == 6) // (0 + 3) * 2
}
}
}
}

end AspectTest

0 comments on commit 7434c55

Please sign in to comment.