I continue to struggle with finding a good, go-to ...
# announcements
p
I continue to struggle with finding a good, go-to pattern for error types in kotlin šŸ˜• Say you have a hardware device, and you want to present a bunch of data about that device. Its serial number, its battery state, etc. Each one of these data requires issuing a command to the device and reading a response, and that command might fail. These values all have different types --- some are counters, some are strings, some are dates, etc. But they all have exactly the same failure semantics --- the device might return an error. Say, further, that all of these data accessors are defined on top of an underlying sendCommand function, which always returns a string or error. From the callerā€™s perspective, the fact that these methods are defined in terms of sendCommand() is an implementation detail ā€” they expect to be informed of how the device access failed (which is to say, the set of errors returned by the lower-level sendCommand is exactly the same as the set of errors returned by these field accessors by definition). Is the best possible pattern in this scenario to define a generic Result<T> type, and for every single field accessor, to write, e.g.,
Copy code
when (sendCommand(val result = command)) {
    is Result.Success<CommandResponse> -> Result.Success(getDerivedValue(result))
    is Result.SomeFailure<CommandResponse> -> Result.SomeFailure(result.message)
    is Result.OtherFailure<CommandResponse> -> Result.OtherFailure(result.message)
}
I canā€™t think of a better way, but this feels incredibly boilerplatey ā€” Iā€™d love to be able to just return the underlying errors (since thatā€™s what the caller will care about), but that doesnā€™t work since the generic type parameter is changing (even though the error subtypes donā€™t actually make use of the type parameter). Alternatively, I could just hide the error, and present these fields as properties that are null if the lookup failed. But then Iā€™ve robbed the caller of the ability to respond meaningfully to the failure. TL;DR: is there any good pattern for forwarding a lower-level error up the call stack, where appropriate? Because I can certainly think of cases where that seems very appropriate.
I suppose one option would be to have a separate DeviceError sealed type, and define my errors there and have the Result type refer to it. Iā€™ll do that.
j
You can also define your own
map()
function on the generic result sealed class, so you don't have to repeat this
when
for every different command
p
I was trying and trying and trying to figure out how to phrase that šŸ˜†
but I couldnā€™t figure out a way to have the base class know what type to return, when it needed to modify a value
ie, Iā€™d need to call myResult.map { successValue -> newSuccessValue }
and map would need to be able to package newSuccessValue up in a new Result.Success
and that works if I define my .map in Result
but if I want to have a bunch of Result types that all have the same operations defined on them (including .map), I canā€™t do that
since BaseResult wonā€™t know what subtypes are defined on FooResult and BleepResult
and wonā€™t know how and when to make a Success object
n
not sure I follow here
j
Why do you need a bunch of result types if the success object is generic?
Copy code
sealed class Result<out T> {
    data class Success<T>(val value: T): Result<T>() {
        override fun <U> map(transform: (T) -> U): Result<U> = Success(transform(value))
    }
    class SomeError<T>(val message: String) : Result<T>() {
        override fun <U> map(transform: (T) -> U): Result<U> = SomeError(message)
    }
    class OtherError<T>(val throwable: Throwable) : Result<T>() {
        override fun <U> map(transform: (T) -> U): Result<U> = OtherError(throwable)
    }

