Union types


#63

I like this example, specifically because it also covers structural typing. Agree that the Union should always express N is-able identities.


#64

Not for overload languages like kotlin, they don’t need to pattern match over the type off a union, just overload a function over all type parameter, then apply it to a value of Union type parametrized by these type parameters:

fun(a:A)=(a,)
fun(b:B)=(b,b)
fun(c:C)=(c,c,c)

x: A | B | C = new A()
fun(x) //valid, not pattern matching or type classes needed.

It would work in my eyes but it would break backward compatibility in source.

No, traits/typeclasses/concepts/protocols/interfaces denote unbounded existentials whereas union types are bounded.
You save only dispatching for the methods of a trait but not for foreign methods which is the default case when pattern matching over unions.
Then, you can’t even overload over all possible types of a typeclass because you may never know how many member it houses.
You can of course create new typeclasses to save pattern matching but it is far more tedious then simple overloading over the type parameters of a union type.


#65

That looks super wrong because with the current Kotlin version, an overload is resolved strictly at compile time. Doing this dynamically at runtime depending on the actual type looks very weird to me and is difficul to implement as there are lots of weird edge cases. Like what happens if an object implements both A and B.


#66

True and it shouldn’t be different for union types. What happens here is that the compiler tries to find fun with type A | B | C -> T but it doesn’t exists.
Then the compiler tries to find a signature of fun for each type parameter of the union type and succeeds, here.
But because it doesn’t know at compile time of which type x really is, it pattern matches for you automatically with:

if(x instanceOf A) { call fun:A->(A,)((A)x)}
else if (x instanceOf B  { call fun:B->(B,B)((B)x)}}
...

It is exactly that what you try to do manually, but the compiler can automate it.

Yes, union types are non disjoint if overlapping occurs, then an appropriate convention has to be defined. What do you do if fun is overloaded with Interface I1 and I2 and fun is applied to i implementing both interfaces?
Similar problems arrive with union types, we could choose for the first match or we could throw an type error.

But because we have already intersection types for functions (method overloading) and for interfaces both inherently causing ambiguities, why we shouldn’t also make use of union types.


#67

As such errors currently only happen at compile time, we just fail the compile and force the programmer to cast to a more specific type.

Doing this at runtime is much more problematic, as the type AB that implements both A and B could be dynamically created or loaded at runtime.

Sure, you could just translate this into chained instanceof calls and ignore ambiguous types, but I don’t feel that that would be the right approach.


#68

You can do then same for union types, if an object implements both interfaces, then you can emit an compiler error becaue of ambuigity.

You go for the worst case, if this can be true at runtime you emit an compiler error at compile time a.k.a path dependent typing


#69

No you can’t. Unless you really restrict union types to final/closed/sealed classes there is no way that you can prevent someone from implementing multiple interfaces or extending a class and implementing an interface. Say you have

fun getResult() : String|Throwable = TODO()

fun handleResult(foo: String) = TODO()
fun handleResult(foo: Throwable) = TODO()

handleResult(getResult())

That would work as String and Throwable are both classes and you can’t extent multiple classes. However

fun getResult() : List<String>|Throwable = TODO()

fun handleResult(foo: List<String>) = TODO()
fun handleResult(foo: Throwable) = TODO()

handleResult(getResult())

would not work, as someone could create an exception class that also is a List<String> (to iterate over error messages or whatever), and they could create it after you compiled your handleResult code. So you wouldn’t know there could be a possible conflict.

You go for the worst case, if this can be true at runtime you emit an compiler error at compile time a.k.a path dependent typing

that would make it almost useless, as in larger projects you usually code with interfaces a lot.


#70

On your example, you need to have a function that treat the declared result :

fun getResult() : List<String>|Throwable = TODO()
fun handleResult(foo: List<String>|Throwable) = TODO() 
handleResult(getResult())

until you check the type

fun getResult() : List<String>|Throwable = TODO()

fun handleResult(foo: List<String>) = TODO() 
fun handleResult(foo: Throwable) = TODO()    

val x = getResult()
if (x !is Throwable) {
    handleResult(x)  // call handleResult(foo: List<String>)
}

But the strength of union type adopt by typescript, ceylon and the next scala version, is for types (like examples show in precedent comments)

// Let be properties type be a map of key string and value (not simple object)
val propreties : Map<String, String|LocalDate|Number>

Others examples with json type.


#71

True, so maybe you don’t want to treat String | Throw as a class :), in the end it is still a class but it is not visible in the abstraction. In your case above, you can’t simply pass a union type into a non union type, they are different by design (and even don’t stay in a subtyping relationship), instead you copy out the reference typecased by type id and cast it to the appropriate target type.
This should be possible in the JVM, for illustration by:

UnionOfStringAndThrowable //generated by compiler
{
 value:Object;
 tag:TypeTag;
 getValue():T=(T)value; 
}

Now, every time you retrieve an value out of this union you pattern match over the current type tag, the order known by the compiler:

if(unionValue.tag == TypeTag.String)
{ 
     fun(unionValue.getValue():String);
}
else
{ 
     fun(unionValue.getValue():Throwable)
};

The point is that every type system needs to know some ‘actual’ type of an expression even if this expression satisfies more than one type assertion.
So, in the end your foo Object is either a T extends Throwable or a T implements List<String>, but not both at the same time.
We could argue that u:Int|Float=2 is of type Int|Float and we don’t know how to destruct it correctly as Int or Float, respectively but 2 is per default an integer and our union type has a type tag to remember for.
Regarding the abstraction, you are right, but because we have one single type tag in the implementation we can reconstruct the initial type choices we made.

The situation changes considerably for concrete types like Int, they aren’t shipped with type tags and it wouldn’t be tractable further to do so by means of a combinatoric explosion.
So, if we allow Int to be regarded as NegativeInt | ZeroInt | PositiveInt we run into troubles and we need to throw an compile time error or another convention.