Kotlin and discriminated unions (sum types)
KotlinAlgebraic Data-TypesDiscriminated UnionKotlin Problem Overview
Does Kotlin have anything like discriminated unions (sum types)? What would be the idiomatic Kotlin translation of this (F#):
type OrderMessage =
| New of Id: int * Quantity: int
| Cancel of Id: int
let handleMessage msg =
match msg with
| New(id, qty) -> handleNew id qty
| Cancel(id) -> handleCxl id
Kotlin Solutions
Solution 1 - Kotlin
Kotlin's sealed class
approach to that problem is extremely similar to the Scala sealed class
and sealed trait
.
Example (taken from the linked Kotlin article):
sealed class Expr {
class Const(val number: Double) : Expr()
class Sum(val e1: Expr, val e2: Expr) : Expr()
object NotANumber : Expr()
}
Solution 2 - Kotlin
The common way of implementing this kind of abstraction in an OO-language (e.g. Kotlin or Scala) would be to through inheritance:
open class OrderMessage private () { // private constructor to prevent creating more subclasses outside
class New(val id: Int, val quantity: Int) : OrderMessage()
class Cancel(val id: Int) : OrderMessage()
}
You can push the common part to the superclass, if you like:
open class OrderMessage private (val id: Int) { // private constructor to prevent creating more subclasses outside
class New(id: Int, val quantity: Int) : OrderMessage(id)
class Cancel(id: Int) : OrderMessage(id)
}
The type checker doesn't know that such a hierarchy is closed, so when you do a case-like match (when
-expression) on it, it will complain that it is not exhaustive, but this will be fixed soon.
Update: while Kotlin does not support pattern matching, you can use when-expressions as smart casts to get almost the same behavior:
when (message) {
is New -> println("new $id: $quantity")
is Cancel -> println("cancel $id")
}
See more about smart casts here.
Solution 3 - Kotlin
The sealed class in Kotlin has been designed to be able to represent sum types, as it happens with the sealed trait in Scala.
Example:
sealed class OrderStatus {
object Approved: OrderStatus()
class Rejected(val reason: String): OrderStatus()
}
The key benefit of using sealed classes comes into play when you use them in a when expression for the match.
If it's possible to verify that the statement covers all cases, you don't need to add an else clause to the statement.
private fun getOrderNotification(orderStatus:OrderStatus): String{
return when(orderStatus) {
is OrderStatus.Approved -> "The order has been approved"
is OrderStatus.Rejected -> "The order has been rejected. Reason:" + orderStatus.reason
}
}
There are several things to keep in mind:
-
In Kotlin when performing smartcast, which means that in this example it is not necessary to perform the conversion from OrderStatus to OrderStatus.Rejected to access the reason property.
-
If we had not defined what to do for the rejected case, the compilation would fail and in the IDE a warning like this appears:
'when' expression must be exhaustive, add necessary 'is Rejected' branch or 'else' branch instead.
- when it can be used as an expression or as a statement. If it is used as an expression, the value of the satisfied branch becomes the value of the general expression. If used as a statement, the values of the individual branches are ignored. This means that the compilation error in case of missing a branch only occurs when it is used as an expression, using the result.
This is a link to my blog (spanish), where I have a more complete article about ADT with kotlin examples: http://xurxodev.com/tipos-de-datos-algebraicos/
Solution 4 - Kotlin
One would be doing something like this:
sealed class Either<out A, out B>
class L<A>(val value: A) : Either<A, Nothing>()
class R<B>(val value: B) : Either<Nothing, B>()
fun main() {
val x = if (condition()) {
L(0)
} else {
R("")
}
use(x)
}
fun use(x: Either<Int, String>) = when (x) {
is L -> println("It's a number: ${x.value}")
is R -> println("It's a string: ${x.value}")
}