Union types


#31

Maybe trough different Either<A,B,…> classes ? Invisible to pure kotlin users, but preserve java interop.


#32

Another use case here: I’d especially want to use discriminated unions for single case types, which effectively allows me to e.g. have two incompatible strings.

F# has this and I have used it for great benefit in the code base, especially when applying string transformations like parsing to make it impossible to accidentally compare a Token to a raw string (you can only compare Tokens to Tokens). Here are some examples: https://fsharpforfunandprofit.com/posts/discriminated-unions/#single-cases

Of course Kotlin has sealed classes but they are not quite the same and especially for the equivalen of an F# single case disriminated union they have much more boilerplate. Type aliases of course only add syntactic sugar, but no actually checked types.

Regarding the compilation: F# compiles DUs down to a simple class hierarchy with an enum Tag property that discrimates the cases. I have always found that to be convenient enough when using DU types from C#. Here’s some reference how that looks like: https://fsharpforfunandprofit.com/posts/fsharp-decompiled/#unions

There are also some MS Research papers on the implementation which I’m sure you can dig out…


#33

Do you really find writing Kotlin’s class CustomerId(val id: Int) vs F#'s type CustomerId = CustomerId of int to have much more boilerplate or there is something else that bothers you? Can you give a more worked out example of the F# code you are trying to port, please?


#34

@damianw,

I don’t believe this is how you will do that today, this creates so much garbage just to create a simple object (all the pairs+ the array that holds them in the vararg), in addition it quite inefficient at runtime (all that instanceof checks) and finally, as you mentioned it is not type safe (the throw UnsupportedOperationException() in the else clause).

Instead, the following creates only one temporal object and effectively uses method overloading to mimic union support at compile-time:

class JsonObjectBuilder(private val json: JSONObject) {
    operator fun String.remAssign(value: Boolean) = json.put(this,value)
    operator fun String.remAssign(value: Double) = json.put(this,value)
    operator fun String.remAssign(value: Int) = json.put(this,value)
    operator fun String.remAssign(value: Long) = json.put(this,value)
    operator fun String.remAssign(value: String) = json.put(this,value)
}

inline fun json(build: JsonObjectBuilder.()->Unit) = JSONObject().also { JsonObjectBuilder(it).build() }

//it will be used as follows:
//(for the faint of heart, the operator overloading could be replaced with an infix method_
val j = json {
    "a" %= "hello"
    "b" %= 7
    "c" %= true
}

Now, if there were only a way to remove that useless temporary object (JsonObjectBuilder)…


#35

You can use sealed classes for discriminated unions.

Note also that union types are a slightly different thing: a union type A | B is compatible with both A and B (with a run-time check). A discriminated union type is not, it requires an explicit constructor and explicit destructuring. They also would be mapped differently on JVM.


#36

Sealed classes are not useful when you don’t “own” all the classes.
For example, there is no way to express “String|MyString” via sealed classes.

Is there a YouTrack ticket about union types that I can vote?


#37

https://youtrack.jetbrains.com/issue/KT-13108


#39

Unfortunately it is not possible to vote on issue: “Voting for a resolved issue is not allowed.”


#40

Generate wrapper class with constructors for all possible values?

class JsonType {
    public final Object is;

    public JsonType(Boolean value) { 
        is = value;
    }
    public JsonType(Char value) { 
        is = value
    }
    …
}

public void example(JsonType value) {
    if (value.is instanceof Boolean) {
        …
    }
}

#41

I think that union types allow something that sealed classes don’t: they’re able to represent SUBSETS of classes with a common ancestor or interface. Even subsets of enumerations, if their behaviour is extended.

For example, consider these classes:

interface Athlete {
}

class FootballPlayer : Athlete {
}

class Swimmer : Athlete {
}

class BasketballPlayer : Athlete {
}

// Etc.

A specific method might want to return instances of only some of those implementations:

typealias BallPlayer = FootballPlayer | BasketballPlayer;

...

fun findPlayersOfBallSports (): BallPlayer {
}

...

let player: BallPlayer = findPlayersOfBallSports ();
if (player instanceof Swimmer) {
  // ^^^ Error!!! Because "player" can only be an instance of either
  // FootballPlayer or BasketballPlayer.
}

In my opinion, it makes the code be much more powerful and self-documented.

If this behaviour were extended to instances of enum, it would also allow things like this:

enum class Device {
  SCREEN,
  KEYBOARD,
  MOUSE,
  CPU,
  RAM,
  DISK;
}

...

typealias InputDevice = Device.KEYBOARD | Device.MOUSE;

...

fun getInputDevices (): InputDevice {
}

...

let device: InputDevice = getInputDevices ();
if (device == Device.CPU) {
  // ^^^ Error!!! Because "device" can only be either
  // Device.KEYBOARD or Device.MOUSE.
}


#42

I agree with 100% Java interop as well as union type is implicit. but nevertheless Kotlin.js need Union Type for JS interop. Yes, parameter of union type can be replace with method overloading but javascirpt can be return union type as follow.

interface HTMLAllCollection {
    ...
    getter (HTMLCollection or Element)? namedItem(DOMString name);
};

this IDL can be translate to kotlin code as follow.

public external abstract class HTMLAllCollection {
    ...
    fun item(nameOrIndex: String = definedExternally): UnionElementOrHTMLCollection?
}

public external @marker interface UnionElementOrHTMLCollection {
}

this is not easy to understand and need to type casting. but what if kotlin.js have union type can be simplify as follow. furthermore dynamic keyword can be replace by union type for explicit.

