-
Notifications
You must be signed in to change notification settings - Fork 27
Description
First, thanks a lot for this update! The possibility to create custom DSLs is amazing. That said, I'm having some issues using them.
I have a library which has its own Either
-like type, called Outcome
, except the left side must implement the Failure
interface. I'm trying to create a custom DSL for it so Arrow users can use it as if it was Either
:
interface Failure { … }
sealed class UserError : Failure {
object NotFound : UserError()
// …
}
sealed class Outcome<out F : Failure, out T> {
class OutcomeFailure<F : Failure>(val failure: F) : Outcome<F, Nothing>
class OutcomeSuccess<T>(val value: T) : Outcome<Nothing, T>
}
Declaring the DSL itself was quite easy following the documentation:
@JvmInline
value class OutcomeDSL<F : Failure>(private val raise: Raise<Outcome<F, Nothing>>) :
Raise<Outcome<F, Nothing>> by raise {
fun <T> Outcome<F, T>.bind(): T = when (this) {
is OutcomeFailure -> raise.raise(this)
is OutcomeSuccess -> value
}
}
@OptIn(ExperimentalTypeInference::class)
inline fun <F : Failure, T> out(@BuilderInference block: OutcomeDsl<F>.() -> T) : Outcome<F, T> =
recover(
block = { OutcomeSuccess(block(OutcomeDsl(this))) },
recover = ::identity,
)
In simple cases, it seems to work fine, however it quickly gets stuck on variance errors:
data class User(val name: String)
fun create(user: User?) = out<UserError, User> {
ensureNotNull(user) { UserError.NotFound } // Type mismatch, expected OutcomeFailure, found UserError.NotFound
TODO()
}
It seemed weird to me that it was expecting the OutcomeFailure
type instead (why is it a Raise<Outcome<F, Nothing>>
and not a Raise<F>
?), so I tried to rewrite it as follows:
@JvmInline
value class OutcomeDsl<F : Failure>(private val raise: Raise<F>) :
Raise<F> by raise {
fun <T> Outcome<F, T>.bind(): T = when (this) {
is OutcomeSuccess -> value
is OutcomeFailure -> raise.raise(failure)
}
}
@OptIn(ExperimentalTypeInference::class)
inline fun <F : Failure, T> out(@BuilderInference block: OutcomeDsl<F>.() -> T) : Outcome<F, T> =
recover(
block = { OutcomeSuccess(block(OutcomeDsl(this))) },
recover = { e: F -> OutcomeFailure(e) },
)
Note how:
OutcomeDsl.raise
is aRaise<F>
instead of aRaise<Outcome<F, Nothing>>
recover
'srecover
properly instantiates anOutcomeFailure
value, thus having a symmetry betweenblock
which instantiates a successful variant andrecover
which instantiates a failure variant
Was there an error in the documentation, or did I completely misunderstand the given example?
I was originally going to ask this on the Slack channel, but as the message grew I thought it would be better here… Is this the right place to ask such questions?