Swift Generics Evolution

Earlier this week, Joe Groff of the Swift Core Team published a massive discussion post on the Swift forums. It discussed a lot of possible changes to the way that generics work in the Swift language, and kicked off the process with a link to SE-244, a proposal to introduce some features around function return values. The post as a whole was an absolutely fascinating read, and made a really compelling case for some powerful new ideas that might come to Swift.

In discussing this post with some colleagues, though, it’s become clear that the intended audience is a little more embedded into the theory and technical details of programming language evolution than your average Swift developer. There’s nothing wrong with that — it’s reassuring to know that the folks driving changes in Swift have a solid background in language design, and that they’re thinking about all manner of hard problems in order to make our lives easier. However, it makes me worry that people might be missing out on a truly exciting conversation about what might be coming in a future Swift version.

To that end, this post aims to walk through some of the proposals from Joe’s document, explaining the syntax and offering examples of how the changes to generics might look in practice. We’ll pick up a couple technical terms in a practical setting, and wrap up with some details about the open Swift Evolution proposal(s) being considered.

Ideas Taking Shape

Before we dig into the proposed changes, let’s establish an example right up front. We want something that’s complex enough to grow and change, but we should avoid the mental overhead of something as complex as Collection, with all its Iterators and Elements and other protocol conformances.

Instead, I’ll borrow an example from SE-244. Let’s start with the very basics of a drawing or diagramming app; we can imagine that we have a blank canvas, and the means to put different shapes, lines, or even text in different places. In true protocol-oriented programming fashion, instead of building a huge class hierarchy for these different elements, let’s start with a protocol.

protocol Shape {
    // Render the shape into the current graphics context
    func draw()
}

struct Rectangle: Shape {
    var width: Float
    var height: Float

    func draw() {  }
}

struct Circle: Shape {
    var radius: Float

    func draw() {  }
}

The Shape of the World

This is enough to get us started with some of the existing generics features already in Swift 5. For example, we might have a helper inside a Canvas object that draws a single Shape at a point:

func render<T: Shape>(_ shape: T, at point: Point) {  }

We might have another helper that adds a new Shape of some type to our Canvas, returning the new instance:

func addShape<T: Shape>() -> T

This is probably familiar ground to Swift developers who have used generics even a little bit. The syntax — the way we type out the function definitions, the use of angle brackets, and the conventional use of T for a generic type — is also probably recognizable to folks familiar with generics in other languages, like Java or C♯.

These are both examples of type-level abstraction. Each of these functions has a placeholder type T; all we know is that T has to conform to Shape. Each call site then gets to pick what concrete type is bound to T, making these functions very flexible and powerful in a variety of situations.

let circle: Circle = addShape()
  // T is inferred to be Circle by the type decoration

render(circle, at: Point(x: 10, y: 10))
  // T is inferred to be Circle by the first argument type

All this happens at the function (or type) level. By contrast, when we start dealing with individual variables, we get into the opposite: value-level abstraction. Now we’re not concerned with making general statements about the types that can be passed into or out of a function; instead, we’re only worried about the specific type of exactly one variable in one place.

var shape: Shape = Rectangle()
  // shape is of type Shape, even though we know it's "really" a Rectangle

shape = Circle()
  // The type of shape is still Shape, even though it's now "really" a Circle

In this example, we would only be able to use Shape methods on the shape variable, no matter what type it “really” is at any given moment. Properties like shape.width or shape.radius are unavailable, even when the shape is really a Rectangle or Circle, because we’ve declared that the variable’s type is constrained only by the Shape protocol.

This leads us to perhaps the most difficult idea in the current generics system: existential types. So far, it seems like we’ve had only three types declared: the Shape protocol, and the concrete Circle and Rectangle, which both conform to that protocol. However, in reality, a fourth type snuck into this last example when we weren’t looking. This type was an existential for Shape.

In can be helpful to think of existential types like wrappers or boxes for other types. When we declared the shape variable, and gave it the type annotation : Shape, what Swift did was set up a variable that could hold on to any concrete type conforming to the Shape protocol. We filled that variable right away with a Rectangle, and later replaced it with a Circle, but in both cases the type of the value was that Shape existential. (Certain bits of the Swift language, like type(of:) or casting with as?, can work through existentials to get at the underlying types.)

This idea — that there’s an extra abstracted type floating around in our code — can be disconcerting at first. This reaction is often compounded by the fact that existentials look very, very similar to protocol types at first. This is what Joe means when he writes:

