edit: I made a mistake in this article! See Covariance in a Result<T, E> type in Kotlin for corrections.
Kotlin provides a mechanism for sum types in the form of sealed classes. It works by closing the class for extension outside the immediate context, allowing the compiler to know every possible subtype and perform the exhaustive checking expected of the variants of a sum type. However, some typical sum type use cases are a touch awkward with this approach. Let's explore by trying to write a Result<T, E> type in Kotlin using a sealed class.
In languages with first-class sum types, it's common to represent the results of fallible operations using a sum type with one variant for the successful result value, and another variant for errors. Haskell uses the extremely generic Either type; Rust uses the more specific Result. We'll use Rust as our template here, if only because its syntax and semantics are much closer to Kotlin than Haskell's.
Rust's Result type is
defined as
pub enum Result<T, E> {
/// Contains the success value
Ok(T),
/// Contains the error value
Err(E),
}
Simple enough: a two-variant sum type parameterized by the types
of the success and error values. Let's try a naïve transformation
of this into Kotlin. We'll have a sealed class with subclasses for
the variants:
sealed class Result<T, E>() {
/** Contains the success value */
data class Ok(val ok: T): Result()
/** Contains the error value */
data class Err(val err: E): Result()
}
Because the variants must be actual classes, they have to
be containers for values of the desired types, resulting in the
somewhat redundant additional ok and err names,
but that's a fairly minor cost. (Also, while I've nested the
sealed class variants within their parent class here, this isn't
required.) The compiler has this to say about this attempt:
Unresolved reference: T
2 type arguments expected for class Result
Oh, okay. The subtypes are technically independent of the parent
sealed class (unlike Rust's enum variants), so we'll have to
parameterize them separately, and use the type parameters with the
parent class constructor.
sealed class Result<T, E>() {
/** Contains the success value */
data class Ok<T>(val ok: T): Result<T, *>()
/** Contains the error value */
data class Err<E>(val err: E): Result<*, E>()
}
Projections are not allowed for immediate arguments of a supertype
Projections are not allowed for immediate arguments of a supertype
Oh dear. So we'll need to parameterize the variants with
both type parameters used by the supertype.
sealed class Result<T, E>() {
/** Contains the success value */
data class Ok<T, E>(val ok: T): Result<T, E>()
/** Contains the error value */
data class Err<T, E>(val err: E): Result<T, E>()
}
And now the compiler accepts it. All this information is
kind of present in the Rust implementation: any given
Ok is a Result, with the values of both type
parameters known, even though one is irrelevant for Ok.
But specifying it is very redundant. Furthermore, we can
do confusing things like
sealed class Result<T, E>() {
/** Contains the success value */
data class Ok<A, B>(val ok: A): Result<A, B>()
/** Contains the error value */
data class Err<C, D>(val err: D): Result<C, D>()
}
which is A-OK as far as the language is concerned and semantically
equivalent to the far clearer version that uses T and
E thoughout.
Okay. So we have a Result type, and it seems like it
should behave correctly. Let's give it a whirl.
fun div(x: Int, y: Int): Result<Int, String> =
if (y == 0) {
Result.Err("Division by zero")
} else {
Result.Ok(x / y)
}
fun main(args: Array<String>) {
(0..5)
.map {
div(5, it)
}
.forEach {
println(it)
}
}
Err(err=Division by zero)
Ok(ok=5)
Ok(ok=2)
Ok(ok=1)
Ok(ok=1)
Ok(ok=1)
Cool. There's some awkwardness, but fundamentally, that worked.
One of the nice things about representing errors as normal values
is that we can easily do things with them; for example,
chaining processing of valid values while short-circuiting errors.
(This is called “monadic error handling”, a term and
topic we definitely don't have room to explore deeply here.)
Continuing to take inspiration from Rust, we have
Result.map
to continue processing of an Ok value while propagating
Errs. Rust
defines
Result.map as
impl<T, E> Result<T, E> {
…
pub fn map<U, F: FnOnce(T) -> U>(self, op: F) -> Result<U, E> {
match self {
Ok(t) => Ok(op(t)),
Err(e) => Err(e),
}
}
…
}
This is straightforward: apply the operator to an Ok;
pass through an Err. We can do essentially a straight
translation into Kotlin:
sealed class Result<T, E>() {
…
fun <U> map(op: (T) -> U): Result<U, E> = when (this) {
is Result.Ok -> Result.Ok(op(this.ok))
is Result.Err -> Result.Err(this.err)
}
}
fun main(args: Array<String>) {
(0..5)
.map {
div(5, it)
.map { it * 2 }
}
.forEach {
println(it)
}
}
Err(err=Division by zero)
Ok(ok=10)
Ok(ok=4)
Ok(ok=2)
Ok(ok=2)
Ok(ok=2)
Nice! Just about the only structural difference is that Kotlin
doesn't
support destructing matches in when statements, so we
have to pull out the values of ok or err in the
right-hand expressions.
We don't have to implement map like this, though.
Because Ok and Err are themselves
classes, they can have member functions independent of their
parent class. So we could write it like this:
sealed class Result<T, E>() {
abstract fun <U> map(op: (T) -> U): Result<U, E>
data class Ok<T, E>(val ok: T): Result<T, E>() {
override fun <U> map(op: (T) -> U): Result<U, E> = Result.Ok(op(this.ok))
}
data class Err<T, E>(val err: E): Result<T, E>() {
override fun <U> map(op: (T) -> U): Result<U, E> = Result.Err(this.err)
}
}
Oof. That's a lot of redundant type parameterization. It does
work, though! This sort of structure might make sense if the
different variants have long, highly divergent implementations.
Probably not here, though. “Is subtyping a good idea”
is a even more out of scope than discussions of monads, so let's
leave it at that.
Let's try another. Rust provides
and_then
to chain fallible operations; essentially, something like
map if op itself could fail and thus returned a
Result.
sealed class Result<T, E>() {
…
fun <U> andThen(op: (T) -> Result<U, E>): Result<U, E> = when (this) {
is Result.Ok -> op(this.ok)
is Result.Err -> Result.Err(this.err)
}
}
fun main(args: Array<String>) {
(0..5)
.map {
div(5, it)
.map { it - 2 }
.andThen { div(2, it) }
}
.forEach {
println(it)
}
}
Err(err=Division by zero)
Ok(ok=0)
Err(err=Division by zero)
Ok(ok=-2)
Ok(ok=-2)
Ok(ok=-2)
That was easy. So, we have a clear demonstration that sealed
classes are expressive enough to capture sum type idioms.
Here's an interesting follow-up, though. Result types are used for error handling. Kotlin already uses exceptions for this purpose; so can we get our Result to interoperate nicely with exceptions so someone could plausibly use it? Let's try.
First, we want to be able to transform a Result into
a value or a thrown exception. Rust calls the analogous operation
unwrap,
so we'll go with that name.
sealed class Result<T, E>() {
…
fun unwrap(): T = when (this) {
is Result.Ok -> this.ok
is Result.Err -> when (this.err) {
is Throwable -> throw this.err
else -> throw RuntimeException(this.err.toString())
}
}
}
The handling of Err values here is a little clunky—
Err.err has no restrictions on type. If it's
Throwable, throwing it is the obvious thing to do; but if
it's some other type, there's not a great way to package it in the
standard library types. For expediency here, we'll stringify it
and throw it in a RuntimeException; but a better solution
might be a custom exception type that can store an Any?
as its cause.
The other function we need is one that runs a potentially throwing
operation and wraps the return value or exception in a
Result.
fun <T> wrapResult(op: () -> T): Result<T, Exception> = try {
Result.Ok(op())
} catch (e: Exception) {
Result.Err(e)
}
(We're only catching Exception, and not
Throwable, because
Errors
in general should not be caught.)
fun main(args: Array<String>) {
(0..5)
.map {
wrapResult { 5 / it }
.map { it - 2 }
.andThen { wrapResult { 2 / it } }
}
.forEach {
println(it)
}
}
Err(err=java.lang.ArithmeticException: / by zero)
Ok(ok=0)
Err(err=java.lang.ArithmeticException: / by zero)
Ok(ok=-2)
Ok(ok=-2)
Ok(ok=-2)
Nifty.
fun main(args: Array<String>) {
(0..5)
.reverse()
.map {
div(5, it)
.map { it - 2 }
.andThen { div(2, it) }
}
.forEach {
println(it.unwrap())
}
}
-2
-2
-2
Exception in thread "main" java.lang.RuntimeException: Division by zero
I'll leave the question of whether handling fallible operations this way in Kotlin codebases is a good idea for another time (but if you want to explore it, start here: it is highly unidiomatic). It's neat to show that it's clearly possible and reasonably ergonomic, though! The sample code here is available in Example.kt. I may at some point consider completing the implementation, if only for curiosity's sake; to my slight surprise, it doesn't seem anyone has done this yet.