I still don’t understand the proper way of handlin...
# getting-started
l
I still don’t understand the proper way of handling recoverable error in Kotlin. eg : a function that could either return a Float, or an invalid result. In rust you have “Result”. Kotlin appear to have that as well, is it what i should use ? (java would use the infamous null). Link to an article would be appreciated.
l
mmm... it still rely on null except that it have the nice syntax sugar. 😕
but it jetbrains say that’s how you do it, i guess that’s how you do it
c
Another alternative is #arrow's Either
n
l
alright, for the use case i had in mind, i guess i’ll rely on null then. thank you.
p
Kotlin's nullable types would probably be similar to Rust's
Option
type. If that satisfies your use-case then that's perfect (i.e. you don't care why it failed, just that you had no result). Alternatively, although Kotlin does have a
Result<T>
type, it's mainly present to support the underlying coroutines framework. It captures a successful value of
T
or a
Throwable
, but is indiscriminate about the
Throwable
, capturing fatal/critical exceptions and interfering with coroutine cancellation. You're better off using #arrow's Either (as suggested by @CLOVIS) or possibly rolling your own
sealed class Result<Err, T>
if you do need to capture/handle the error cases.
The benefit of using #arrow here would be taking advantage of the
either { }
computation blocks that allow for a nicer mechanism for unwrapping
Either<L, R>
using `bind()`operator (the equivalent of Rust's
?
operator)
n
or possibly rolling your own
sealed class Result<Err, T>
if you do need to capture/handle the error cases
in this case I would recommend using the library mentioned above instead 😉
p
ah beat me to it 😉 I'd not used/seen it before having normally brought in arrow, but it seems to be exactly that.
🙂 1
👍🏻 1
e
in my experience, having a per-project
Copy code
sealed class FooResult<out T> {
    data class Success<out T>(val value: T) : FooResult<T>()
    data class Error(val error: FooError) : FooResult<Nothing>()
}
is good enough. it's only a little bit of boilerplate to add some operators and mapping functions for convenience
c
I agree with @ephemient. I like Arrow because it adds a lot of sugar over that sealed result type, which makes it more fun when using coroutines etc, but deep down that's pretty much what it is.
1
n
you don't care why it failed, just that you had no result
On longer term I realize in most cases that I need to know the cause of the failure as well. So nowdays I try to implement all functions (that can fail) with a Result<V, E> return type. My experience is pretty bad with exception-based error handling, so I try to switch to the more explicit error handling mechanisms :)
1
l
An exemple : a function that return the distance to the closest food source. But there might be no food source available. So it either return a “Distance” or “???“. it could be a “try” but it’s not really an exception, it could be a “null” because there is no distance to nothing. Or the A* path of an unreachable location.
p
In Rust i'd expect that to be a
Option<Distance>
and so I'd likely use
Distance?
in kotlin (unless it's being used specifically in an API that requires non-null values, in which case I'd use an
Option<T>
monad to wrap it)
l
yes in rust it would be Option
null would work but ... it’s null 😭
p
That's the theoretical beauty of
null
in Kotlin - it's surfaced in the type-system so no where near as scary as a null pointer in Java.
2
n
So it either return a “Distance” or “???“
As you wrote it is not an exception, and not even an error. It is just a normal case of the possible results of the function. You could simply write a sealed interface/class for the result type:
Copy code
sealed interface DistanceCalculationResult {

    data class Available(...): DistanceCalculationResult 

    object NotAvailable: DistanceCalculationResult 
}

fun calculateDistanceToFoodSource(...): DistanceCalculationResult
Or, if you treat the case when the food source is not available as an error, you can use
Result<V, E>
or the corresponding Arrow types:
Copy code
data class FoodSourceAvailable(...)

object FoodSourceNotAvailable

fun calculateDistanceToFoodSource(...): Result<FoodSourceAvailable, FoodSourceNotAvailable>
l
and so the return type of the function would be “DistanceCalculationResult” ?
r
it's surfaced in the type-system so no where near as scary as a null pointer in Java.
this,
null
is not as "scary" in Kotlin as it is in Java. The problem with null in Java is that it can surface anywhere, anytime, with no warning. But in Kotlin you would see its presence in type system, so returning
Distance?
is imho completely valid option in this case. If you do need more states then sealed classes are way to go, and if recoverable errors then either mentioned Result libraries, or roll your own sealed class Result
2
n
and so the return type of the function would be “DistanceCalculationResult” ?
In my first example, yes, and the return value can simply be processed using a `when`:
Copy code
...
when (calculateDistanceToFoodSource(...)) {
    is FoodSourceAvailable -> ...
    FoodSourceNotAvailable -> ...
}
l
ha perfect, this is exactly what i would like to do. And keep null as a possibility and it make more sense to do so.
it’s very easy to read too.
n
null
is not as "scary" in Kotlin as it is in Java.
+1 If you don't want/need to know the cause of the error then simply returning
null
may be a good solution. But as I wrote above, in most cases (in longer term) I usually need the cause of the error as well to handle recovery/logging/etc properly.
In my first example
For my second example, it would be similarly easy to use the
Result<FoodSourceAvailable, FoodSourceNotAvailable>
return value:
Copy code
calculateDistanceToFoodSource(...)
    .map {
        assertTrue(it is FoodSourceAvailable)
    }
    .mapError {
        assertTrue(it is FoodSourceNotAvailable)
    }
