[swift-evolution] [Pre-Proposal-Discussion] Union Type - Swift 4

Xiaodi Wu xiaodi.wu at gmail.com
Fri Aug 19 09:38:25 CDT 2016


Ad-hoc enums have been discussed already, at length, and all the weaknesses
touched on then still apply now. For instance, since they're ad-hoc, can
you pass an instance of type "Int | String" as an argument if the function
expects a "String | Int | Float"? Enums don't have duck typing behavior
like that; if your ad-hoc type does, then it's not very much like an enum;
if it doesn't, it won't feel much like a union type.

Moreover, an ad-hoc "String | Int" may look like a union type, but until
switching over an instance to cast it, you can't invoke any methods common
to String and Int. So it really doesn't feel like a union type at all.

Don't get me wrong--like Brent, I'm not convinced I see a scenario in which
union types would help write clearly better code, just code that is more
"convenient" in the eyes of the beholder. But ad-hoc enums have had their
day on this list, and I'm not sure that re-visiting that discussion is
going to be very fruitful.

In any case, this all seems very, very out of scope for Swift 4.


On Fri, Aug 19, 2016 at 9:07 AM Matthew Johnson via swift-evolution <
swift-evolution at swift.org> wrote:

>
> > On Aug 19, 2016, at 1:20 AM, Brent Royal-Gordon via swift-evolution <
> swift-evolution at swift.org> wrote:
> >
> >> On Aug 18, 2016, at 2:05 AM, Maximilian Hünenberger via swift-evolution
> <swift-evolution at swift.org> wrote:
> >>
> >> While purpose of the types are clear in this case there is not only
> intersection. I also want to find out the distance between different
> GeometryTypes and other properties like angels between two lines or a Line
> and a Plane but this doesn't make sense for a Point and some other
> GeometryType.
> >
> > But there are extremely complicated interactions between different pairs
> of types:
> >
> >       Line.intersection(with: Line) -> Void | Point | Line
> >       Line.intersection(with: Circle) -> Void | Point | (Point, Point)
> >       Line.intersection(with: Polygon) -> [Point | Line]
> >
> >       Circle.intersection(with: Line) -> Void | Point | Line
> >       Circle.intersection(with: Circle) -> Void | Point | (Point, Point)
> | Circle
> >       Circle.intersection(with: Polygon) -> [Point | (Point, Point)]
> >
> >       Polygon.intersection(with: Line) -> [Point | Line]
> >       Polygon.intersection(with: Circle) -> [Point | (Point, Point)]
> >       Polygon.intersection(with: Polygon) -> [Point | Line] | Polygon
> >
> > What exactly are you planning to do with a `[Point | Line] | Polygon`?
> Honestly, your only real option is to test it for specific subtypes and try
> to use them. But there's already a type for that kind of thing: an enum.
> Enums are better for this application because they lend more structure and
> allow you to explain cases with descriptive labels:
> >
> >       enum PossiblyCoincidental<CoincidentalType, IntersectingType> {
> >               case coincidental (CoincidentalType)
> >               case intersecting (IntersectingType)
> >       }
> >
> >       typealias LineIntersection = PossiblyCoincidental<Line, Point>
> >
> >       enum CircleIntersection {
> >               case tangent (Point)
> >               case secant (Point, Point)
> >       }
> >
> >       Line.intersection(with: Line) -> LineIntersection?
> >       Line.intersection(with: Circle) -> CircleIntersection?
> >       Line.intersection(with: Polygon) -> [LineIntersection]
> >
> >       Circle.intersection(with: Line) -> CircleIntersection?
> >       Circle.intersection(with: Circle) -> PossiblyCoincidental<Circle,
> CircleIntersection>?
> >       Circle.intersection(with: Polygon) -> [CircleIntersection]
> >
> >       Polygon.intersection(with: Line) -> [LineIntersection]
> >       Polygon.intersection(with: Circle) -> [CircleIntersection]
> >       Polygon.intersection(with: Polygon) ->
> PossiblyCoincidental<Polygon, [LineIntersection]>
> >
> > This is more complicated, but it's also a lot clearer about what each
> return type actually means. The existence of rich type information also
> helps you add functionality:
> >
> >       protocol IntersectionType {
> >               var isEmpty: Bool { get }
> >       }
> >
> >       extension CircleIntersection: IntersectionType {
> >               var isEmpty: Bool { return false }
> >       }
> >
> >       extension PossiblyCoincidental: IntersectionType {
> >               var isEmpty: Bool: { return false }
> >       }
> >
> >       // Cascade inward if IntersectingType happens to itself be an
> intersection.
> >       extension PossiblyCoincidental where IntersectingType:
> IntersectionType {
> >               var isEmpty: Bool {
> >                       switch self {
> >                       case .coincidental:
> >                               return false
> >                       case .intersecting(let intersection):
> >                               return intersection.isEmpty
> >                       }
> >               }
> >       }
> >
> >       // Note: Using future conditional conformances
> >       extension Optional: IntersectionType where Wrapped:
> IntersectionType {
> >               var isEmpty: Bool {
> >                       return map { $0.isEmpty } ?? true
> >               }
> >       }
> >
> >       // Retroactive modeling yay!
> >       extension Array: IntersectionType where Element: IntersectionType
> {}
> >
> > Of course, it might be the case that this is *way* more information than
> you really need, and you just want to say:
> >
> >       GeometricElement.intersection(with: GeometricElement) ->
> [GeometricElement]
> >
> > But if you want something simple and uniform, you probably don't want
> the complex `Void | Point | Line`-type stuff, either. Union types are
> neither simple and uniform, nor complex and descriptive; they are neither
> fish nor fowl. I just don't see a strong reason to prefer them here.
> >
> >       * * *
> >
> > I'll ask again what I think has been the crux of this argument from the
> beginning, and what hasn't really been satisfactorily answered in several
> months of discussions.
> >
> > **What use cases are better served by union types than by the
> alternatives?**
> >
> > Can you show us code where union types are clearly better—not just
> shorter—than an equivalent design based on (depending on the need)
> protocols, enums, or overloading? What about with minor extensions to these
> features, like closed protocols (allowing for exhaustive checking of
> protocol types) or implicit type lifting for enums (allowing you to avoid
> explicitly constructing the case you need, as Optional.some does)?
>
> I think implicit lifting with some additional syntactic sugar would be a
> very good solution here.
>
> First, an example of an enum with implicit lifting (using the strawman
> `autolift` modifier which conservatively could be restricted to cases with
> a single associated value and could not be used on cases with associated
> values whose type is also the type of an associated value in another case):
>
> enum Foo {
>     autolift case .string(String)
>     autolift case .int(Int)
>     // etc
> }
>
> func bar(_ foos: [Foo]) {}
>
> // “a string” is implicitly lifted to be .string(“a string”) and 42 is
> implicitly lifted to be .int(42)
> bar([“a string”, 42])
>
> This would solve the use case I have run into where I was working on a
> library design and wanted to accept a heterogeneous collection without
> requiring callers to have any knowledge beyond what types can be uses (i.e.
> I don’t want them to have to deal with the syntactic noise of manual
> lifting, but more importantly I would consider an enum like this an
> implementation detail, not something users should really rely on).
>
> If we *are* going to support implicit lifting like this, why not also
> adopt syntactic sugar for creating / referencing them in an ad-hoc manner?
> Rather than writing out the enum above, we could just declare:
>
> func bar(_ foos: [String | Int]) {}
>
> The enum would be created “on demand” each time a distinct set of types
> was referenced, with implicit lifting for each case.  What is the downside
> of this syntactic sugar?  It would bring the “lightweight” feel of union
> types to Swift while retaining the semantics of enums.  This feels like a
> nice middle ground to me.
>
> As you mention, closed protocols are another possible solution to the
> design problem I was working on, although it is somewhat less elegant as it
> would add conformances to the underlying types which is not necessarily
> desirable.
>
> >
> > There is clearly overlap between union types and some of our other
> features. When do you expect people would use each feature? Are there other
> languages which support both union types and sum types (i.e. Swift
> `enum`s)? When do they use each of these?
> >
> > (Incidentally, even if we had union types, I don't think we'd want to
> change Optional's definition. Nested optionals are a bit confusing at
> times, but they're important for correctness in many cases, like
> collections of Optionals. If Optional is a union type, you also lose the
> opportunity to have operations like `map` and `flatMap` on it.)
> >
> > --
> > Brent Royal-Gordon
> > Architechies
> >
> > _______________________________________________
> > swift-evolution mailing list
> > swift-evolution at swift.org
> > https://lists.swift.org/mailman/listinfo/swift-evolution
>
> _______________________________________________
> swift-evolution mailing list
> swift-evolution at swift.org
> https://lists.swift.org/mailman/listinfo/swift-evolution
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20160819/aa71cadf/attachment-0001.html>


More information about the swift-evolution mailing list