Skip to content
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

data: Rename Maybe.Defined/Empty to Present/Absent and export them to the kyo package #734

Merged
merged 6 commits into from
Oct 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 18 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,12 @@ In Kyo, computations are expressed via the infix type `<`, known as "Pending". I
```scala
import kyo.*

// 'Int' pending 'Abort[Maybe.Empty]'
Int < Abort[Maybe.Empty]
// 'Int' pending 'Abort[Absent]'
// 'Absent' is Kyo's equivalent of 'None' via the 'Maybe' type
Int < Abort[Absent]

// 'String' pending 'Abort[Maybe.Empty]' and 'IO'
String < (Abort[Maybe.Empty] & IO)
// 'String' pending 'Abort[Absent]' and 'IO'
String < (Abort[Absent] & IO)
```

> Note: The naming convention for effect types is the plural form of the functionalities they manage.
Expand Down Expand Up @@ -2616,8 +2617,8 @@ import kyo._
// Create a 'Maybe' value
val a: Maybe[Int] = Maybe(42)

// 'Maybe.empty' represents the absence of a value
val b: Maybe[Int] = Maybe.empty[Int]
// 'Absent' represents the absence of a value
val b: Maybe[Int] = Absent

// 'Maybe.when' conditionally creates a 'Maybe' value
val c: Maybe[Int] = Maybe.when(true)(42)
Expand Down Expand Up @@ -2680,6 +2681,12 @@ val s: Maybe[Int] = for {
// Nesting 'Maybe' values
val nested: Maybe[Maybe[Int]] = Maybe(Maybe(42))
val flattened: Maybe[Int] = nested.flatten

// Pattern matching with 'Present' and 'Absent'
val result: String =
flattened match
case Present(value) => s"Value: $value"
case Absent => "No value"
```

### Duration: Time Representation
Expand Down Expand Up @@ -3206,7 +3213,7 @@ import kyo.*

// An example computation with
// nested effects
val a: Int < IO < Abort[Maybe.Empty] =
val a: Int < IO < Abort[Absent] =
Abort.get(Some(IO(1)))

// Can't handle a effects of a
Expand All @@ -3226,15 +3233,15 @@ Kyo performs checks at compilation time to ensure that nested effects are not us
```scala
import kyo.*

// def test[T](v: T < Abort[Maybe.Empty]) =
// def test[T](v: T < Abort[Absent]) =
// Abort.run(v)
// Compilation failure:
// Method doesn't accept nested Kyo computations.
// Cannot prove 'T' isn't nested. This error can be reported an unsupported pending effect is passed to a method. If that's not the case, provide an implicit evidence 'kyo.Flat[T]'.

// It's possible to provide an implicit
// evidence of `Flat` to resolve
def test[T](v: T < Abort[Maybe.Empty])(using Flat[T]) =
def test[T](v: T < Abort[Absent])(using Flat[T]) =
Abort.run(v)
```

Expand Down Expand Up @@ -3310,15 +3317,15 @@ IO.Unsafe.run { // Handles IO

### Failure conversions

One notable departure from the ZIO API worth calling out is a set of combinators for converting between failure effects. Whereas ZIO has a single channel for describing errors, Kyo has different effect types that can describe failure in the basic sense of "short-circuiting": `Abort` and `Choice` (an empty `Seq` being equivalent to a short-circuit). `Abort[Maybe.Empty]` can also be used like `Choice` to model short-circuiting an empty result. It's useful to be able to move between these effects easily, so `kyo-combinators` provides a number of extension methods, usually in the form of `def effect1ToEffect2`.
One notable departure from the ZIO API worth calling out is a set of combinators for converting between failure effects. Whereas ZIO has a single channel for describing errors, Kyo has different effect types that can describe failure in the basic sense of "short-circuiting": `Abort` and `Choice` (an empty `Seq` being equivalent to a short-circuit). `Abort[Absent]` can also be used like `Choice` to model short-circuiting an empty result. It's useful to be able to move between these effects easily, so `kyo-combinators` provides a number of extension methods, usually in the form of `def effect1ToEffect2`.

Some examples:

```scala
val abortEffect: Int < Abort[String] = ???

// Converts failures to empty failure
val maybeEffect: Int < Abort[Maybe.Empty] = abortEffect.abortToEmpty
val maybeEffect: Int < Abort[Absent] = abortEffect.abortToEmpty

// Converts empty failure to a single "choice" (or Seq)
val choiceEffect: Int < Choice = maybeEffect.emptyAbortToChoice
Expand Down
53 changes: 26 additions & 27 deletions kyo-combinators/shared/src/main/scala/kyo/Combinators.scala
Original file line number Diff line number Diff line change
Expand Up @@ -226,11 +226,10 @@ extension [A, S](effect: A < S)
* @param condition
* The condition to check
* @return
* A computation that produces the result of this computation wrapped in Maybe.Defined if the condition is satisfied, or Maybe.Empty
* if not
* A computation that produces the result of this computation wrapped in Present if the condition is satisfied, or Absent if not
*/
def when[S1](condition: => Boolean < S1)(using Frame): Maybe[A] < (S & S1) =
condition.map(c => if c then effect.map(Maybe.Defined.apply) else Maybe.Empty)
condition.map(c => if c then effect.map(Present.apply) else Absent)

/** Performs this computation catching any Throwable in an Abort[Throwable] effect.
*
Expand All @@ -248,15 +247,15 @@ extension [A, S](effect: A < S)
def tap[S1](f: A => Any < S1)(using Frame): A < (S & S1) =
effect.map(a => f(a).as(a))

/** Performs this computation unless the given condition holds, in which case it returns an Abort[Maybe.Empty] effect.
/** Performs this computation unless the given condition holds, in which case it returns an Abort[Absent] effect.
*
* @param condition
* The condition to check
* @return
* A computation that produces the result of this computation with Abort[Maybe.Empty] effect
* A computation that produces the result of this computation with Abort[Absent] effect
*/
def unless[S1](condition: Boolean < S1)(using Frame): A < (S & S1 & Abort[Maybe.Empty]) =
condition.map(c => if c then Abort.fail(Maybe.Empty) else effect)
def unless[S1](condition: Boolean < S1)(using Frame): A < (S & S1 & Abort[Absent]) =
condition.map(c => if c then Abort.fail(Absent) else effect)

/** Ensures that the specified finalizer is executed after this effect, whether it succeeds or fails. The finalizer will execute when
* the Resource effect is handled.
Expand Down Expand Up @@ -303,19 +302,19 @@ extension [A, S, E](effect: A < (Abort[E] & S))

def someAbortToChoice[E1 <: E](using Frame): SomeAbortToChoiceOps[A, S, E, E1] = SomeAbortToChoiceOps(effect)

/** Translates the Abort[E] effect to an Abort[Maybe.Empty] effect in case of failure.
/** Translates the Abort[E] effect to an Abort[Absent] effect in case of failure.
*
* @return
* A computation that produces the result of this computation with the Abort[E] effect translated to Abort[Maybe.Empty]
* A computation that produces the result of this computation with the Abort[E] effect translated to Abort[Absent]
*/
def abortToEmpty(
using
ct: SafeClassTag[E],
tag: Tag[E],
flat: Flat[A]
)(using Frame): A < (S & Abort[Maybe.Empty]) =
)(using Frame): A < (S & Abort[Absent]) =
effect.handleAbort.map {
case Result.Fail(_) => Abort.fail(Maybe.Empty)
case Result.Fail(_) => Abort.fail(Absent)
case Result.Panic(e) => throw e
case Result.Success(a) => a
}
Expand Down Expand Up @@ -398,36 +397,36 @@ extension [A, S, E](effect: A < (Abort[E] & S))
.map(_.fold(e => throw e.getFailure)(identity))
end extension