l
much less readable compared to when ->
🤔 1
n
To summarize, it is a matter of personal preference. As I wrote nowadays a starting to prefer the very explicit
Result<V, E>
return types because I think that writing software is getting more and more easier, but correctly handling failures and recover from them is still hard :)
p
I'd advocate against using
Option<T>
or in this case a
DistanceCalculationResult
with an effectively
None
value - when it comes to heavy usage you'd either need/want to provide lots of `map`/`flatMap` operators to help use it or ultimately end up with a
getOrNull
anyway (from my experience). The null-safe-call operator
?.
and elvis
?:
really help to write succinct and idiomatic code for these use-cases.
e
well, if you go all-in on the #arrow approach, that would be ok too… but yeah, I generally agree with that
n
much less readable compared to when ->
For more complex cases it has advantages, eg. when chaining function calls:
Copy code
calculateDistanceToFoodSource(...)
    .andThen { doSomethingWithTheFirstResult() }
    .andThen { doSomethingWithTheSecondResult() }
    .mapError { ... }
e
keep it no more complicated than necessary
Copy code
calculateDistanceToFoodSource(...)
    ?.doSomethingWithTheFirstResult()
    ?.doSomethingWithTheSecondResult()
    ?: error("...")
not always what fits, but when it does, use it
c
As another option, in the future Arrow 2.0 will allow for this kind of pattern:
Copy code
context(Raise<FoodSourceNotAvailable>)
fun calculateDIstanceToFoodSource(): FoodSourceAvailable {
    if (…)
        raise(FoodSourceNotAvailable(…))

    …
}
This can easily be converted into an Either/Result/Option type, but you don't need to do
map
/`mapError` everywhere since your functions return regular values. It's essentially the same as how Kotlin made null not suck by having proper syntax for it: it makes checked exceptions not suck by having proper syntax for them (and also you can use any object you want for failure, it's not limited to Throwable subclasses).
But in this case it sounds like a simple nullable is enough
👍🏻 1
n
not always what fits, but when it does, use it
It may be perfect if you don't want to know the cause of the error, only the fact that an error has happened. Sometimes it is enough, sometimes not.
p
I believe #arrow is actively considering demoting
Option<T>
(or at least moving it outside of core) to avoid the overlap with idiomatic nullables
👍🏻 1
c
@phldavies they are keeping Option because you can't nest nullables, but you can
Option<Option<Foo>>
. In all other cases, you should prefer nullable values to Arrow's Option.
p
Yes - I corrected myself from "deprecating" to "demoting" 🙂
👍 1
m
e
IMO: nested options are not great even in languages that support it as first-class: what is the difference between
None
and
Some(None)
?
👍 1
c
Note for the Result type in the standard library, and especially
runCatching
: you should not use it if you're using Coroutines
e
sometimes it comes up when writing generic code, e.g. you want to have the equivalent of
T
and
T?
even when
T
is a nullable type, which you can't do when
T??
collapses to
T?
. but otherwise…
c
I've been using Arrow in all my projects for the past year or so, and never used Option, I always use nullables or Either
p
I think the only usecase for nested options I've come across is the "optional query parameter", or where in a map the presence of a key and the presence of a value for that key are distinct, i.e.
Map<K, V?>
- rare though.
👍 1
m
I've used something like
Option
with generics that don't support nullable types. Normally an issue when the generic got exposed to iOS through Objective-C where nullable bound type parameter break.
p
Another use-case is in JSON when
{myValue: null}
vs
{}
matters (
PATCH
request handling, for example)
👍 1
j
@Laurent Laborde You seem to be disliking null, which is perfectly fine, as opposed to disliking the fact that java didn't consider null in the type system, making it super inconvenient. Kotlin doesn't have this problem, null is really a great option to represent a potentially missing value. That's the essence of its existence
r
In fact, a concept of "null" comes in many forms Some call it null, nil, and so... and some call it None or None()
l
@Joffrey yes, i dislike null. But even more, i dislike that something become nullable program-wide because of some edge case. Sure, a path to an unreachable location could be null (the path doesn’t exist). But a distance to a location, shouldn’t. ever. (0, yes. infinite, sure. but null ? no). I’ll try the sealed interface suggestion.
j
Representing none with a sealed subclass
None
or with a null doesn't make any conceptual difference. You can see that you need the concept of the absence of value. A distance cannot be null, but a variable can hold "no distance" in some contexts (like yours, apparently)
c
(and when there is no conceptual difference,
null
should be preferred because it's integrated into the language, so you can use
?.
,
?:
etc, instead of
flatMap
,
orElse
etc. It also uses less memory)
2
j
I dislike that something become nullable program-wide because of some edge case
If you're using "become" in the sense of mutability, then don't use mutable state, this has nothing to do with null. If you're using "become" in the sense of the programmer changing the type to be a nullable type in the code so that some edge case can be handled, then there is no difference in doing that versus using a sealed type. You still represent 2 possibilities via the type system. That is the main difference with java, where null wasn't probably taken into account in the type system. With that in mind, using null is made exactly for this and has nicer facilities than using a sealed type if your purpose is to express a type that can be another type or its absence
👍 1
d
IMO part of moving into Kotlin from Java and other languages is rethinking null from first principles. the notoriety of null derives from lack of static nullability treatment in e.g. Java and C++. in Kotlin
T?
is no worse than an
Option
or
Maybe
type from other languages. semantics are slightly different (e.g. nesting) but that isn't necessarily a bad thing
105 Views