Union types

Just heard from the source at Devoxx Belgium that Union types are coming to Java, probably 15 or 16.

Do you have a reference where I can read about that?

Sorry, now that the video is published (Java Language Futures: Late 2019 Edition by Brian Goetz - YouTube) and I had a chance to look at the announcement again, I see they’re really only talking about sealed types which I guess are a specific kind of union types. My bad!

here is another possible use-case for union types that is currently not possible:

return when(union) {
    is A,
    is B ->  when(union) {
        // smart-cast `union` to A|B union type
        // do something common for A & B
        when(union) {
            is A -> // do A-specific
            is B -> // do B-specific
            // don't handle `is C`
        }
    }
    is C -> // do C-specific
}
2 Likes

another great thing: you could use it in combination with a bigger abstraction of ?. do do great things like allow for specifying on which concrete type a call-chain should continue / allow for call-chains to continue regardless of the concrete type.
Say we would be using unions to implement a result-type (i know, this isn’t the best example, as a Result type is easy to implement with sealed-classes, its just an easy-to-understand example
say you have

val value: [ Int, String ] = 12
// scope-functions (or maybe any method) could take an additional type argument 
// which specifies that they will only be called if the concrete value is of a specific type
// and otherwise continue in the call-chain without executing.
val v2: [ Long, String ] = value.<Int>let { it.toLong() }

// you could chain multiple of these to transform all possible types into other types
val v3: [ Long, Boolean ] = value.<Int>let { it.toLong() }
                                 .<String>let { it == "foo" }

// if you transform them in a way that turns all types into the same type
// they could get combined automatically
val v4: Int = value.<String>let { it.toInt() }

this would allow for arbitrary implementations of ADTs to have a syntax simmilar to the beautifully concise null-safe chaining syntax. you could (nearly) implement the nullable type like that:

typealias Nullable<T> = [ T, null ]

val ex1: Nullable<Int> = 123
ex1.<Int>let { println(it) }

val ex2: List<String> = listOf("hi", "  14", "2")
val nums: List<Float> = ex2.mapOnly<Int> { 
    it.trim()
      .toIntOrNull() // <- returns [ Int, null ] here 
      .<Int>toFloat()
}

this would allow for a lot more expressive use of result-types and simmilar types in general!

2 Likes

I find a lot of these proposals to be unnecessarily verbose to both define and use in practice. I think an idiomatic approach to this that leans on existing kotlin principles could be a simple sealed typealias

sealed typealias FlexibleDate = String | Long | Instant

Basic principle is that the alias alone is just a bounded set of types, and you but then unwrap with a conventional when or instance check. That’s all Kotlin would be doing special here, and the rest continues on as normal kotlin code once the type is checked.

  • Keywords already exist. RHS expression syntax would need a little work but it could work
  • At runtime they’re all just objects getting casted, but there’s plenty of precedent for this (generics)
  • Self-contained in the alias expression
  • Doesn’t allow anonymous expressions like typescript or similar languages do, which I think is a net positive
  • Can be compiler-checked just like sealed classes but without requiring inheritance. Instance check just results in a smart cast like sealed classes or other instance checks do today
when (someDate) {
  is String -> println("Date is $someDate")
  is Number -> println(someDate - 3L)
  is Instant -> println("Date is ${someDate.epochMillis()}")
}
12 Likes
sealed typealias FlexibleDate = String | Long | Instant

Yes, if you can easily add code

fun bar( x : String | Long) { ... }

fun foo( y : FlexibleDate) {
   if (!(x is Instant)) { // so x is String | Long
       bar(x)
   }
}
2 Likes

I don’t know if this is precisely on topic or not, but I ran across the same issue that OP laid out, basically. I don’t own the objects so I can’t seal them. I also don’t like the idea of writing that boilerplate for this case, because, in this case, I’m actually just trying to simplify utilization of an existing set of overloads (provided by GSON).

My approach to simplifying this would be to implement perfect forwarding with an inline reified function:

inline fun <reified T> JsonWriter.property(key: String, prop: T) { name(key); value(prop) }

This code would be valid for any t: T where value(t) was valid.

It wouldn’t suffer for Java interop because it’s inline; the reified flag indicates that the type parameter is a placeholder for a concrete type resolved at compile time.

In case this use of reified would compete with existing usage, maybe we could use lazy instead or similar–indicating that, rather than absorbing the type T from call context, it absorbs the type from compile time success instead.

This, combined with varargs, could be a mechanism for implementing perfect forwarding, i.e. producing syntax sugar methods that enable logical code reuse across types of flexible parentage.

1 Like

Union types would make a big difference in Kotlin. At the moment, a sealed class is the closest thing to a union type, but it has several limitations, the biggest being that you can only have subtypes defined by you as part of a sealed class.

The proposal would be for the type system to understand union types as a concept. A few examples.

Dynamic casting:

val something: Int|String|Boolean = TODO("implement")
if (something is String|Boolean) {
    // here it's a string or a boolean
} else {
  // here it's an int
}

Intersection of available functions:

class First(val value: Int) {
      
     fun speak() { println("shouting") }
}

class Second(val value: Int, val name: String) {
   
     fun speak() { println("talking") }
}

fun main() {

     val something: First|Second = TODO("implement")
     val value = something.value
     something.speak()
     // something.name doesn't compile
}

Syntax sugar for Null type:

val messageOrNull: String? = TODO("implement")
val messageOrNull2: String|Null = TODO("implement")

Covariance and contravariance:

fun main() {

     val consumerWider: (String|Int) -> Unit = TODO("implement")
     val consumerNarrower: (Int) -> Unit = consumerWider

     val producerNarrower: () -> Int = TODO("implement")
     val producerWider: () -> String|Int = producerNarrower
}

Covariant and contravariant overriding:

interface MessageProcessor {
    
     val format: String|Format

     fun process(input: String|Message): String|Message
}

class InMemoryMessageProcessor(override val format: Format) : MessageProcessor {

     override fun process(input: String|Message?): Message
}
3 Likes

Intersection of available methods? I don’t see how that could work without structural typing (which I think is a bad idea, as discussed on other pages).

In the example above, while both First and Second have methods called speak(), they’re not the same method. (Unless both classes implement some interface defining speak() — but then you don’t need union types, as you can simply use that interface instead.)

Assuming two methods behave the same simply because they have the same name (and types) seems unwarranted, and potentially very dangerous. In an example like this where both classes are defined in the same project, you can mitigate the risk — but AIUI the main benefit of union types is to unify types you don’t have control over (else you could use an interface).

But apart from intersection of methods, what can you do with an instance of a union type? Apart from stuff common to every type (equality checks, string conversion, and storage), you’re reduced to type checks and smart casts just about everywhere you use them. I’m failing to see much benefit.

1 Like

Benefits :
1 - On java, properties are defined as Map<String, Object>, with union type, you could have properties as :

class Tree<T>() {
    val value = HashMap<String, T|Tree<T>>()

    fun add(id : String, value : T|Tree<T>) {
        value.put(id, value)
    }

    fun get(id : String) : T|Tree<T>|Null {
        return value.get(id)
    }
}

class Properties : Tree<String|Number|Date>

Then, this is a hierarchical properties (can also be for json …)

2 - Iterator, with only one method

Interface Iterator<T> {
    fun next() : T|End
}
2 Likes

Why? instanceof checks are generated at compile time, e.g. instance is T[] or instance is String. Is a generic cast not possible?

Further, I think we need to check for possibly equal type parameters, e.g. S,T may be the same type.
And also the order of subtyping have to be taken into account to correctly arrange the instance of checks.
So also +1 for this proposal from me, it reminds me on the proposed introduction of union types in C#.

Feature is also available on scala 3, typed script and abandoned ceylon language

3 Likes

I work with Kotlin daily and I use Result types everywhere, and every time I write one, I REALLY miss having unions to represent error types.

This’s what a typical interface of mine looks like (simplified):

interface SomeRepository {

    fun add(
            item: ItemToAdd
    ): Either<AddException, RetrievedItem>

    fun find(
            itemId: ItemId
    ): Either<FindException, RetrievedItem?>

    fun findAll(
            query: ItemsQuery
    ): Either<FindAllException, Map<SimpleRetrievedItem, Weight>>

    sealed class AddException {
        object NoInternetConnectionException : PublishException()
        object NoSignedInUserException : PublishException()
        data class UnknownException(val origin: Throwable) : PublishException()
    }

    sealed class FindException {
        object NoInternetConnectionException : FindException()
        object NoSignedInUserException : FindException()
        data class InternalException(val origin: Throwable) : FindException()
        data class UnknownException(val origin: Throwable) : FindException()
    }

    sealed class FindAllException {
        object NoInternetConnectionException : FindAllException()
        object NoSignedInUserException : FindAllException()
        data class InternalException(val origin: Throwable) : FindAllException()
        data class UnknownException(val origin: Throwable) : FindAllException()
    }
}

This pattern is pretty much everywhere in my code and it’s pretty tiresome, wouldn’t it be nice to be able to do something like this:

object NoInternetConnectionException
object NoSignedInUserException
data class InternalException(val origin: Throwable)
data class UnknownException(val origin: Throwable)

typealias AddException =
    NoInternetConnectionException | NoSignedInUserException | UnknownException

typealias FindException =
    NoInternetConnectionException | NoSignedInUserException | InternalException | UnknownException

typealias FindAllException =
    NoInternetConnectionException | NoSignedInUserException | InternalException | UnknownException

Not only that, but it also removes the necessary mapping when moving these “Exceptions” between layers as all the layers, now, are basically using the same class and not different versions of the same class.
For example mapping RemoteDataSource.AddException to Repository.AddException to AddInteractor.Exception … etc.

It would also remove the unnecassary jargon when creating these exceptions

SomeRepository.AddException.UnknownException(...)

Simply becomes:

UnknownException(...)

It’s not limited to error types either, this also comes from a project I’m working on (simplified):

data class ItemToPublish(
    val user: SimpleRetrievedUser,
    val name: Either<Name, FullName>?,
    ...
)

Either works here but it’s a hack, the Either type is right-biased, its left is typically reserved for Errors. A union type would be much more appropriate here.

6 Likes

I think the most kotlin like way is to do it like they wanna it in C# over mapping union types to objects.

That is, you simply map a union type of multiple classes to the least common super class but with compile time knowledge what is inside and what can be safely extracted out and assigned in. It would also support implicit subsumption of union types.

The only disadvantage I see so far is that overloading over multiple union types might not be possible because different union types have the same upper bound, i.e. the same least common superclass.
However, we have the same problem with generics too which can be alleviated with the JvmName annotation to map the overloads to different function names.

I could live with that restriction and these union types would be easier to implement by compiler designers as it doesn’t require to implement a new vtable design, just use that of classes.

1 Like

I can’t see why kotlin way would better match with a non JVM language like C# than JVM one like Scala3 or Ceylon.

If i understand C# discussion (which is still just an idea ?) and your comment, a union type as ‘String|Int’ will be transformed to “least common super class”, here mean ‘Any’ ?

So, you couldn’t have ?

fun f1(y : Int|Bool) { ... }

fun foo(x : String|Int|Bool) {
    if (!(x is String) {
        f1(x)
    }
}

If i understand C# discussion (which is still just an idea ?) and your comment, a union type as ‘String|Int’ will be transformed to “least common super class”, here mean ‘Any’ ?

Yes

So, you couldn’t have ?

No, you couldn’t have

fun foo(y : Int|Bool) { ... }

fun foo(x : String|Int|Bool) {...}
1 Like

And if the types in the union implemented the same interfaces (note the plural here), the compiler should let you do as per the following:

interface Flying {
    fun fly()
}

interface Screaming {
   fun scream()
}

class A : Flying { ... }

class B : Screaming { ... }

class C : Flying, Screaming { ... }

fun main {
   
   val any: A|B|C = TODO("here")
   if (any !is B) {
       any.fly()
   }
}
4 Likes

+1 to what @AhmedMourad said.
Now that we’re going away from exceptions for error handling towards result types I find myself wanting union types almost every day. sealed classes are great but have severe limitations due to type hierarchy.

Let’s say I have 10 API endpoints. Each has a different successful result and a different set of error results.
4 can return UserNotFound, 7 can return NotAllowed, 2 can return InvalidPassword, all can return InternalError, and so on.

So I either

  • create 10 sealed classes (one per endpoint) where each lists all possible outcomes of the endpoint. That means there are 10 InnternalError subclasses, 7 NotAllowed subclasses etc… That means I cannot perform a generic error handling over NotAllowed and erase the type from the result because they are distinct types. Or…
  • I wrap everything into a generic Result<Value> (contains Value or Error) where Error is a common base type. Even if that Error is a sealed type I’d have to handle every possible error for every API endpoint even if they can never happen.

Both approaches have significant drawbacks.

Checked exceptions solved that problem, even though they had other issues. Now in Kotlin we don’t have checked exceptions and go towards using exceptions only for programming errors. That means we’re left with no good alternative to model data where a function can return multiple different outcomes, some if which are shared with other the outcome of other functions.

That’s a major use case I have quite frequently.

Another use case is JavaScript interoperability with Kotlin/JS. That is notoriously difficult if you have to consume TypeScript definitions with plenty of unions.

Another use case is better interoperability with GraphQL which does support union types.


Regarding JVM representation I think the “common supertype” approach is more than sufficient. The logic is already there in the compiler.

val foo = if (true) 1 else "bar" // = common supertype `Any`
// actually of common supertype `Comparable<*> & java.io.Serializable`

val bar = if (true) listOf(1) else setOf(2.2) // = common supertype `Collection<Any>`
// actually of common supertype `Collection<Comparable<*> & Number>`

I also wouldn’t mind if the Java API to a Kotlin library becomes “less safe” that way. If I want Java-interop and safety I’d not use unions. If I’m 100% in the Kotlin universe I don’t have to care about that.


Regarding overload conflicts on JVM I think that’s a non-issue.

We already have cases today where there is a legit overload in Kotlin (e.g. overloading by nullability or overloading by more specific generic type arguments) but it’s conflicting in JVM. Those cases are easily solved with the @JvmName annotation. The only exception are constructors which don’t allow for renaming.


Adding and removing types from a union (similar to @sollecitom’s example) would be an important feature. Also something the compiler is already capable of. But the syntax would be tricky.

fun <T> handleB(value: T|B): T without B
   = …

Roughly like this.

Use case:

fun <Result> process(result: Result|Error, processor: (Result without Error) -> Unit) {
   if (result is Error)
      // handle error
   else
     processor(result)
}

Some syntax thoughts:

  • Any type that’s not A~A
  • A type that’s T or AT | A
  • A type that’s T and AT & A
  • A type that’s T but not AT & ~A (the without from above)

So if T = A | B | C then T & ~A = B | C.
Basically like binary operators, but for types.

8 Likes

What’s difference between

 fun <T> handleB(value: T|B): T without B

and

fun <T> handleB(value: T|B): T

Does first means that if T and B are interfaces, you can’t return a type that implements B ? How can you control it ?
Or, does it mean that if B inherits from T, you can’t return a B instance ??