extension [A, S, E](effect: A < (Abort[Maybe.Empty] & S))
extension [A, S, E](effect: A < (Abort[Absent] & S))

/** Handles the Abort[Maybe.Empty] effect and returns its result as a `Maybe[A]`.
/** Handles the Abort[Absent] effect and returns its result as a `Maybe[A]`.
*
* @return
* A computation that produces the result of this computation with the Abort[Maybe.Empty] effect handled
* A computation that produces the result of this computation with the Abort[Absent] effect handled
*/
def handleEmptyAbort(using Flat[A], Frame): Maybe[A] < S =
Abort.run[Maybe.Empty](effect).map {
case Result.Fail(_) => Maybe.Empty
Abort.run[Absent](effect).map {
case Result.Fail(_) => Absent
case Result.Panic(e) => throw e
case Result.Success(a) => Maybe.Defined(a)
case Result.Success(a) => Present(a)
}

/** Translates the Abort[Maybe.Empty] effect to a Choice effect.
/** Translates the Abort[Absent] effect to a Choice effect.
*
* @return
* A computation that produces the result of this computation with the Abort[Maybe.Empty] effect translated to Choice
* A computation that produces the result of this computation with the Abort[Absent] effect translated to Choice
*/
def emptyAbortToChoice(using Flat[A], Frame): A < (S & Choice) =
effect.someAbortToChoice[Maybe.Empty]()
effect.someAbortToChoice[Absent]()

/** Handles the Abort[Maybe.Empty] effect translating it to an Abort[E] effect.
/** Handles the Abort[Absent] effect translating it to an Abort[E] effect.
*
* @return
* A computation that produces the result of this computation with the Abort[Maybe.Empty] effect translated to Abort[E]
* A computation that produces the result of this computation with the Abort[Absent] effect translated to Abort[E]
*/
def emptyAbortToFailure(failure: => E)(using Flat[A], Frame): A < (S & Abort[E]) =
for
res <- effect.handleSomeAbort[Maybe.Empty]()
res <- effect.handleSomeAbort[Absent]()
yield res match
case Result.Fail(_) => Abort.get(Result.Fail(failure))
case Result.Success(a) => Abort.get(Result.success(a))
Expand Down Expand Up @@ -456,10 +455,10 @@ end SomeAbortToChoiceOps

class SomeAbortToEmptyOps[A, S, E, E1 <: E](effect: A < (Abort[E] & S)) extends AnyVal:

/** Handles the Abort[E] effect translating it to an Abort[Maybe.Empty] effect.
/** Handles the Abort[E] effect translating it to an Abort[Absent] effect.
*
* @return
* A computation that produces the result of this computation with Abort[Maybe.Empty] effect
* A computation that produces the result of this computation with Abort[Absent] effect
*/
def apply[ER]()(
using
Expand All @@ -469,9 +468,9 @@ class SomeAbortToEmptyOps[A, S, E, E1 <: E](effect: A < (Abort[E] & S)) extends
reduce: Reducible[Abort[ER]],
flat: Flat[A],
frame: Frame
): A < (S & reduce.SReduced & Abort[Maybe.Empty]) =
): A < (S & reduce.SReduced & Abort[Absent]) =
Abort.run[E1](effect.asInstanceOf[A < (Abort[E1 | ER] & S)]).map {
case Result.Fail(_) => Abort.get(Result.Fail(Maybe.Empty))
case Result.Fail(_) => Abort.get(Result.Fail(Absent))
case p @ Result.Panic(e) => Abort.get(p.asInstanceOf[Result[Nothing, Nothing]])
case s @ Result.Success(a) => Abort.get(s.asInstanceOf[Result[Nothing, A]])
}
Expand Down
12 changes: 6 additions & 6 deletions kyo-combinators/shared/src/main/scala/kyo/Constructors.scala
Original file line number Diff line number Diff line change
Expand Up @@ -156,24 +156,24 @@ extension (kyoObject: Kyo.type)
def fromEither[E, A](either: Either[E, A])(using Frame): A < Abort[E] =
Abort.get(either)

