Skip to content

Commit

Permalink
prelude: Check effect (#629)
Browse files Browse the repository at this point in the history
  • Loading branch information
fwbrasil authored Sep 4, 2024
1 parent a068811 commit 0f2366a
Show file tree
Hide file tree
Showing 3 changed files with 212 additions and 0 deletions.
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1296,6 +1296,35 @@ def example3(db: Database) =
}
```

### Check: Runtime Assertions

The `Check` effect provides a mechanism for runtime assertions and validations. It allows you to add checks throughout your code that can be handled in different ways, such collecting failures or discarding them.

```scala
import kyo.*

// Create a simple check
val a: Unit < Check =
Check(1 + 1 == 2, "Basic math works")

// Checks can be composed with other effects
val b: Int < (Check & IO) =
for
value <- IO(42)
_ <- Check(value > 0, "Value is positive")
yield value

// Handle checks by converting the first failed check to Abort
val c: Int < (Abort[CheckFailed] & IO) =
Check.runAbort(b)

// Discard check failures and continue execution
val e: Int < IO =
Check.runDiscard(b)
```

The `CheckFailed` exception class, which is used to represent failed checks, includes both the failure message and the source code location (via `Frame`) where the check failed, making it easier to locate and debug issues.

### Console: Console Interaction

```scala
Expand Down
47 changes: 47 additions & 0 deletions kyo-prelude/shared/src/main/scala/kyo/Check.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package kyo

import Ansi.*
import kyo.kernel.*

class CheckFailed(val message: String, val frame: Frame) extends AssertionError(message):
override def getMessage() =
Seq(
"",
"──────────────────────────────".dim,
"Check failed! ".red.bold + message,
"──────────────────────────────".dim,
frame.parse.show,
"──────────────────────────────".dim
).mkString("\n")
end CheckFailed

sealed trait Check extends ArrowEffect[Const[CheckFailed], Const[Unit]]

object Check:

inline def apply(inline condition: Boolean)(using inline frame: Frame): Unit < Check =
Check(condition, "")

inline def apply(inline condition: Boolean, inline message: => String)(using inline frame: Frame): Unit < Check =
if condition then ()
else ArrowEffect.suspend[Unit](Tag[Check], new CheckFailed(message, frame))

def runAbort[A: Flat, S](v: A < (Check & S))(using Frame): A < (Abort[CheckFailed] & S) =
ArrowEffect.handle(Tag[Check], v)(
[C] => (input, cont) => Abort.fail(input)
)

def runChunk[A: Flat, S](v: A < (Check & S))(using Frame): (Chunk[CheckFailed], A) < S =
ArrowEffect.handle.state(Tag[Check], Chunk.empty[CheckFailed], v)(
handle = [C] =>
(input, state, cont) =>
(state.append(input), cont(())),
done = (state, result) => (state, result)
)

def runDiscard[A: Flat, S](v: A < (Check & S))(using Frame): A < S =
ArrowEffect.handle(Tag[Check], v)(
[C] => (_, cont) => cont(())
)

end Check
136 changes: 136 additions & 0 deletions kyo-prelude/shared/src/test/scala/kyo/CheckTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package kyo

class CheckTest extends Test:

"apply" - {
"with message" - {
"passes when condition is true" in run {
Check.runDiscard(Check(true, "This should pass").map(_ => succeed))
}

"fails when condition is false" in run {
Abort.run(Check.runAbort(Check(false, "This should fail"))).map { r =>
assert(r.failure.get.asInstanceOf[CheckFailed].message == "This should fail")
}
}
}
"no message" - {
"passes when condition is true" in run {
Check.runDiscard(Check(true).map(_ => succeed))
}

"fails when condition is false" in run {
Abort.run(Check.runAbort(Check(false))).map { r =>
assert(r.failure.get.asInstanceOf[CheckFailed].message == "")
}
}
}
}

"runAbort" - {
"returns success for passing checks" in run {
val result = Check.runAbort {
for
_ <- Check(true, "This should pass")
_ <- Check(1 + 1 == 2, "Basic math works")
yield "All checks passed"
}
Abort.run(result).map(r => assert(r == Result.success("All checks passed")))
}

"returns failure for failing checks" in run {
val result = Check.runAbort {
for
_ <- Check(true, "This should pass")
_ <- Check(false, "This should fail")
_ <- Check(true, "This won't be reached")
yield "Shouldn't get here"
}
Abort.run(result).map { r =>
assert(r.failure.get.asInstanceOf[CheckFailed].message == "This should fail")
}
}
}

"runChunk" - {
"collects all check failures" in run {
val result = Check.runChunk {
for
_ <- Check(false, "First failure")
_ <- Check(true, "This passes")
_ <- Check(false, "Second failure")
yield "Done"
}
result.map { case (failures, value) =>
assert(failures.size == 2)
assert(failures(0).message == "First failure")
assert(failures(1).message == "Second failure")
assert(value == "Done")
}
}
}

"runDiscard" - {
"discards check failures and continues execution" in run {
val result = Check.runDiscard {
for
_ <- Check(false, "This failure is discarded")
_ <- Check(true, "This passes")
yield "Execution completed"
}
result.map(r => assert(r == "Execution completed"))
}
}

"multiple checks" in run {
val result = Check.runChunk {
for
_ <- Check(true, "This should pass")
_ <- Check(false, "This should fail")
_ <- Check(true, "This should pass too")
_ <- Check(false, "This should also fail")
yield "Done"
}
result.map { case (failures, value) =>
assert(failures.size == 2)
assert(failures(0).message == "This should fail")
assert(failures(1).message == "This should also fail")
assert(value == "Done")
}
}

"checks with effects" in run {
val result = Env.run(5) {
Check.runChunk {
for
env <- Env.get[Int]
_ <- Check(env > 0, "Env should be positive")
_ <- Check(env < 10, "Env should be less than 10")
_ <- Check(env % 2 != 0, "Env should be odd")
yield env
}
}
result.map { case (failures, value) =>
assert(failures.isEmpty)
assert(value == 5)
}
}

"combining with other effects" in run {
val result = Var.run(0) {
Check.runChunk {
for
_ <- Check(true, "Initial check")
_ <- Var.update[Int](_ + 1)
v <- Var.get[Int]
_ <- Check(v == 1, "Var should be updated")
yield v
}
}
result.map { case (failures, value) =>
assert(failures.isEmpty)
assert(value == 1)
}
}

end CheckTest

0 comments on commit 0f2366a

Please sign in to comment.