Union types


#21

The union type could be erased to the higher super type, not systematically Any. That would be better for Java interop.


#22

You don’t really need union types for this thing at all. All you need is a shared interface with classes that implement that interface for each of the things you want in your union. Below is a Kotlin translation of the way I have my students do this sort of thing in Java8. Every subclass has a method to tell you what it actually is, and the interface supports a match method that takes one lambda for each subtype and calls the appropriate one. (You could do this as well with a when statement that checks the concrete type.) The resulting code that deals with JsonValue instances can then call more specific methods on the concrete types to get their internal values, as appropriate.

interface JsonValue {
    enum class JsonTypes { OBJECT, ARRAY, STRING, NUMBER, BOOLEAN, NULL }

    fun getType(): JsonTypes

    fun <T> match(objectFunc: (JObject) -> T,
                  arrayFunc: (JArray) -> T,
                  stringFunc: (JString) -> T,
                  numberFunc: (JNumber) -> T,
                  booleanFunc: (JBoolean) -> T,
                  nullFunc: (JNull) -> T) =

        when(getType()) {
            JsonTypes.OBJECT -> objectFunc(this as JObject)
            JsonTypes.ARRAY -> arrayFunc(this as JArray)
            JsonTypes.STRING -> stringFunc(this as JString)
            JsonTypes.NUMBER -> numberFunc(this as JNumber)
            JsonTypes.BOOLEAN -> booleanFunc(this as JBoolean)
            JsonTypes.NULL -> nullFunc(this as JNull)
        }
    

    class JObject : JsonValue {
        override fun getType() = JsonTypes.OBJECT
    }

    class JArray : JsonValue {
        override fun getType() = JsonTypes.ARRAY
    }

    class JString : JsonValue {
        override fun getType() = JsonTypes.STRING
    }

    class JNumber : JsonValue {
        override fun getType() = JsonTypes.NUMBER
    }

    class JBoolean : JsonValue {
        override fun getType() = JsonTypes.BOOLEAN
    }

    class JNull : JsonValue {
        override fun getType() = JsonTypes.NULL
    }
}

#23

@dwallach your approach is sub-optimal.

This is a better approach to union type emulation in Kotlin:

sealed class JsonValue<out T>(val value: T) {
    class JsonString(value: String) : JsonValue<String>(value)
    class JsonBoolean(value: Boolean) : JsonValue<Boolean>(value)
    class JsonNumber(value: Number) : JsonValue<Number>(value)
    object JsonNull : JsonValue<Nothing?>(null)
    class JsonArray<V>(value: Array<V>) : JsonValue<Array<V>>(value)
    class JsonObject(value: Map<String, Any?>) : JsonValue<Map<String, Any?>>(value)
} 

The match function you created is totally unnecessary in Kotlin, that’s what the when block is used for (but much more convenient than calling a function like that).

This helps in some cases, but a lot of times, when you get used to unions, you need intermediate union types that you really shouldn’t need an interface or sealed class for… for example, when processing data in Streams, you might just want to be able to process both Strings and Ints (or Floats, Booleans…) in the same pipeline… but going down to Any would bypass type-safety entirely…

When you have union type at your disposal, you can be quite accurate in your definitions… see what Json type definitions in Ceylon look like.

I really look forward to being able to use union types in Kotlin, and hope the Kotlin team gives it more consideration.


#24

@salomonbrys May I ask you some details on what exactly you were trying to achieve before you’ve started this topic. I’m intrigued by the fact that you’ve started it by mentioning JsonType. Since I’ve already have a working prototype of native type-safe Kotlin serialization (that also happen to already support basic JSON) I’d like to learn more about your specific use-case if you can follow up with it in this thread, please: Kotlin Serialization


#25

I’m curious if any movement has happened on this topic. In response to @elizarov’s question, the point (or at least the primary use case in my view) is being able to create type-safe APIs using existing patterns and APIs - JSON is just one example where plenty of Java libraries have worked around the lack of union types. Consider org.JSONObject which has several overloads of put:

public JSONObject put(String name, boolean value);
public JSONObject put(String name, double value);
public JSONObject put(String name, double value);
public JSONObject put(String name, int value);
public JSONObject put(String name, long value);
public JSONObject put(String name, Object value);

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. The same can be said for android.os.Bundle and many others - even Anko does this, and it feels like it’s a common enough use case that the language can help with.


#26

I’m even more intrigued now. Assume we have union types. How would it simplify your JSON code, then?


#27

Consider writing a jsonObjectOf function (like the bundleOf function from Anko). Today, you might write something like:

fun jsonObjectOf(vararg pairs: Pair<String, Any?>) = JSONObject().apply {
  for ((key, value) in pairs) {
    when (value) {
      null -> putNull(key)
      is String -> put(key, value)
      is Boolean -> put(key, value)
      is Double -> put(key, value)
      is Int -> put(key, value)
      is Long -> put(key, value)
      else -> throw UnsupportedOperationException()
    }
  }
}

There is no way to enforce compile-time type constraints for the arguments. Wouldn’t it be better to enforce that the arguments are of some union type?


#28

Using a union type here seems to be appropriate, but the corresponding union type may end up polluting the code everywhere, because you’ll have to mention null|String|Boolean|Double|Int|Long all over the place. It is much better from design standpoint to encapsulate it into JsonValue class as that is going be the only type you’ll have to carry around and you’ll be able to attach all your utility functions to this type. 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. Having defined them once, you’ll reap benefits everywhere throughout your code.


#29

That’s what type aliases are for.


#30

For wrapping javascript libraries it is very useful. I know “dynamic” is a catch all, but again some type of compiler checking rather than allowing everything through would be nice.


#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.
}