There is a newer API available, that we've recently released that simplifies building such abstractions in a more convenient way. While also offering bigger performance benefits!
Here is an example for Option.
Translated to your domain:
First we create a function that maps Effect<ErrorResponse, A>
to the custom type. This is useful for when you'd write any other program/Effect
that might result in ErrorResponse
and you want to turn into your custom type.
public suspend fun <A> Effect<ErrorResponse, A>.toPoseidonRes(): PoseidonRes<A> =
fold({ it }) { SuccessResponse(it) }
Next we create some extra DSL sugar, so you can conveniently call bind
on your own type.
@JvmInline
public value class PoseidonResEffectScope(private val cont: EffectScope< ErrorResponse>) : EffectScope<ErrorResponse> {
override suspend fun <B> shift(r: None): B =
cont.shift(r)
suspend fun <T> PoseidonRes<T>.bind(): T = when (this) {
is SuccessResponse -> this.response
is ErrorResponse -> shift(this)
}
public suspend fun ensure(value: Boolean): Unit =
ensure(value) { ErrorResponse }
}
@OptIn(ExperimentalContracts::class)
public suspend fun <B> PoseidonResEffectScope.ensureNotNull(value: B?): B {
contract { returns() implies (value != null) }
return ensureNotNull(value) { ErrorResponse }
}
And finally we create a DSL function, that enables the above defined additional DSL syntax.
suspend fun <A> posRes(
block: suspend PoseidonResEffectScope.() -> A
): PoseidonRes<A> = effect<ErrorResponse, A> {
block(PoseidonResEffectScope(this))
}.toPoseidonRes()
Additional info:
With context receivers (and upcoming feature in Kotlin) we could simplify the above 2 code snippets a ton.
context(EffectScope<ErrorResponse>)
suspend fun <T> PoseidonRes<T>.bind(): T = when (this) {
is SuccessResponse -> this.response
is ErrorResponse -> shift(this)
}
suspend fun <A> posRes(
block: suspend EffectScope<ErrorResponse>.() -> A
): PoseidonRes<A> =
effect<ErrorResponse, A>(block).toPoseidonRes()
EDIT to answer additional questions from comment:
ensure
is a monadic function to check for invariants.
In pure-functional land type-refinement is often used instead to check invariants at compile-time, or force runtime checks like this. In Java, and Kotlin, people typically use if(condition) throw IllegalArgumentException(...)
.
ensure
replaces that pattern for a monadic equivalent, and ensureNotNull
does the same but it leverages Kotlin contracts to smart-cast the passed value to non-null
.
Yes, you could change the signatures to:
suspend fun <A> posRes(
block: suspend EffectScope<PoseidonRes<A>>.() -> A
): PoseidonRes<A> =
effect<PoseidonRes<A>, A>(block)
.fold({ res: PoseidonRes<A> -> res }) { a -> SuccessResponse(a) }
This signature is valid, and you don't "lose anything", and there can be some good use-cases for this. For example if you finish early, and want to skip the remaining logic. Finishing early doesn't mean failure perse.
It can for example also mean that you returned 500 Internal Server
to the user, and thus handled the result already. By-passing any additional computation since a response was already send.
- Currently there is no way to skip calling
bind
. At least not stably for Kotlin MPP. bind
is the equivalent of <-
in Haskells do-notation or Scala's for comprehensions. You can leverage experimental context receivers on the JVM to remove the need for calling bind
though.
context(EffectScope<ErrorResponse>)
suspend fun one(): Int {
shift(ErrorResponse) // We have access to shift here
1
}
context(EffectScope<ErrorResponse>)
suspend otherCode(): Int = one() + one()
More details on this pattern can be found here:
Currently you have to explicitly enable the experimental feature, and it only works for the JVM:
withType<KotlinCompile>().configureEach {
kotlinOptions {
freeCompilerArgs = freeCompilerArgs + "-Xcontext-receivers"
}
}