Union types

Relating to previous example, as for how general Kotlin union types could be implemented from technical point of view, especially for types from another modules we don’t control, a compiler could emit synthetic sealed classes for each union type. More less like this:

fun fetchJsonObject(): JSONObject | FetchJsonError {
	return when {
		no internet -> FetchJsonError.NoInternet
		timeout -> FetchJsonError.Timeout
		bad json -> FetchJsonError.BadJson
		else -> jsonObject
	}
}

could be compiled to this:

@JvmSynthetic
sealed class fetchJsonObject$union {
	data class JSONObject$unionType(val value: JSONObject): fetchJsonObject$union
	data class FetchJsonError$unionType(val value: FetchJsonError): fetchJsonObject$union
}

fun fetchJsonObject(): fetchJsonObject$union {
	return when {
		no internet -> FetchJsonError$unionType(FetchJsonError.NoInternet)
		timeout -> FetchJsonError$unionType(FetchJsonError.Timeout)
		bad json -> FetchJsonError$unionType(FetchJsonError.BadJson)
		else -> JSONObject$unionType(jsonObject)
	}
}

So, it’s pretty doable in my opinion.

5 Likes

I do not understand, how this definiton can be used to create a variable, which contains either a JsonString or a JsonNumber. This does not compile:

var pi: JsonValue
pi = JsonValue.JsonString("pi")
pi = JsonValue.JsonNumber(3.14)
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)
}

fun main() {
  var pi: JsonValue<Any?>
  pi = JsonValue.JsonString("pi")
  pi = JsonValue.JsonNumber(3.14)
}

You just need to specify the type as JsonValue<Any?>

1 Like

That’s a shame. Union types could make this language perfect for functional programming. I really hope you’ll reconsider this.

6 Likes

Maybe Union type could be done via code generation (KSP)?
I am not an expert, but I see that’s the solution of Dart/Flutter community.

It’s worth noting that this design makes sense mostly in the situation where the class/package (Error in this case) is able to decide in advance which components the union type consists of. In other cases the union types might be better to be defined in the client/user code where the ad-hoc composability of the proposed language enhancement is much more flexible.

For instance, the might be many different Vehicle classes (Car, Truck, Bike, etc.) and various users might decide how they want to classify the classes, e.g. by number of wheels, person capacity, speed buckets, etc.

1 Like

This discussion has been going on a long time and is quite lengthy so my use case may already be well represented, especially by JSON-specific use cases.

I’m writing an application that consumes an XML-RPC API, and the XML-RPC spec prescribes a set of allowable types similar to JSON: int, string, double, boolean, array, struct, and null.

Right now I have these represented as a pile of sealed types that looks more-or-less like this (left out @Serializable and @JvmInline annotations where applicable for the sake of brevity):

sealed interface ValueType
sealed interface PrimitiveType : ValueType
sealed interface CompositeType : ValueType

value class StringType(val value: String) : PrimitiveType
value class IntType(val value: Int) : PrimitiveType
value class BooleanType private constructor(val value: Int) : PrimitiveType
value class DoubleType(val value: Double) : PrimitiveType
object NullType : PrimitiveType

data class ArrayType(val data: List<ValueType>) : CompositeType
data class StructType(val members: Map<String, ValueType>) : CompositeType

Right now I’m trying to add an extra layer on top of StructType to help unboxing members so application code can work directly with builtin types instead of ValueType. Unfortunately the API so far is very boilerplate-y with a set of functions like:

fun StructType.intOrNull(key: String): Int? {
    require(key in members) { "No member named $key" }
    return members[key]
}
fun StructType.doubleOrNull(key: String): Double? ...
fun StructType.booleanOrNull(key: String): Boolean? ...
fun StructType.stringOrNull(key: String): String? ...

where each on is identitcal except the return type.

If instead I was able to do something like this (which I think is some combination of many other suggestions in this thread):

typealias XmlRpcValue = Int? | Double? | Boolean? | String? | List<ValueType>? | Map<String, ValueType>?

it would not only make my “unboxing” API easier, but also (probably) allow me to drop my pile of sealed types all together and work directly with builtin types.

Is this still needing discussion? You can interface with Java using an approach similar to what GraphQL and others use and implement a sealed interface with decorator sub-classes that use functions differentiating when the sub-class is set. Just give us at the Kotlin api level the ability to actually have a necessary abstract math concept so that we might better implement behavior as we see fit.

Kotlin is already better than Java we don’t need to be constrained by their poor design decisions. Lets go beyond and make Kotlin the language of pure abstraction

One of the reasons I’m starting to use Rust is Union types (called enums in rust Defining an Enum - The Rust Programming Language). The type system in rust is more powerful, but a similar solution would be much appreciated.

Those are just sealed classes, a.k.a discriminated unions. I believe this discussion is about indiscriminate unions

3 Likes

I can’t see why union type would be a “poor design decision”
It would generalized kotlin nullable syntax. String? is nothing more than String|Null (union type between String and Null)
And union type is not available in Java; but, at least, in scala3, typedscript

1 Like