Also, protocols currently do double-duty as the spelling for existential types, but this relationship has been a common source of confusion.

The best way to distinguish a protocol type from an existential type is to look at the context. Ask yourself: when I see a reference to a protocol name like Shape, is it appearing at a type level, or at a value level? Revisiting some earlier examples, we see:

func addShape<T: Shape>() -> T
  // Here, Shape appears at the type level, and so is referencing the protocol type

var shape: Shape = Rectangle()
  // Here, Shape appears at the value level, and so creates an existential type

Now that we’ve got a good handle on existentials, we can start to dig into the core of Joe’s post: how things might change in the future.

Any Shape You Like

Existentials have long been a part of Swift, but it’s rare that developers have to confront them consciously. In fact, that was one of the goals of the current design:

We gave existential types an extremely lightweight spelling, just the bare protocol name … partially out of a hope that they would “just work” the way people expect; if you want a type that can hold any type conforming to a protocol, just use the protocol as a type, and you don’t have to know what “existential” means or anything like that.

However, there are a few sharp edges that resulted from this decision, so one of the goals laid out in the forum post is to help clarify the difference between a protocol and its existential. The initial proposal involves introducing a new keyword any into the language. This keyword would be required to appear when declaring a variable of existential type, so it would become obvious when the usage differed from the protocol type.

func addShape<T: Shape>() -> T
  // No change, since Shape is used as a the protocol type here

var shape: any Shape = Rectangle()
  // The new keyword `any` distinguishes a variable of existential type

This keyword follows in the footsteps of languages like Rust, which uses dyn in a very similar way. It’s interesting to note that the forum post explicitly argues against dyn or auto, a C++ism, for this feature — but that some of the early commentary has raised questions about the choice of any, so the exact syntax might be a point of contention when this change comes to Swift Evolution.

Hidden Shapes

Beyond changes to existentials, Joe’s post raises several possibilities for improvements in the type-level generics system as well. (In fact, the post offers the any keyword last, and I just flipped the order around here.) One of the most exciting changes up for debate is the ability to have a method return a value of a generic type chosen by the method itself.

This is another one of those subtle distinctions that isn’t immediately obvious. After all, methods can already have generics in their return types, right? We even had one earlier:

func addShape<T: Shape>() -> T
  // Returns T, a generic type conforming to Shape

The trick with this kind of type-level abstraction is that it’s the code calling the function that gets to pick the type. That is, the implementation of addShape() doesn’t get to choose just any type conforming to Shape in its return value; it has to work with a real type coming from each call site.

For this particular function, that’s probably fine — we might expect our hypothetical Canvas to know how to construct and add each concrete kind of Shape. However, there are other cases where we might want the function implementation to be the one choosing the concrete type, not the caller. For example, let’s hypothesize a function that wants to return a Shape that is big enough to encompass all the other existing Shapes. This kind of thing can be useful when drawing a background, for example, or for visually grouping existing shapes together.

There are a couple ways we could write this. The first and most obvious is to return the exact type that we use for the background shape.

func allEncompassingShape() -> SpecialGiantShape

However, that means exposing some implementation details of allEncompassingShape(), including a type declaration for the concrete shape that’s returned. While this might be OK in some circumstances, there are definitely times when — for one reason or another — we don’t want to have to expose those types. Let’s say this is one of those times, and having callers know about the existence of SpecialGiantShape is undesirable. Instead, we can try to cover it up with generics.

func allEncompassingShape<T: Shape>() -> T

The trouble is, just like addShape() above, this function’s return type is picked by the caller — and that might not be right! If the function wanted to continue using SpecialGiantShape, but was declared with generics this way, a caller could produce a type mismatch for the result value.

let background: Circle = allEncompassingShape()

What we want is a middle ground that lets allEncompassingShape() hide the specifics of what type it’s returning, but also not let the caller have much say in the matter.

The proposal on the table is to add something that behaves a lot like a generic type, but lets the implementation choose the concrete type, and only inform the caller about some protocols that type conforms to. The first syntax proposal for this feature has colloquially been called “reverse generics.” It introduces a new place that angle-bracketed generic types could appear: to the right of the return arrow in a function definition.

func allEncompassingShape() -> <T: Shape> T