public external abstract class HTMLAllCollection {
    ...
    fun item(nameOrIndex: String = definedExternally): (Element | HTMLCollection)?
}

#43

The Java interop argument regarding union types doesn’t seem consistent with coroutine design choices. You pretty much need a third party library for Kotlin coroutine features to be usable from Java code. Seems to me that there are many potential union type designs that wouldn’t be nearly as messy to call from Java as suspending code is with no additional libraries. In my opinion, it’s ok to require a third party library on the Java end to make calling some Kotlin features clean as long the Kotlin team provides this support.

Although some of the other arguments about maintainability and readability do seem valid. I don’t think simply allowing | operator in between types anywhere in the codebase is the right choice as I think it will be messy and overused.


#44

Kotlin has already a limited form of a union types, the unbounded union type a.k.a Any.

So it would be nice to have bounded types as well.

Assume you have a function jsonfy to pack elements of Int, Float, String into a JSON value.

Then, the following is valid:

    l:List<Union<String,Int,Float>>=...
    l2:List<JsonValue>=map(l,jsonfy)

This would be tedious with Any, as all types have to be covered in order to work it out.

In fact, the advantage of union types is that methods that are available to all ingredients of it, can be applied to any value of this type.

For special purposes, one have to reflect over it as already mentioned in posts before.

For the case of java interop I would implement a Union type as class for each arity containing an element and an index ranging from zero to arity.

The index will be updated alongside of assignments made to union values.


#45

Maybe I misunderstood something, but to me it seems that the thing you actually want is ducktyping or being able to implement interfaces for 3rd party classes. Personally, I really like how Rust solved this issue…

impl JsonValue for String {
  ...
}

#46

@nyxcode

Sorry for the late reply but I don’t get any notifications from discourse when someone writes something.

but to me it seems that the thing you actually want is ducktyping

Union typing is dynamic typing but its runtime polymorphism is bounded.

Rust can do this with existentials which may be possible in kotlin if kotlin adds typeclasses and typeclasses are first class.

But there are valid points for union type because operations which exists on all its element types are implicitly available for union types such that for a l1,l2:List<Union<Int,Float,Rational>> you can implicitly map(+,l1,l2) without to create a typeclass/trait for this.

Further, union types are structural and they subtype, does trait object sprovide the same?


#47

@elizarov wrote:

I would actually go a step further, and instead of Pair<String, Any?> or Pair<String,JsonValue> also define JsonPair class with the convenient constructors for it, too.

The point is the a JsonValue Classis unbounded, even a Rust trait a.k.a as tyepclass is also unbounded.

@WickeDev wrote:

Yes, parameter of union type can be replace with method overloading but javascirpt can be return union type as follow

Then you could also state that subclassing replaces method overloading, but there is a difference here, method overloading is a static variant of ad hoc polymorphism whereas union types and subclassing are runtime variants of it, they should be orthogonal.
You could also argue that typeclasses replace method overloading, but we need typeclasses anyway.

@160R wrote:

Generate wrapper class with constructors for all possible values?

Then you implement union types over and over again from. Furthermore, you will need compiler support such that methods accepting all types accepted by your Json type accepting also the Json type itself and implicit extract the json inner value into a value of its accepting types.

@damianw wrote:

The last of these overloads is for “throw your hands in the air and hope you have one of those types”. We could do better if we had polymorphic dispatch, which is related to (and can be solved by?) union types.

You don’t need polymorphic dispatch for this scenario, rather it would be better to provide only one signature of type union.

@SkittishSloth wrote:

I was thinking this could be why; if you have a union type that would generate overloads, you might run into a conflict with a separate, manual overload.

Good point, what we need are either overload conventions for ambiguities or we don’t compile.


#48

Ducktyping is different. Expressed with the words of ducktyping: Union types allow you to define a Duck and you are still allowed to implement a Frog which quacks like a Duck, but isn’t a Duck.
Based on this you can define:

  • a function which take Ducks as parameter
  • and a function which take Frogs
  • and another function which takes Ducks or Frogs : This would accept the union type (Duck|Frog) and then distinguish what to do with Frogs and what to do with Ducks - or do something which it can do with Ducks and Frogs equally. But the union-type ensures, that it will never be called with a DuckOrFrogSimilar thing which is neither a Duck or a Frog.

#49

not sure if it helps, but ceylon (https://ceylon-lang.org/) have union type support… and union type generally ends up with a split of code (so a when in kotlin).

that been said, union types is a good thing, I believe.


#50

How about:

enum class Device {
    SCREEN,
    KEYBOARD,
    MOUSE,
    CPU,
    RAM,
    DISK;
}

enum class InputDevice {
    KEYBOARD,
    MOUSE;
}

val InputDevice.asInputDevice
    get() = this

val Device.asInputDevice: InputDevice
    get() = when (this) {
        Device.KEYBOARD,
        Device.MOUSE -> InputDevice.valueOf(this.toString())
        else -> throw Exception("Device is not of “InputDevice” kind")
    }

fun operate(device: InputDevice): Unit {
    print("Operating: ${device}")
}

fun operate(device: Device): Unit {
    print("Operating: ${device.asInputDevice}")
}

fun usage(): Unit {
    operate(Device.KEYBOARD.asInputDevice)
    operate(Device.CPU.asInputDevice) // Not gonna work
}

#51

Scala language project to support union type in version 3.0 (2020) as well as intersection type.