Some languages provide a facility to return an error from a call to method/function, along with the actual result value. This allows (or forces) the programmer to take into account potential runtime errors.
Something similar can be done in Kotlin.
Sources are here.
Ok - Err
In Rust, functions may return a result
type that can be handled with match
:
enum Result<T, E> {
Ok(T),
Err(E),
}
...
match a_result {
Ok(v) => ..., // deal with the value
Err(e) => ...,// handle error
}
Kotlin’s take on enums are sealed classes and the definition looks quite similar:
package result
import result.Result.Err
import result.Result.Ok
sealed class Result<out T> {
data class Ok<out T>(val value: T): Result<T>()
data class Err(val exception: Exception, val msg): Result<Nothing>()
}
For better ergonomics, we can also provide default values in Err
constructor:
data class Err(val exception: Exception = RuntimeException(),val msg: String = ""): Result<Nothing>()
With these definitions in place we can take this code for test drive:
fun query(id: Int): Result<String> = when (id) {
1 -> Ok("a result")
else -> Err(msg = "something went wrong...")
}
val result = query(...)
when (result) {
is Ok -> println("Got ${result.value}")
is Err -> {
val (exe, msg) = result
println("Exception = $exe , msg = $msg")
}
}
None - Some - Err
By using Ok
to indicate that no error has occurred, we are not well equipped to deal with the situations that a value is
not available. This is often indicated by null
and Kotlin has language-level support
to deal with this case.
In the context of this post, it is more natural to incorporate the lack of value into the result
object. We replace Ok
with None
and Some
:
package optionalresult
import optionalresult.OptionalResult.*
sealed class OptionalResult<out T> {
object None : OptionalResult<Nothing>()
data class Some<out T>(val value: T) : OptionalResult<T>()
data class Err(val exception: Exception = RuntimeException(), val msg: String = "") : OptionalResult<Nothing>()
}
In this case the test drive looks like this:
fun query(id: Int): OptionalResult<String> = when (id) {
0 -> None
1 -> Some("a result")
else -> Err(msg = "something went wrong...")
}
val result = query(...)
when (result) {
is None -> println("not found")
is Some -> println("Got ${result.value}")
is Err -> {
val (exe, msg) = result
println("Exception = $exe , msg = $msg")
}
}
Still following the Rust approach we can add
an expect()
method to OptionalResult
. This may be useful if we want to fail without much ceremony when we are expecting
a result value to be available (in a unit test for example).
sealed class OptionalResult<out T> {
object None : OptionalResult<Nothing>()
data class Some<out T>(val value: T) : OptionalResult<T>()
data class Err(val exception: Exception = RuntimeException(), val msg: String = "") : OptionalResult<Nothing>()
fun expect(errorMsg: String): T = when (this) {
is None -> throw IllegalStateException(errorMsg)
is Err -> throw IllegalStateException(errorMsg)
is Some -> this.value
}
}
lateinit var v: String
v = None.expect("expected: a result") // IllegalStateException: expected a result
v = Err().expect("expected: a result") // IllegalStateException: expected a result
v = Some("a result").expect("expected: a result") // v has the value "a result"