Photo via Wikipedia

Kotlin tools - Union Types

Posted: 14 Jul 2021. Last modified on 04-Jun-22.

This article will take about 5 minutes to read.


You can run the gist for this article here


Union Types are a common language feature that stems from systems level programming. They are a type that stands for one of a set of types. They were originally intended as a way to save space - when allocating space for a variable, a union would allocate enough space for the largest type that it supported. That would guarantee that no room was wasted, and the allocated space could continue to be used for other resource types without the need for another allocation.

We no longer need that level of resource management, particularly in kotlin, but Union types have further uses. They allow for Domain Driven Development, a style of programming which enables us to validate our data at compile time, and gives us new ways to reason about our program. A good way of putting it is that, in a properly modeled domain, “Illegal operations should be impossible to represent.”

One example of this would be validation of an address. In our system, we would like to guarangee that an address is a string with 3 parts, which is up to 100 characters long. It may be fine to represent this as a String, but that is leaving out a crucial bit of information about the properties of this object. It would be better to perform validation that allows us to box a String into an Address that has validation in its init. If we to take the analogy further and have types for AddressUSA and AddressDE, we would run into a problem - both addresses are valid, but we only want to have one of them at a time. Even more so, what if we want to include a type called InvalidAddress? This is where unions come in.

It would be acceptable to specify that we need a Union of AddressUSA and AddressDE, and this would guarantee that we could not accidentally add an invalid address, or an address for an invalid country. The code would not compile! Kotlin does not have this functionality yet (but it might in 1.6!), so this is a union tool that will allow us to emulate that ability.

abstract class UnionBase<T>(s: Set<T?>) {
    init {
        val c = s.count { it != null }
        if (c != 1) throw Exception("Unions must have exactly one type, saw $c")
    }

    val value = s.first { it != null }
}

In the union class, I have stored all of the inputs into a set, which on initialization will validate that there is only one entry. The value field will make that entry available. However, this only allows one type in the union - we’ll have to extend it.

typealias Union<A, B> = UnionTyped<Any, A, B>

class UnionTyped<T, A : T, B : T> private constructor(
    val a: A? = null,
    val b: B? = null
) : UnionBase<T?>(setOf(a, b)) {

    companion object {
        fun <T, A : T, B : T> ofA(a: A) = UnionTyped<T, A, B>(a = a)
        fun <T, A : T, B : T> ofB(b: B) = UnionTyped<T, A, B>(b = b)
    }
}

inline fun <reified T, reified A : T, reified B : T> UnionTyped<T, A, B>.resolve(
    fa: (A) -> Unit = {},
    fb: (B) -> Unit = {}
) = when (value) {
    is A -> fa(value)
    is B -> fb(value)
    else -> throw Exception("Wrong type")
}

Here, we define a binary Union, that can take 2 types. If they share a common type, that can be expressed as T.

When unboxing the union, we have 2 options - call resolve with the handler for the type that we would like to handle, or access value and use it in the rest of the code.

In practice, it will be used something like this

val union: Union<Int, Boolean> = Union.ofB(true)
union.resolve(
  { it: Int -> println("A: $it") },
  { it: Float -> println("B: $it") }
)
// true

This can easily be extended into a Union3, or further, depending on your needs. Hopefully we are able to see a syntax like typescript or F# in the future!

lateinit var a: Result | Failure