In this situation, it’d be allEncompassingShape() that picks T, rather than the caller. Together with the any keyword from before, we might stash the result of calling this function into a variable with existential type, or let Swift infer that for us.

let background: any Shape = allEncompassingShape()
let otherBackground = allEncompassingShape() // still `any Shape`

The reason for the label “reverse generics” is that the flow of information is backwards from the existing system. Where right now, the caller binds generic types as it calls a function, the proposal would have the function itself bind the return types and pass concrete values back out.

Angling for Change

This sort of reverse-generics idea is already, on its own, quite powerful. It would fill in what Joe calls a “hole in the feature matrix for method API design” by allowing a new kind of abstraction, which in turn allows framework and library authors much more flexibility in their designs. The downside, though, is that it exacerbates one of the common complaints about Swift syntax: the prevalence of angle brackets in generics syntax.

So far, our examples have been fairly tame; we’ve had a function with a generic argument, or generic return, but not more than one of either. In practice, though, it’s easy to let generics get out of hand. We could imagine that our graphics program, like most, offers the ability to combine or “union” shapes. If we wanted this to work with arbitrary pairs of shapes, we’d need to write a function that accepted multiple generic parameters — and if we want the function to control the kind of Shape it returns, we’d want this new “reverse generics” approach to defining the return type.

func union<T: Shape, U: Shape>(_ leftShape: T, _ rightShape: U) -> <V: Shape> V

Already, the list of one-letter abbreviations and multiple angle-bracketed declarations are a lot to handle, and it gets even worse when the protocol in question has associated types. Let’s take a look at an example by extending our original Shape protocol somewhat.

Early on, we mentioned the possibility for our canvas to have regular shapes, but also text and images. Instead of a plain draw() method in the protocol, what if each of these kinds of Shape had their own Renderer, which was responsible for doing the drawing? That way, we could use a text view or related Core Text class to render text, some bitmap helpers to render raster images, and another library to handle vector shapes. Revisiting our very early Shape examples, we might refactor to express this to Swift.

protocol Shape {
    associatedtype Renderer
    var renderer: Renderer { get }
}

Now, going back to our union(_:_:) function, we might make the argument that you should only be able to combine shapes that would use the same type of Renderer. After all, you can blend bitmap images, or combine vector paths, but it’s not immediately obvious how to treat the union of text and a vector image. (There are some answers, but those are beyond the scope of a Swift type system discussion.) Let’s try to express this using the angle bracket syntax.

func union<T: Shape, U: Shape>(_ leftShape: T, _ rightShape: U) -> <V: Shape> V
    where T.Renderer == U.Renderer, U.Renderer == V.Renderer

This just keeps getting more and more extensive. Not to worry, though — the discussion post has some proposals to ease this syntax as well, starting by cleaning up those angle brackets.

Much like we were able to help distinguish existentials with a new keyword any, the idea here is to use a related keyword to smooth over the need to declare a single-letter generic type with a constraint. And since we want to communicate that the function will get some single type at call time, the immediate keyword proposal is some. This would make our generic-return function declarations shorter right away.

func allEncompassingShape() -> some Shape
  // The basic case: replace `<T: Shape> T` with the less angular `some Shape`

func union(_ leftShape: some Shape, _ rightShape: some Shape) -> some Shape
  // `some` can appear multiple times in a declaration, meaning a new generic type each time

The one last piece of information we’re still missing is the desire to have all the shapes involved in union(_:_:) have the same associated Renderer type. This is where some of the most fervent discussion has happened following Joe’s original post, and where there are a few syntax options up in the air. One is to use the argument labels in place of the single-letter generic types, but leave a lot of the constraint syntax the same. The special keyword return would be able to stand in for the generic return type in this case.

func union(_ leftShape: some Shape, _ rightShape: some Shape) -> some Shape
  where type(of: leftShape).Renderer == type(of: rightShape).Renderer,
    type(of: rightShape).Renderer == type(of: return).Renderer

Some feedback after the post proposed refining the constraint syntax to imply the type(of:) expressions, avoiding boilerplate where the compiler could figure it out.

func union(_ leftShape: some Shape, _ rightShape: some Shape) -> some Shape
  where leftShape.Renderer == rightShape.Renderer, rightShape.Renderer == return.Renderer

This almost comes full circle to the existing syntax, but with some standing in for angle brackets and single-letter generic types — a readability win. However, there’s still a concern about the constraints being too far away from the original types. How obvious was it to you that the some Shape being returned from the function had a limitation on its Renderer, by the time we got to the end of the declaration?

