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