    abstract fun <U> map(transform: (T) -> U): Result<U>
}
n
Yeah. You can look at the Result type that kotlin provides although it's not supposed to be used in user code
but that will give you an idea
the major issue with
Result<T>
is that you want to at least have the option to also be generic on the type of the error, IMHO
j
It depends on the use case, but if you only have a small well known number of possible errors like I did above, you don't really need a generic there.
n
I think nice error handling in kotlin is possible, the issues are a) they don't provide something out of the box, so you gotta roll your own or look at third party libs, as a result it's not universally used, b) there isn't any super-nice syntactic sugar for working with such things
p
@Joffrey because I want to constrain the set of errors that can be returned from a given function.
j
Then maybe you can play with sealed interfaces on your error types
n
@Joffrey Okay, I see now in your example though that it's not really a generic Result type, it specifically mentions a couple of specific kind of errors
p
unfortunately weā€™re still on kotlin 1.4
I donā€™t think we have sealed interfaces yet
n
@Patrick Ramsey It sounds like what you want is to basically bubble up all the static types that any function can produce
j
@Nir yep that was my point, in some scenarios you don't need a generic error
n
yeah, but then you're re-implementing map and things like that
p
I apologize for responding slowly --- the conversation is going faster than Iā€™m able to keep up šŸ˜† give me a second.
j
@Patrick Ramsey ah, yes then it will be more annoying, because sealed interfaces are introduced in 1.5
p
So @Nir, I want to bubble up specific, constrained types. Basically, if this were java or another jvm language with checked exceptions, sendCommand() would throw its errors, and all the field accessors would explicitly state that they they throw those errors. Because the set of errors that they all can return is defined by the set of errors the device can produce.
So Iā€™d explicitly like to have the Result type constrain the field accessors to returning either a value or just that set of errors.
j
yeah, but then you're re-implementing map and things like that
This is a reasonable price if the number of error types is small and/or if they encapsulate more than one property. For multi-property errors, you would have to have more wrapping if you want a single type parameter
p
the only way I can think to built the set of possible errors into the return type is to have a custom Result type for each place that might need to return errors
but then, in order to have shared behavior between all these result types, they need to inherit from a base class --- one which doesnā€™t know about the various subtypes of each Result type
lemme catch up a bit now šŸ˜†
j
@Patrick Ramsey I think I understand better your problem now, thanks for the details
p
and @Joffrey, I really donā€™t like the idea of reimplementing map everywhere
the size of these sealed classes is already bigger than the equivalent exceptions Iā€™d have declared in another language
having boilerplate methods attached to each one is just asking for another developer to screw up and omit it or write a bug in one.
I think one of the things Iā€™ve really been struggling with is, thereā€™s a lot of shared behavior between Result types that would normally be convenient to add with inheritence
but because of the nature of sealed classes and the way theyā€™re being used here, you canā€™t really do that.
except by sort-of cheating (my Success types pass an isSuccess predicate to the superclass constructor, so it can do things that are conditioned on success)
@Patrick RamseyĀ I think I understand better your problem now, thanks for the details
And thank you!
Iā€™m afraid Iā€™ve got to disappear for an hour, but I will check in later
šŸ‘Œ 1
thanks for thinking about it! Helps enormously to have other more informed brains to bounce things off šŸ™‚
n
I think we had this convo before
p
it was in a different context, with a different problem to solve
n
What you're doing results in reinventing checked exceptions
p
but yes, this is the piece of the language Iā€™ve been having the most trouble with
n
And nobody wants that
p
wellā€¦ okay, but
n
I mean if you like it, you like it, just telling you, this isn't how result types work in other languages typically
p
I feel like I keep hearing, ā€œdonā€™t do thing xā€, but not much ā€œdo thing y insteadā€. The problem statement is, ā€œI have a bunch of methods that all are capable of failing in the exact same wayā€ ā€œWhich is the same as the set of ways that the underlying method can failā€ ā€œIs there any way to model this without boilerplate?ā€
n
Usually what you want is to either handle errors immediately in a strongly.typed way or bubble things up in a type erased way
p
If the question itself is unreasonable, then I need help finding the right question to ask
n
It's not unreasonable
What I'd probably do is have some kind of interface that resembles the standard library result<T>
And then have a class that inherits from it, Outcome<T, E>
j
"that all are capable of failing in the exact same way" -> I thought the subset of errors was different for each method, isn't that the meat of the problem?
p
nono
itā€™s the exact same set of errors for all these methods.
n
That way, low level functions return a strongly typed Outcome<T, E>
p
but I want to solve Result types globally for my lbirary
in a consistent way.
so Iā€™m not reinventing the wheel each time
n
Just have something that is generic on both the success type and the error type then
p
and I just realized I donā€™t actually have to go šŸ˜„ my weekly meeting isnā€™t happening this week
j
Yeah this is pretty easy with a simple type with 2 generic params
n
The real meat though I also expected to be, how do you know deal with bubbling up errors with different error types
p
@Nir yeah.
n
Hence my suggestion above
p
I suppose one option would be to have a separate DeviceError sealed type, and define my errors there and have the Result type refer to it.Ā Iā€™ll do that.
n
Gotcha
p
that notion had just occurred to me literally 5 minutes after asking the question, so I hadnā€™t thought it all the way through yet šŸ˜† Kind of annoyed at myself
but yeah, I think having Result<ValueT, ErrorT> might work. Would allow me to have only one result type and have the errors be local to where I need them
which is nicer anyway
n
And when a function foo calls two functions, bar1 and bar2
And bar1 and bar2 both return Results with different error types
What's your plan?
Will you just throw at that point?
p
To repackage the errors, as I would with exceptions generally hehe
the only reason to bubble these up directly is because the field accessors are such a thin layer on top of sendCommand.
n
So create a third error type and then write out functions converting it?
p
yes.
n
That will get super old super quickly for very low benefit, in most cases :-)
p
so what would you recommend? Now it sounds like you are advocating for checked exceptions haha
n
Either throw or use what I suggested above
j
I'm honestly not too keen on railway programming with results all the way, I find that it bloats the type system unnecessarily. But if you really want to go this road, there's a library for this
Result
type with 2 generic params, and nice operators to go with it: https://github.com/michaelbull/kotlin-result
p
@Joffrey I really donā€™t. Iā€™m not enjoying it so if thereā€™s a better pattern Iā€™m way open to it
n
Realistically in Kotlin specifically in the example I gave I'd probably throw
p
Iā€™m just trying to learn the best patterns for signalling error states to callers in kotlin
okay.
n
However what I wrote above also works pretty well
p
neat.
j
I usually handle errors early, or model specific result types for specific situations when it feels like it's more appropriate than an exception
p
@Joffrey This all has been an exercise in, ā€œI really want to follow the advice of, exceptions in kotlin are not for signaling error returnsā€
n
In my suggestion, Result<T> is a super type to Outcome<T, E>
p
but trying to figure out what the ā€œthen whatā€ is in various edge cases
@Joffrey nods
n
So, in the example discussed, if you are getting back outcomes with different error types you just return result
The benefit is that callers can see statically that it can fail
p
let me read back a bit --- I know Iā€™m misssing things, text is just going by fast
and I need to stop talking and start listening šŸ˜†
j
haha, don't worry
n
Th downside is this approach doesn't have language support like exceptions do
j
Well
when
expressions + smart cast is the language support for that
n
Yeah, it's still uglier though
There's no ? a la Rust for example
There's no support for propagation is what I should say, whereas obviously there is for exceptions
p
@Nir Re: The Result type that kotlin provides --- one thing about it that seems problematic is that its error case wraps an exception. Which seems like a weird use of exceptions. @Nir And yeah, your approach sounds like a good idea. @Joffrey sidenote: I really wish when ( ) { } put the type of the thing you are switching on in scope! It seems like the compiler ought to know that in
Copy code
when (someResult) {
    is Success -> ...
    is Error -> ....
 }
