Union types

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).

11 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

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

2 Likes

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
    }
}
2 Likes

@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.

4 Likes

@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 - #21 by mikehearn

1 Like