Union and Intersection Types [feature request]

Currently it is possible to declare a variable explicitly nullable. This is a very nice feature and looks very similar to what Ceylon allows.

Unfortunately, while in Ceylon Null is a type, and String? is just syntax sugar for String|Null reading String or Null, in Kotlin union and intersection types are not supported at the moment.

Would it be possible to get union and intersection types in Kotlin as well?

2 Likes

This has been discussed a long time ago. I don’t see a reaction from the Kotlin team on the last responses, but generally speaking you have to provide a convincing use case for them to be included in the language: Any thoughts on Ceylon-style union and intersection types? - #19 by renatoathaydes

1 Like

Basic language features like Tuples or Union and Intersection types open new approaches to solve the same problems.

It’s hard to provide convincing cases for, technically speaking, you could achieve the same without them, but it’s hardly the point.

Integer|ParseError parseInt(String value) is a much better contract than Integer parseInt(String value) throws ParseException.

The compiler will force you to handle it. Ideally it will allow you to call methods that, in this case, are in the intersection of the Integer and ParseError types, if any, without further checking.

It could infer variable types in logical branches, so that the following would be allowed:

Integer|String variable = ...
if (variable is Integer) {
    variable++
} else {
    variable = variable + "a"
}

Also, methods could accept intersection types to reduce usage of overloading, and you could also allow contra-variant overriding of interface methods. As an example, in most languages the following is allowed:

interface ObjectSupplier {
    Object supply()
}

class StringSupplier implements ObjectSupplier {
    @Override
    String supply() {
         ...
    }
}

Now, we could allow something like:

interface StringConsumer {
     void accept(String value)
}

class ObjectConsumer implements StringConsumer {
     @Override
     void accept(Object value) {
         ...
     }
}

and also

class ObjectOrStringConsumer implements StringConsumer {
     @Override
     void accept(String|Object value) {
         ...
     }
}
2 Likes

How is this better? With the former I am forced to handle the error directly in the client. With the latter I have a choice: Let my caller handle the exception or handle the exception directly.

I am not saying unions and intersections cannot be useful, but you have to provide more objective advantages if you want to have them added to the language. The example you give above looks to me like going back in time: Handling errors using special return values instead of using exceptions. (Yes, I know special return values are returning because of functional programming.)

If union types were added to Kotlin, this would add enormous complication to the type system - perhaps even more complexity than adding multiple class inheritance - and I agree with @jstuyts that the Kotlin team would need to see some compelling use cases for it to be entertained at all.

I recently tried Crystal which has union types and, whilst it is a nice language in many ways (a bit like Ruby on steroids), I found union types to be an infuriating feature. This is because the compiler was always trying to interpret my code as if I’d intended to use a union type when in fact they were just simple newbie mistakes on my part :frowning:

So, after that experience, it would be a definite thumbs down from me.

1 Like

I have some mixed feelings about union types, but intersection ones appeal to me much more.

It somehow relates to the task I posted recently: https://youtrack.jetbrains.com/issue/KT-19645. There I wanted to limit sealed class branches by applying marker interfaces through generics. In other words by using multiple bounded generics. Later I noticed that this is just how you would implement intersection types in Java:

fun <T> handleSealedType(obj: T) where T : SealedClass, T : Marker {...}

But to be honest in most cases like this one most convenient would be to use Ceylon-like syntax:

fun handleSealedType(obj: SealedClass & Marker) {...}

Generics usage is ofcourse limited only to generic declarations. You can’t use this approach for properties or local variables explicitly.

I think that generic constraints solve your problem. You can write:

fun <T> handleSealedType(obj: T) where T: SealedClass, T: Marker {...}

Ceylon syntax looks better though. Maybe it is a good idea to introduce interface joining operation instead of where.
On the other hand, I believe that requirement for such types is usually a consequence of bad class hierarchy design.

2 Likes