[swift-evolution] Union instead of Optional

Thorsten Seitz tseitz42 at icloud.com
Mon May 16 12:12:56 CDT 2016


> Am 16.05.2016 um 17:29 schrieb Michael Peternell via swift-evolution <swift-evolution at swift.org>:
> 
> 
>> Am 16.05.2016 um 12:07 schrieb Austin Zheng via swift-evolution <swift-evolution at swift.org>:
>> 
>> Precisely. To me unions are to enums with associated type the same way tuples are to structs. One is named, has well-defined semantics, can conform to protocols, can have complex internal structure, can have methods, etc. The other is ad-hoc and lightweight, easy to define at the site of use, best suited for simple purposes, has special syntax to support it. 
>> 
>> Even if we can extend tuples in the future, though, I wouldn't want structs to go away. When exceeding some level of complexity structs are just more explicit than tuples, and therefore easier to understand.
>> 
>> Finally, please note that Ceylon is a pervasively object-oriented language with a single root superclass. Neither of those is true for Swift, which chooses to solve a lot of problems in a different (and I would argue, superior) way. So solutions that might work well in Ceylon might not be suited for Swift, at least not without modification, and vice versa. The core team could certainly have chosen to model Swift's type system after that of e.g. Scala, but they chose not to, and I think they did so for good reason.
> 
> Swift has a root class, it is called SwiftObject and it's visible from Objective-C ;) Just thinking about it.. it makes sense: the whole reference-counting-stuff has to live somewhere in the object, and there has to be some kind of isa-pointer to allow for subclassing (and Objective-C interoperability). So there is some common behavior regarding all classes defined from Swift (they all implement retain, release, autorelease, isEqual:, class, respondsToSelector:, the whole NSObject-protocol...) => what I want to express: Java has a root-class, C++ has no root class - that's uncontested; but Swift is somewhere in-between IMHO.
> 
> And for enums and unions.. I think they are different. enums are "sum-types" and unions are... well... "union-types", when you think of data types as sets. E.g. if A, B are data types (sets), then an enum that can be either anA(A) or aB(B) can be thought of as the set A+B (and if A \intersect B \not\eq \emptyset, you can think of this as (0,A)\union (1,B) ). A union B is not the same. In C, the unions are not even safe, because they are not discriminated in any way. In Swift they make an isomorphic set if A and B are disjoint. If A=String and B=Int, A `enum` B is isomorphic to A `union` B (sorry for abusing Haskell syntax :-/ ). But if A=Iterable<String> and B=Array<String>, they are not the same, because now A is a superset of B. So A `enum` B is a strict superset of A `union` B (== A). I can already imagine weird bugs coming to the surface from this distinction, all of which can be solved by disallowing union types altogether.

Ceylon’s type system handles these things nicely. Iterable<String> | Array<String> would reduce to Iterable<String>, for example, and when switching over union types the type checker knows when types are not disjoint (disallowing the switch) or whether the switch is exhaustive.

For more details, see http://ceylon-lang.org/documentation/1.2/tour/types/

> 
> Therefore I think that unions are not worth the trouble at all. And they are even possible right now: Instead of writing
> 
>    union MyUnion { Int, String }
>    // or typealias MyUnion = (Int | String) // ?
> 
> you'd have to write
> 
>    protocol MyUnion {}
>    extension Int: MyUnion {}
>    extension String: MyUnion {}
> 
> The two definitions are basically equivalent, and the second is already valid Swift.

Alas, that is only possible for explicit named unions but the power of type unions lies in their „ad hoc“-ness, e.g. in generic unions like

func union<T, U>(a: Set<T>, b: Set<U>) -> Set<T | U> { … } 	// Note: all examples in hypothetically extended Swift syntax instead of Ceylon syntax

extension Dictionary {
	func getOrDefault<Default>(default: Default) -> Value | Default	// in the general simple case Default == Value, which results in Value | Default == Value
}

Ceylon models the concept of empty and non-empty streams by defining

protocol Iterable {
	associatedtype Element
	associatedtype Absent : Null = Null 	// Null is the type containing a single value null (= nil in Swift); Ceylon’s optionals are just type unions T? == T | Null
	
	var first: Absent | Element { get }
	var rest: Iterable where Iterable.Element == Element, Iterable.Absent == Null

	// Produces a non-empty stream with a given initial element, followed by the elements of this stream, in the order in which they occur in this stream.
	func follow(head: Other) -> Iterable where Iterable.Element == Element | Other, Iterable.Absent = Nothing
	…
}

This means that
	Iterable // with the default Absent = Null
can be empty, whereas
	Iterable where Absent == Nothing
cannot be empty because Nothing is the bottom type (the intersection of all types) with no elements, i.e. the union Nothing | Element == Element and therefore `first` always answers an element!

An example for intersection types in Ceylon (which requires that optionals are modeled as type union T | Null where Null is not a subtype of Any):

protocol Collection {
	func coalesced() -> [Value & Any] // answer an array containing all non nil elements
}

Just some examples demonstrating a bit of the beauty of type unions and type intersections if baked into the language and embraced by the standard library.

-Thorsten

> 
> I have used unions a few times in C though, e.g. for converting from void* to int or stuff like that, at a time when I didn't know about reinterpret_cast (Swift: unsafeBitcast) yet. The few occasions where they are use in the C standard library, they are always discriminated, like a union between a struct(int type, float foo, ...) and another struct(int type, char bar[20], ...) where the type parameter is in both values and can be used to distinguish them. In Swift they are always distinguishable because of runtime type information (except for the case described above), so you can as well just use an enum and make that information obvious. To unpack the information you can use `if let` when dealing with a union, and you can use `if case` or `switch` when dealing with an enum. I don't see how unions would be more convenient to be worth the trouble. Swift unions would be like (C unions + RTTI (runtime type information)), with RTTI replacing the discriminator element of enums, except when the types overlap. So I doubt that anyone will find a decent (realistic!) use-case that is not just as easily (and conveniently) implemented using enums. I think I just proved that there is no such use case ;) The best way to convince me otherwise would be to provide a realistic piece of example code.
> 
> Regards,
> Michael
> 
> _______________________________________________
> swift-evolution mailing list
> swift-evolution at swift.org
> https://lists.swift.org/mailman/listinfo/swift-evolution



More information about the swift-evolution mailing list