To help keep these constraints closer to their related types, one idea is — believe it or not — to reintroduce angle brackets. If we name a generic type for the Renderer that’s involved all the way through this function declaration, we can add back constraints on each some Shape as it appears.

func union<T>(_ leftShape: some Shape<.Renderer == T>,
              _ rightShape: some Shape<.Renderer == T>)
    -> some Shape<.Renderer == T>

This syntax has all the advantages of the some keyword, which eliminates angle brackets in functions with simple generic arguments or return types. At the same time, it has all the flexibility of the current where-clause constraint system, since it can make rules about associated types right where the generics are used.

A Familiar Shape

After this post was published, some eagle-eyed readers asked how returning some Shape really helps us over the current state of affairs. After all, we can already write something incredibly similar, just without the some keyword.

func allEncompassingShape() -> Shape
  // Legal Swift 5 syntax

The difference is twofold. Under the new proposal, there would be a change both in what type comes back and in what protocols are eligible. Let’s tackle them one at a time.

Remember that right now, we have a choice between two syntaxes: returning plain old Shape or returning T, a generic type chosen by the caller. The first produces a value with a Shape existential type, and the second produces a value with a known concrete type that the function has no say over. If you’re going to write a function like this today, either you have to be able to accept that your caller picks the concrete type and return T, or you have to accept the limits of existential types.

One of these limits is probably very familiar to Swift developers who have spent any extended amount of time with generics: in Swift 5, a function cannot return a protocol existential value if that protocol has associated types. If we worked with the enhanced Shape protocol above, including its Renderer associated type, then we’d actually get an error trying to write our function.

func allEncompassingShape() -> Shape
  // Error: protocol 'Shape' can only be used as a generic constraint
  // because it has Self or associated type requirements

The other limitations are detailed at length in Joe’s post; they primarily center around this kind of type system restriction, plus some deficiencies when dealing with multiple related values of the same existential type. (Very briefly: what happens if we call allEncompassingShape() multiple times, then try to relate their renderers? Can we be assured they’re of the same type?)

This is the hole that some Shape tries to fill between the two existing syntaxes: it lets the function, not the caller, pick the concrete type, and it dodges some holes in the current state of affairs with existentials. If we were to lay out all the choices, including the new proposal, it might look something like the following.

func allEncompassingShape() -> Shape
  // Legal now, but Shape cannot have associated types, and there are other
  // practical concerns that might make this difficult in large systems.

func allEncompassingShape() -> any Shape
  // A potential future wording of the above, with all the same restrictions and pitfalls.

func allEncompassingShape<T: Shape>() -> T
  // Legal now, but the caller gets to pick the actual concrete type bound to T,
  // which might not be what the implementation wants.

func allEncompassingShape() -> <T: Shape> T
  // Future proposal that both avoids the restrictions on existentials and lets
  // the implementation pick the concrete type bound to T.

func allEncompassingShape() -> some Shape
  // A nicer wording of the "reverse generics" return; again, allows the
  // implementation to pick the concrete type, and avoids existential problems.

Anyone interested in more details can check out the Twitter thread ending here, where various smart people hash out the differences at length. Thanks to all involved for bringing this topic further under the microscope.

The Final Shape

Phew! That was a lot to tackle, and we even glossed over some of the most complex or unusual edge cases of the original post. Nevertheless, there’s the kernel of some incredibly powerful ideas throughout these proposals; if the Swift language adopted even half of what’s being discussed, I’d be thrilled.

That process has actually already gotten started, with formal review in progress on SE-244: Opaque Result Types. This proposal covers the “reverse generics” idea and some keyword in return types. If adopted, it would give us the ability to return a concrete type hidden from the caller, indicating only that the returned value conforms to some protocol(s). It’s authored by Joe Groff, who gave us the discussion post summarized here, and Doug Gregor, the author of the original “Generics Manifesto” for Swift years ago. I’m truly excited to see where it goes.

To wrap up, I’d like to reiterate how grateful I am for Swift. The language already felt like a leap forward shortly after it was introduced, and the rapid pace of growth — even if sometimes challenging to keep up with — has made it easier and easier to develop truly great apps. It’s amazing to see the core team continue working on major changes in such a public way. Here’s to another five years!

Sincere thanks to the members of the Seattle Xcoders community for feedback on an early revision of this post.