Kotlin and discriminated unions (sum types)
Solution 1:
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:
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:
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/