[swift-evolution] union types

Drew Crawford drew at sealedabstract.com
Fri Dec 11 17:22:23 CST 2015


This is not really a well-thought-out proposal, but more of a brainstorm about if there is a good language-level solution.

A problem I frequently run into in Swift is the inability to use generic types as first-class "complete" types.

Let's say that I have a protocol (with an associated types) and some structs that implement it (with different associated types).

//all good feature requests involve factories

protocol Factory {
    typealias Product
    func make() -> Product
    var description : String { get }
}

struct IntFactory : Factory {
    typealias product = Int
    func make() -> Int { return 0 }
    var description : String { get { return "IntFactory" } }
}

struct StringFactory : Factory {
    typealias product = String
    func make() -> String { return "Hello world" }
    var description : String { get { return "StringFactory" } }
}

Now it is easy to work on the underlying IntFactory and StringFactory:

IntFactory().make() //static dispatch
StringFactory().make() //static dispatch

...but how do I write a function that works on either Factory?  

func foo(a: Factory) {
    a.make() //dynamic dispatch
}  
error: protocol 'Factory' can only be used as a generic constraint because it has Self or associated type requirements

I could use generics:

func foo<A: Factory>(a: A) {}

but now I need to bubble up generics all over the stack frame:

func baz<A: Factory>(a: A){bar(a)}
func bar<A: Factory>(a: A){foo(a)}
func foo<A: Factory>(a: A) {a.make()}

class WhyIsthisGeneric<A: Factory> {
    var a: A //because of an implementation detail of Factory, of course
}

I submit that this couples the implementation details of Factory too tightly to unrelated functions and methods (and perhaps entire classes that now become generic so I can create ivars).

Here's what I think is an elegant solution:

typealias EitherFactory = union(Factory, [IntFactory, StringFactory])
let a : EitherFactory = IntFactory()
func baz(a: EitherFactory){bar(a)}
func bar(a: EitherFactory){foo(a)}
func foo(a: EitherFactory){a.make()}

The union function being a new builtin, that causes the compiler to automatically write this type behind the scenes:

enum EitherFactory {
    case intFactory(IntFactory)
    case stringFactory(StringFactory)
    
    func make() -> Any {
        switch(self) {
        case .intFactory(let f):
            return f.make()
        case .stringFactory(let f):
            return f.make()
        }
    }
    
    var description : String {
        get {
            switch(self) {
            case .intFactory(let f):
                return f.description
            case .stringFactory(let f):
                return f.description
            }
        }
    }

    var intFactory? : IntFactory {
        switch(self) {
            case .intFactory(let f):
            return f
            default:
            return nil
        }
    }
    var stringFactory? : StringFactory {
        switch(self) {
            case .StringFactory(let f):
            return f
            default:
            return nil
        }
    }
}

This generated type is fully-specified, and so it may be used in any place a first-class type is allowed.

Arguments in favor of this proposal:

1.  It allows protocols with Self or associated type constraints to be promoted to "full" types in many practical usecases, (specifically, when the list of types can be enumerated, and this is always the case for protocols with private or internal visibility).  
2.  It allows the user to opt into the simplicity of dynamic dispatch with their generic types
3.  Since the boilerplate is automatically generated, it updates automatically for new functions and methods added to the protocol, whereas my current solution is tedious, manual, and error-prone
4.  The semantics allow for a (future) optimizer to optimize away the boilerplate.  For example, if we write

let a: EitherFactory = IntFactory()
func foo(a: EitherFactory){a.make()}
foo(a)

     Our optimizer may emit a specialization "as if" I had bubbled generics:

let a: IntFactory = IntFactory()
func foo_specialized_intFactory (a: IntFactory){a.make()}
foo_specialized_intFactory(a)


    Under this optimization the switch statement and the dynamic dispatch are eliminated.  So the semantics allow "at least" dynamic dispatch performance, and "up to" static dispatch performance, given a strong optimizer

Motivating case:

This proposal arises (most recently) from the problem of trying to write code that is generic across IPv4 and IPv6.  For example

final class Socket {
    func getsockname() -> ???
}

In the IPv4 case this function should return `sockaddr_in`, but in the v6 case it should return `sockaddr_in6`.  So this socket can only be represented as a protocol with associated type requirements, and so it cannot be trivially used e.g. as a function parameter, as an ivar, etc.  This significantly complicates the implementation.

Incompleteness:

The full semantics of the union builtin are underspecified.  

1.  In the example, `make() -> Int` and `make() -> String` unify to `make() -> Any`, but would `sequence() -> CollectionType` and `sequence() -> SequenceType` unify to  `sequence() -> Any`?  Perhaps not.
2.  What is the behavior if the arguments/return values of a function are themselves unions?

And finally, would it be a better idea merely to promote generics to "full" types, without the use of an explicit union builtin?  The approach here is more narrowly tailored, but that is not necessarily the right language design.



-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20151211/3c09c37d/attachment.html>


More information about the swift-evolution mailing list