Richard Schielek
11/29/2023, 2:54 PMAdamW
11/29/2023, 4:47 PMI feel like it is an unnecessary price to pay if I use Either only for error handling. (For other cases I have sealed data types and would never long for Either.)
I’m not sure I understand this part. My first instinct is that if the semantics of
Either
don’t fit your use in error handling, why not use a sealed hierarchy more specific to your domain instead and work with raises just the same? 🤔 Do you have a more specific example?AdamW
11/29/2023, 5:06 PMCLOVIS
11/29/2023, 5:08 PMforcing error/result semantics doesn’t seem right
You say that, but since 1.2 that ship has sailed. The documentation page is just called “functional error handling”.
AdamW
11/29/2023, 5:09 PMCLOVIS
11/29/2023, 5:21 PMMervyn McCreight
11/29/2023, 5:55 PMRenaming left/right equally breaks convention for those familiar with Either in other languages. Technically, using it for error handling is just an arbitrary application, so forcing error/result semantics doesn’t seem rightThat was my first thought about this topic as well. It fits to what is written in the documentation of
Raise
:
The Raise DSL allows you to work with logical failures of type Error. A logical failure does not necessarily mean that the computation has failed, but that it has stopped or short-circuited. (…)But I can understand that the term
Raise
is something that is tightly bound to error handling in other concepts or languages.
Maybe it’d be good idea to find a name that expresses the general concept more, because error handling is just one example use-case where short-circuiting as a general pattern is useful most of the time.Mervyn McCreight
11/29/2023, 5:57 PMit will be trivial to create your own custom types that behave like EitherNot sure If I got you right, but I feel like it shouldn’t be the encouraged way to reinvent the wheel 🤔
Javier
11/29/2023, 6:03 PMRichard Schielek
11/29/2023, 9:29 PMRichard Schielek
11/29/2023, 9:33 PMwhy not use a sealed hierarchy more specific to your domain instead and work with raises just the same? 🤔I don't understand this part. Could you give me a code example for this?
Mervyn McCreight
11/29/2023, 9:36 PMThere are several solutions to the problem. Renaming would probably be the most drastic solution and also not my favorite one. But what about an alias or another type next to either, that behaves practically the same but has semantics solely for error handling. (Result is unfortunately already taken)Wouldn’t that be what
Validated
once was before it got deprecated? 🤔AdamW
11/30/2023, 2:10 AMMathError
into a result hierarchy of sorts which includes a Either.Right
more suited to your domain. Something like:
sealed interface MathResult {
data class Success(…) : MathResult
sealed interface Failure : MathResult {
…
}
}
With the raises becoming Raise<MathResult.Failure>
instead.
This may or may not be useful to you, in the end Either offers better ergonomics. You can always invoke such function in a either builder if needed though.Richard Schielek
11/30/2023, 8:23 AMEither
fulfills all my needs and my pain comes only from some of its semantics. Either<MathError, BigDecimal>
is just as understandable as MathResult
, maybe even better. I am just concerned about the left/right semantics. Left and right are sementically equal, but in all of my code they are logically unequal. I assume we can agree that Either<BigDecimal, MathError>
is not an option. I could of course just fix the problem for me by introducing some extension functions like mapFailure
for mapLeft
.Richard Schielek
11/30/2023, 8:25 AMEither
is probably the most controversial topic. Are there any thoughts on getOrRaise
vs. bind
and parallelMap
vs. parMap
?CLOVIS
11/30/2023, 1:31 PMRaise
interface. Either
, and all other data types, are just representations of the result of executing a Raise
-based function—but they're not the actual code.
A function like:
context(Raise<Foo>)
fun bar(number: Int) {
…
}
never uses Either
nor any other data type from Arrow. The only place where these types are used is when you want to combine results / use them elsewhere, but there's really nothing forcing you to.
For example, if you added:
sealed class Outcome<out Failure, out Success> {
data class Failure<F>(val failure: F) : Outcome<F, Nothing>()
data class Success<S>(val success: S) : Outcome<Nothing, S>()
}
context(Raise<F, S>)
fun <F, S> Outcome<F, S>.bind(): S = when (this) {
is Failure -> raise(failure)
is Success -> success
}
fun <F, S> out(block: context(Raise<F>) () -> S): Outcome<F, S> =
fold(block, recover = { Outcome.Failure(it) }, transform = { Outcome.Success(it) })
and that's it, you can now use your own Outcome
type to represent the result of any Raise
operation. That's the power of Raise
and context receivers: we stopped encoding values, we now encode the "execution context", so the result can be represented anyway we want.
If you like this name, I recommend using my library (https://opensavvy.gitlab.io/pedestal/api-docs/state/index.html) which also adds intermediate/"unfinished" results with progress information. Currently, it's not as smooth as the above, because we don't have context receivers, but that's the goal.Javier
11/30/2023, 1:32 PM