CLOVIS
05/14/2023, 12:12 PMsealed class ParsingError {
object InvalidFormat : ParsingError()
object InvalidArguments : ParsingError()
}
fun parse(…): Either<ParsingError, SomeType> = TODO()
sealed class ComputationError {
data class ParsingError(val error: ParsingError) : ComputationError()
object SomeOtherError : ComputationError()
}
fun computation(…): Either<ComputationError, SomeType> = either {
val a = parse(…)
.mapLeft(ComputationError::ParsingError)
.bind()
…
}
The .mapLeft().bind()
is quite verbose and appears a lot… is there some nice API I'm missing to simplify it?
It seems it would be even worse with context receivers, because I would be forced to add an either {}
block to all sub-function calls, defeating the purposeYoussef Shoaib [MOD]
05/14/2023, 1:18 PMcontext(Raise<ParsingError>)
fun parse(…): SomeType = TODO()
context(Raise<ComputationError>)
fun computation(…): SomeType {
val a = recover({parse(…)}) {
raise(ComputationError.ParsingError(it)
}
…
}
It allows you to call parse
while handling its error by raising another error. You could also do a similar thing with effect
or either
, but why over complicate things? recover
is made to do exactly this, and it allows you to sometimes treat the error as success if you really want toCLOVIS
05/14/2023, 1:19 PMYoussef Shoaib [MOD]
05/14/2023, 1:19 PMparse().bind()
CLOVIS
05/14/2023, 1:20 PMrecover({ parse(…).bind() }) {
raise(ComputationError.ParsingError(it))
}.bind()
Youssef Shoaib [MOD]
05/14/2023, 1:20 PMparse
in your case will return an Either, hence why we need to bind it inside recover
, but afterwards recover
will just return the value itselfCLOVIS
05/14/2023, 1:21 PMrecover
overload that always raised it would be nicer:
recover({ field.validate().bind() }, Form.Failures::InvalidImport)
IMO that looks close to optimal for this caseYoussef Shoaib [MOD]
05/14/2023, 1:29 PMpublic inline fun <OuterError, Error, A> Raise<OuterError>.withMappedErrors(
@BuilderInference errorMapper: (error: Error) -> OuterError,
@BuilderInference block: Raise<Error>.() -> A,
): A = fold(block, { throw it }, { raise(errorMapper(it)) }, ::identity)
Which makes the code like this:
fun computation(…): Either<ComputationError, SomeType> = either {
withMappedErrors(ComputationError::ParsingError) {
val a = parse(…).bind()
…
}
}
And the bind goes away if you use context receivers.
One thing I'm not sure of is if the @RaiseDsl
DslContext
annotation might prevent you from raising ComputationError
inside of the withMappedErrors
. If Intellij does complain, you can either suppress the warning, or (with context receivers) make withMappedErrors re-apply the outer Raise context like this:
public inline fun <OuterError, Error, A> Raise<OuterError>.withMappedErrors(
@BuilderInference errorMapper: (error: Error) -> OuterError,
@BuilderInference block: context(Raise<Error>, Raise<OuterError>) () -> A,
): A = fold(block, { throw it }, { raise(errorMapper(it)) }, ::identity)
CLOVIS
05/14/2023, 1:31 PMwithMappedErrors
function looks great. Should I create a feature request for it?Youssef Shoaib [MOD]
05/14/2023, 1:34 PMResultDsl
, so it might have to wait until context receivers are stableCLOVIS
05/14/2023, 1:53 PM