Union types

I really like sealed types, and I would love for kotlin to support some kind of union type. e.g.:
union JsonType {   Boolean,   Char,   Number,   String,   JsonElement }

public fun JsonObject.set(key: String, value: JsonType?) {
  when (value) {
          is Number  -> addProperty(key, value)
       is Char   -> addProperty(key, value)
       is Boolean -> addProperty(key, value)
       is String  -> addProperty(key, value)
       is JsonElement -> add(key, value)
  }
}


Note the absence of else :wink:

BTW : typedefs are also very mch needed, IMHO.

29 Likes

Typedefs (or typealiases, as we call them) are on the roadmap to be added after 1.0.

How exactly is this union type different from an enum? Note that enums also allow you to omit the ‘else’ clause if you use them in a ‘when’ statement.

Well, the only way we have, currently, to say "This function can take either Number, a Char, a Boolean, a String or a JsonElement" is : - to overload each methods (Kotson have loads of methods that can take either one of this types, so that's not a good option) - to take an Any argument and to throw an exception at runtime if the given argument is not one of the supported types.

Both options are not good to me :frowning:

2 Likes

I think that this use case ("I have a function that can take a value of one of these six different types") comes up in a very specific set of contexts, that usually involve writing RPC and/or serialization libraries. For a user of these libraries, it's not obvious why having the six overloads would be worse than having a single function that takes a union of six types. And the developers of those libraries comprise a relatively small minority of the users of the language, so we would need very strong arguments to introduce additional complexity into the language only to support these developers.

4 Likes

One great alternative would be to support C++ like auto reification from argument when no valid function is found.

public class JsonType {   constructor(str: String) { /*...*/ }   constructor(int: Int) { /*...*/ } }

public fun doSomethingJsony(autoreified arg: JsonType) { // }

public fun main() {
  doSomethingJsony(“Hello, world!”) // Auto-compiles to doSomethingJsony(JsonType(“Hello, world!”))
  doSomethingJsony(42) // Auto-compiles to doSomethingJsony(JsonType(42))
}


That would be freakin’ awesome :slight_smile:
BTW : I know I’m dreaming…

2 Likes

I can confirm that I greatly desired union types when working with Scala.JS (I assume a similar desire would be present when trying to model JS libraries in Kotlin when a JS parameter could be one of several types, when really it shoudl not be... IMO). And after I struggled for a day or two* and eventually resorted to overloading of constructors (which took some getting used to in Scala), Scala.JS actually ended up implementing union types, so that all my struggles could have been avoided!

http://www.scala-js.org/news/2015/08/31/announcing-scalajs-0.6.5/

1 Like

This looks very much like Scala's implicits to me, and we're pretty certain that we do not want to have this feature in Kotlin.

3 Likes

May I ask why ?

I think implicits is a bit of a controversial feature because of the way in which it hides effects in a non-obvious way. A bit like global variables but not quite so bad.

Working proof of concept, just for fun :

 
public class Test {
    public fun doSomething(el: JsonElement) {
    }
}

public class JsonFunction<T, R>(private val self: T, private val method: T.(JsonElement) -> R) {
  public operator fun invoke(arg: Boolean) = self.method(JsonPrimitive(arg))
  public operator fun invoke(arg: Number) = self.method(JsonPrimitive(arg))
  public operator fun invoke(arg: String) = self.method(JsonPrimitive(arg))
  public operator fun invoke(arg: Char) = self.method(JsonPrimitive(arg))
  public operator fun invoke(arg: JsonElement) = self.method(arg)
}

public val Test.doSomethingAuto: JsonFunction<Test, Unit> get() = JsonFunction(this) { doSomething(it) }

public fun main() {
  Test().doSomethingAuto(42)
  Test().doSomethingAuto(“Hello, world”)
}


The Kotlin guys are very against implicits. Everything in Kotlin is explicit. Even a conversion from Int to Long has to be done explicity. Scala's implicits are one of the major culprits for long compilation times. I once wrote an article about it: https://dzone.com/articles/implicits-scala-conversion. Then they are a real problem for code readability if over used. There was once a thread on Scala's user group where someone was asking for a special debugger to debug what implicit conversions are happening at runtime to better understand what is going on after compile time. Unhappily, I didn't note down the link, but you should be able to google it. Several people replied and said this would seem a good idea to them and this really scared me. My understanding of Scala is nevertheless not sufficient to say how problematic implicits really are. As I understand it they are mostly needed to create the blend of OOP and FP. If you are not heavily into FP like the Kotlin guys (There is a remark by Andrey Breslav saying something similar to this: "FP solves many problems easily, sometimes at a high price") implicits is not something you need. And I don't think they were a good idea in Scala to begin with.

– Oliver

7 Likes

I just found it: https://groups.google.com/forum/#!topic/scala-user/OBEFMIYvO98

"Hi,

Are  there any tools for debugging implicits? Other than getting the  compiler to dump out gigabytes of logs? I want a 'debug implicits'  console session were it lets me recursively explore what implicits it  considered and let me say which I think it should have used, and it  tells me why it couldn't use them. (...)"
1 Like

There is another kind of usage I see for this feature. It would be very convenient for modeling state sets. Currently I use sealed classes for this:

sealed class UserScreenState {
    class Loading : UserScreenState()
    class Data(val data: User) : UserScreenState()
    class Error(throwable: Throwable) : UserScreenState()
}

And these sets share some states, but since there is no inheritance for sealed classes I have to type the same states again and again:

sealed class FriendsScreenState {
    class Loading : FriendsScreenState()
    class Data(val data: List<User>) : FriendsScreenState()
    class Error(throwable: Throwable) : FriendsScreenState()
    class Empty : FriendsScreenState()
}

With union types I could do this:

class Loading
class Empty
class Data<T>(val data: T)

union UserScreenState = Loading | Data<User> | Throwable
union FriendsScreenState = Loading | Data<User> | Throwable | Empty

The benefits here are greater code reuse and less visual clutter (I mean these FriendsScreenState() calls in sealed class version).

10 Likes

How do you envision this to be represented on the JVM?

In my understanding there may be no need to represent it on the JVM: these types can be just erased to Any after compilation. I think that these types themselves are only needed to make sure that nothing wrong is passed to the functions, and also for the when operator to be exhaustive, and both of these are compile-time mechanisms.

1 Like

One of Kotlin’s key design goals is 100% Java interop. This means that a method with a parameter of such a union type needs to be callable from Java. If the parameter was erased to Object, then the Java callers would be able to pass arbitrary objects to it, and Kotlin would need to enforce the correct use of the API at runtime, not at compile time.

3 Likes

It’s not that different from non-nullable types, they also are enforced at runtime at the public API boundary.

3 Likes

I’m not sure if it’s possible, but maybe you could generate an overload for each of these union type variants?

2 Likes

I was thinking the same thing - Ceylon has union types, so there must be some way to manage it. But Ceylon also doesn’t allow method overloading, and 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.

Of course, the compiler could also catch that and say “Hey, this isn’t supported, chose one or the other.”

Also, +1 for union types. I would love to see those added too.

3 Likes

Aside from overloads, the only other alternative I’d see would be generating a method that just takes an Object and has a switch statement inside that executes code based on type matches; if it doesn’t match, you throw an exception. Downside of course is you lose any kind of type safety on the Java side of things - someone would have to know ahead of time what are valid types to pass.

2 Likes