68

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
Elias Zamaria
  • 96,623
  • 33
  • 114
  • 148
ehnmark
  • 2,656
  • 3
  • 24
  • 20

4 Answers4

65

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()
}
Adeynack
  • 1,200
  • 11
  • 18
  • This is Kotlin's way to encode Algebraic Datatypes but it is apparently a common misconception that algebraic datatypes or sum types would be union types. Algebraic datatypes define new type names but do not combine existing type constraints into a new type. As a structural type operation, union types are much more flexible than an Algebraic Datatype which is purely a nominal type. I am a beginner at Kotlin but as I understand it, this sealed class does not allow you to express something like `Int | Double`. – ChrisoLosoph Jun 26 '23 at 19:29
  • 1
    Nor is there any way to express `Int | Double` in Kotlin, as far as I know. That being said, I haven't touch that language since 2018 and it may have evolved since then. Interested in knowing more, if you have the knowledge or get it at any point @ChrisoLosoph :-) – Adeynack Jul 11 '23 at 09:53
39

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.

Andrey Breslav
  • 24,795
  • 10
  • 66
  • 61
  • 1
    Hi, thanks for your reply! In Scala I would use a `sealed Trait OrderMessage` and `case class New(..) extends OrderMessage` etc. I could then pattern match on order message types and get access to their fields at the same type (just like in the F# example above). Any chance we'd be able to do that with `when` in Kotlin any time soon? :) – ehnmark Feb 25 '15 at 12:53
  • @enhmark What you can do is this: http://kotlin-demo.jetbrains.com/?publicLink=104074971561017308771-1714234109. See more about smart casts: http://kotlinlang.org/docs/reference/typecasts.html#smart-casts – Andrey Breslav Feb 26 '15 at 12:21
  • @AndreyBreslav "will complain about exhaustiveness". Did you forget to put a "not" there? Otherwise I don't understand that part of your answer. – HRJ Aug 28 '15 at 03:22
  • @HRJ kind of yes, I did – Andrey Breslav Sep 02 '15 at 12:50
  • 34
    Kotlin now has [Sealed Classes](https://kotlinlang.org/docs/reference/classes.html#sealed-classes) which allow you to control the possible hierarchy and have the compiler check that you exhausted all options in a `when` statement / expression. This addresses the issue Andrey and @HRJ mention. – Jayson Minard Dec 31 '15 at 15:44
11

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/

xurxodev
  • 1,639
  • 20
  • 19
1

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}")
}