/** Creates an effect from an Option[A] and handles None to Abort[Maybe.Empty].
/** Creates an effect from an Option[A] and handles None to Abort[Absent].
*
* @param option
* The Option[A] to create an effect from
* @return
* An effect that attempts to run the given effect and handles None to Abort[Maybe.Empty].
* An effect that attempts to run the given effect and handles None to Abort[Absent].
*/
def fromOption[A](option: Option[A])(using Frame): A < Abort[Maybe.Empty] =
def fromOption[A](option: Option[A])(using Frame): A < Abort[Absent] =
Abort.get(option)

/** Creates an effect from a Maybe[A] and handles Maybe.Empty to Abort[Maybe.Empty].
/** Creates an effect from a Maybe[A] and handles Absent to Abort[Absent].
*
* @param maybe
* The Maybe[A] to create an effect from
* @return
* An effect that attempts to run the given effect and handles Maybe.Empty to Abort[Maybe.Empty].
* An effect that attempts to run the given effect and handles Absent to Abort[Absent].
*/
def fromMaybe[A](maybe: Maybe[A])(using Frame): A < Abort[Maybe.Empty] =
def fromMaybe[A](maybe: Maybe[A])(using Frame): A < Abort[Absent] =
Abort.get(maybe)

/** Creates an effect from a Result[E, A] and handles Result.Failure[E] to Abort[E].
Expand Down
32 changes: 16 additions & 16 deletions kyo-combinators/shared/src/test/scala/kyo/AbortCombinatorTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,16 @@ class AbortCombinatorTest extends Test:

"should construct from option" in {
val effect = Kyo.fromOption(None)
assert(Abort.run[Maybe.Empty](effect).eval.failure.get == Maybe.Empty)
assert(Abort.run[Absent](effect).eval.failure.get == Absent)
val effect1 = Kyo.fromOption(Some(1))
assert(Abort.run[Maybe.Empty](effect1).eval.getOrElse(-1) == 1)
assert(Abort.run[Absent](effect1).eval.getOrElse(-1) == 1)
}

"should construct from maybe" in {
val effect = Kyo.fromMaybe(Maybe.Empty)
assert(Abort.run[Maybe.Empty](effect).eval.failure.get == Maybe.Empty)
val effect1 = Kyo.fromMaybe(Maybe.Defined(1))
assert(Abort.run[Maybe.Empty](effect1).eval.getOrElse(-1) == 1)
val effect = Kyo.fromMaybe(Absent)
assert(Abort.run[Absent](effect).eval.failure.get == Absent)
val effect1 = Kyo.fromMaybe(Present(1))
assert(Abort.run[Absent](effect1).eval.getOrElse(-1) == 1)
}

"should construct from a throwing block" in {
Expand Down Expand Up @@ -97,25 +97,25 @@ class AbortCombinatorTest extends Test:
"should convert all abort to empty" in {
val failure: Int < Abort[String] =
Abort.fail("failure")
val failureEmpty: Int < Abort[Maybe.Empty] = failure.abortToEmpty
val handledFailureEmpty = Abort.run[Maybe.Empty](failureEmpty)
assert(handledFailureEmpty.eval == Result.Fail(Maybe.Empty))
val success: Int < Abort[String] = 23
val successEmpty: Int < Abort[Maybe.Empty] = success.abortToEmpty
val handledSuccessEmpty = Abort.run[Any](successEmpty)
val failureEmpty: Int < Abort[Absent] = failure.abortToEmpty
val handledFailureEmpty = Abort.run[Absent](failureEmpty)
assert(handledFailureEmpty.eval == Result.Fail(Absent))
val success: Int < Abort[String] = 23
val successEmpty: Int < Abort[Absent] = success.abortToEmpty
val handledSuccessEmpty = Abort.run[Any](successEmpty)
assert(handledSuccessEmpty.eval == Result.Success(23))
}

"should convert some abort to empty" in {
val failure: Int < Abort[String | Boolean | Double | Int] =
Abort.fail("failure")
val failureEmpty: Int < Abort[Maybe.Empty | Boolean | Double | Int] =
val failureEmpty: Int < Abort[Absent | Boolean | Double | Int] =
failure.someAbortToEmpty[String]()
val handledFailureEmpty = Choice.run(failureEmpty)
val handledFailureAbort = Abort.run[Any](handledFailureEmpty)
assert(handledFailureAbort.eval == Result.fail(Maybe.Empty))
assert(handledFailureAbort.eval == Result.fail(Absent))
val success: Int < Abort[String | Boolean | Double | Int] = 23
val successEmpty: Int < (Abort[Maybe.Empty | Boolean | Double | Int]) =
val successEmpty: Int < (Abort[Absent | Boolean | Double | Int]) =
success.someAbortToEmpty[String]()
val handledSuccessEmpty = Abort.run[Any](successEmpty)
assert(handledSuccessEmpty.eval == Result.success(23))
Expand Down Expand Up @@ -282,7 +282,7 @@ class AbortCombinatorTest extends Test:
"should convert some abort to empty" in {
val effect: Int < Abort[String | Boolean] = Abort.fail("error")
val emptyEffect = effect.someAbortToEmpty[String]()
assert(Abort.run[Any](emptyEffect).eval == Result.fail(Maybe.Empty))
assert(Abort.run[Any](emptyEffect).eval == Result.fail(Absent))

val effect2: Int < Abort[String | Boolean] = 42
val emptyEffect2 = effect2.someAbortToEmpty[String]()
Expand Down
16 changes: 8 additions & 8 deletions kyo-combinators/shared/src/test/scala/kyo/ConstructorsTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -43,24 +43,24 @@ class ConstructorsTest extends Test:
val someEffect = Kyo.fromOption(Some(42))
val noneEffect = Kyo.fromOption(None)

val someResult = Abort.run[Maybe.Empty](someEffect).eval
val noneResult = Abort.run[Maybe.Empty](noneEffect).eval
val someResult = Abort.run[Absent](someEffect).eval
val noneResult = Abort.run[Absent](noneEffect).eval

assert(someResult == Result.success(42))
assert(noneResult == Result.fail(Maybe.Empty))
assert(noneResult == Result.fail(Absent))
}
}

"fromMaybe" - {
"should create an effect from Maybe[A]" in {
val definedEffect = Kyo.fromMaybe(Maybe.Defined(42))
val emptyEffect = Kyo.fromMaybe(Maybe.Empty)
val definedEffect = Kyo.fromMaybe(Present(42))
val emptyEffect = Kyo.fromMaybe(Absent)

val definedResult = Abort.run[Maybe.Empty](definedEffect).eval
val emptyResult = Abort.run[Maybe.Empty](emptyEffect).eval
val definedResult = Abort.run[Absent](definedEffect).eval
val emptyResult = Abort.run[Absent](emptyEffect).eval

assert(definedResult == Result.success(42))
assert(emptyResult == Result.fail(Maybe.Empty))
assert(emptyResult == Result.fail(Absent))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ class EffectCombinatorTest extends Test:
val getState = IO(state)
val effectWhen = (toggleState *> getState).when(getState)
effectWhen.map { handledEffectWhen =>
assert(handledEffectWhen == Maybe.Empty)
assert(handledEffectWhen == Absent)
}
}
"condition is true" in run {
Expand All @@ -128,7 +128,7 @@ class EffectCombinatorTest extends Test:
val getState = IO(state)
val effectWhen = (toggleState *> getState).when(getState)
effectWhen.map { handledEffectWhen =>
assert(handledEffectWhen == Maybe.Defined(false))
assert(handledEffectWhen == Present(false))
}
}
}
Expand All @@ -141,7 +141,7 @@ class EffectCombinatorTest extends Test:
effect
}
}.map { result =>
assert(result == Result.fail(Maybe.Empty))
assert(result == Result.fail(Absent))
}
}
"condition is false" in run {
Expand Down
4 changes: 2 additions & 2 deletions kyo-core/jvm/src/main/scala/kyo/Path.scala
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,8 @@ class Path private (val path: List[String]) derives CanEqual:
Resource.acquireRelease(acquire)(release).map { res =>
readOnce(res).map { state =>
Loop(state) {
case Maybe.Empty => Loop.done(Emit.Ack.Stop)
case Maybe.Defined(content) =>
case Absent => Loop.done(Emit.Ack.Stop)
case Present(content) =>
Emit.andMap(writeOnce(content)) {
case Emit.Ack.Stop => Loop.done(Emit.Ack.Stop)
case _ => readOnce(res).map(Loop.continue(_))
Expand Down
1 change: 0 additions & 1 deletion kyo-core/shared/src/main/scala/kyo/Async.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package kyo

import java.util.concurrent.atomic.AtomicInteger
import kyo.Maybe.Empty
import kyo.Result.Panic
import kyo.Tag
import kyo.internal.FiberPlatformSpecific
Expand Down
Loading
Loading