Success and Error are Result.Success and Result.Error, without having to explicitly specify it.
n
It wraps something that's throwable, which makes sense because there are convenience methods where you get the value or throw
You usually really want those methods
j
There's no support for propagation is what I should say, whereas obviously there is for exceptions
I see your point, thanks
n
Sometimes you may call a function that can fail with inputs such that you know it shouldn't ever fail, in that case you just want a way to unwrap the value
And obviously you still need to do *something*if it fails
p
@Nir sure! but that makes its utility a little limited
n
Hmm can you just write a dataclass that inherits throwable?
p
because itā€™s really meant for use with .runCatching
j
Sometimes you may call a function that can fail with inputs such that you know it shouldn't ever fail, in that case you just want a way to unwrap the value
I think that is the point of unchecked exceptions, precisely. It's about trusting that you're calling the methods with the correct arguments, and thus not bloating the type system and the code with error checking (and yet still be able to catch errors, should they arise)
p
Except that some errors are not the result of bad arguments
some are the result of external actors you have no control over, like services, or devices, or the user
and those expected failures are nice to have built into the function type signature
j
True, then it becomes a matter of what is "usual errors" and what is exceptional situations
p
and there, the answer is I think reasonably sealed classes! But as both of you have pointed out it would be nice if there was sugar/tools in the standard library to make that less boilerplatey.
yup
and if I were writing java, that would be the breakdown between checked and RuntimeExceptions
in kotlin, itā€™s sealed types vs exceptions
just want to know how to make things easier on myself šŸ˜†
Thank you @Nir, @Joffrey. I really appreciate your taking the time to talk this through
šŸ‘ 2
n
i was messing around a bit with writing result/outcome; it's doable but it's just kidn of annoying and requires a lot of compromises
type erasure is really painful in such ac ontext
p
sure is.
let me look at kotlin-result. No need to reinvent the wheel
eh. actually,
I suppose one option would be to have a separate DeviceError sealed type, and define my errors there and have the Result type refer to it.
made things embarrassingly easy. Iā€™m annoyed at myself
but kotlin-result looks interesting and Iā€™ll consider it for the future
it reads a lot like promise chaining which is kinda funny. Because thereā€™s nothing new under the sun
e
kotlin.Result isn't parameterized over the error type (which may or may not matter for you) and is used internally by Kotlin (so there's some bad corner cases when you try to use it in your own code, e.g. https://youtrack.jetbrains.com/issue/KT-44867)
nothing that can't be fundamentally fixed eventually, but if you use it in your own function returns, don't be too surprised if you run into a few issues