Laurent Laborde
02/10/2023, 11:20 AMhfhbd
02/10/2023, 11:23 AMLaurent Laborde
02/10/2023, 11:37 AMLaurent Laborde
02/10/2023, 11:40 AMCLOVIS
02/10/2023, 11:41 AMNorbi
02/10/2023, 11:42 AMLaurent Laborde
02/10/2023, 12:03 PMphldavies
02/10/2023, 12:11 PMOption
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.phldavies
02/10/2023, 12:13 PMeither { }
computation blocks that allow for a nicer mechanism for unwrapping Either<L, R>
using `bind()`operator (the equivalent of Rust's ?
operator)Norbi
02/10/2023, 12:14 PMor possibly rolling your ownin this case I would recommend using the library mentioned above instead 😉if you do need to capture/handle the error casessealed class Result<Err, T>
phldavies
02/10/2023, 12:14 PMephemient
02/10/2023, 12:17 PMsealed 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 convenienceCLOVIS
02/10/2023, 12:18 PMNorbi
02/10/2023, 12:19 PMyou don't care why it failed, just that you had no resultOn 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 :)
Laurent Laborde
02/10/2023, 12:49 PMphldavies
02/10/2023, 12:52 PMOption<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)Laurent Laborde
02/10/2023, 12:52 PMLaurent Laborde
02/10/2023, 12:53 PMphldavies
02/10/2023, 12:54 PMnull
in Kotlin - it's surfaced in the type-system so no where near as scary as a null pointer in Java.Norbi
02/10/2023, 12:55 PMSo 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:
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:
data class FoodSourceAvailable(...)
object FoodSourceNotAvailable
fun calculateDistanceToFoodSource(...): Result<FoodSourceAvailable, FoodSourceNotAvailable>
Laurent Laborde
02/10/2023, 12:57 PMRoukanken
02/10/2023, 12:59 PMit'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 ResultNorbi
02/10/2023, 1:02 PMand 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`:
...
when (calculateDistanceToFoodSource(...)) {
is FoodSourceAvailable -> ...
FoodSourceNotAvailable -> ...
}
Laurent Laborde
02/10/2023, 1:03 PMLaurent Laborde
02/10/2023, 1:04 PMNorbi
02/10/2023, 1:04 PM+1 If you don't want/need to know the cause of the error then simply returningis not as "scary" in Kotlin as it is in Java.null
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.Norbi
02/10/2023, 1:08 PMIn my first exampleFor my second example, it would be similarly easy to use the
Result<FoodSourceAvailable, FoodSourceNotAvailable>
return value:
calculateDistanceToFoodSource(...)
.map {
assertTrue(it is FoodSourceAvailable)
}
.mapError {
assertTrue(it is FoodSourceNotAvailable)
}
Laurent Laborde
02/10/2023, 1:09 PMNorbi
02/10/2023, 1:10 PMResult<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 :)phldavies
02/10/2023, 1:10 PMOption<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.ephemient
02/10/2023, 1:12 PMNorbi
02/10/2023, 1:12 PMmuch less readable compared to when ->For more complex cases it has advantages, eg. when chaining function calls:
calculateDistanceToFoodSource(...)
.andThen { doSomethingWithTheFirstResult() }
.andThen { doSomethingWithTheSecondResult() }
.mapError { ... }
ephemient
02/10/2023, 1:12 PMephemient
02/10/2023, 1:13 PMcalculateDistanceToFoodSource(...)
?.doSomethingWithTheFirstResult()
?.doSomethingWithTheSecondResult()
?: error("...")
not always what fits, but when it does, use itCLOVIS
02/10/2023, 1:15 PMcontext(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).CLOVIS
02/10/2023, 1:15 PMNorbi
02/10/2023, 1:15 PMnot always what fits, but when it does, use itIt 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.
phldavies
02/10/2023, 1:16 PMOption<T>
(or at least moving it outside of core) to avoid the overlap with idiomatic nullablesCLOVIS
02/10/2023, 1:17 PMOption<Option<Foo>>
. In all other cases, you should prefer nullable values to Arrow's Option.phldavies
02/10/2023, 1:18 PMmkrussel
02/10/2023, 1:18 PMephemient
02/10/2023, 1:19 PMNone
and Some(None)
?CLOVIS
02/10/2023, 1:19 PMrunCatching
: you should not use it if you're using Coroutinesephemient
02/10/2023, 1:20 PMT
and T?
even when T
is a nullable type, which you can't do when T??
collapses to T?
. but otherwise…CLOVIS
02/10/2023, 1:20 PMphldavies
02/10/2023, 1:22 PMMap<K, V?>
- rare though.mkrussel
02/10/2023, 1:24 PMOption
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.phldavies
02/10/2023, 1:25 PM{myValue: null}
vs {}
matters (PATCH
request handling, for example)Joffrey
02/10/2023, 2:13 PMRoukanken
02/10/2023, 2:19 PMLaurent Laborde
02/10/2023, 3:53 PMJoffrey
02/10/2023, 3:55 PMNone
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)CLOVIS
02/10/2023, 3:57 PMnull
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)Joffrey
02/10/2023, 4:26 PMI 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
Damien O'Hara
02/12/2023, 5:40 AMT?
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