I tend to write a lot of code like the following (...
# arrow
c
I tend to write a lot of code like the following (sorry for the lack of imagination in naming):
Copy code
sealed 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 purpose
❤️ 1
y
Why not do this with context receivers:
Copy code
context(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 to
c
I'm on Kotlin/JS, I don't have context receivers…
y
Oops, well, the inner part with parse and raise still works, you'll just have to do
parse().bind()
c
So something like this?
Copy code
recover({ parse(…).bind() }) {
    raise(ComputationError.ParsingError(it))
}.bind()
y
No need for that last bind.
parse
in your case will return an Either, hence why we need to bind it inside
recover
, but afterwards
recover
will just return the value itself
c
Seems less readable to me 😕
The real code
I guess if there was a
recover
overload that always raised it would be nicer:
recover({ field.validate().bind() }, Form.Failures::InvalidImport)
IMO that looks close to optimal for this case
y
With context receivers though I think it makes more sense. Also, you can define something like this:
Copy code
public 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:
Copy code
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:
Copy code
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)
c
Actually, that
withMappedErrors
function looks great. Should I create a feature request for it?
y
Absolutely! I think it might be a useful pattern. It probably needs a better name though. It might also not work because of
ResultDsl
, so it might have to wait until context receivers are stable
c