Issue #888

Embrace Swift generics

This generic pattern is really common, so there’s a simpler way to express it. Instead of writing a type parameter explicitly, we can express this abstract type in terms of the protocol conformance by writing “some Animal”.

The “some” in “some Animal” indicates that there is a specific type that you’re working with.

func feed<A>(animal: A) where A: Animal {}

func feed(animal: some Animal)

An abstract type that represents a placeholder for a specific concrete type is called an opaque type. The specific concrete type that is substituted in is called the underlying type. For values with opaque type, the underlying type is fixed for the scope of the value.

Opaque types can be used for both inputs and outputs, so they can be declared in parameter position or in result position. The function arrow is the dividing line between these positions. The position of an opaque type determines which part of the program sees the abstract type and which part of the program determines the concrete type.

Screenshot 2022-06-09 at 13 30 25
protocol AnimalFeed {
    associatedtype CropType: Crop where CropType.Feed == Self
    static func grow() -> CropType
}

protocol Crop {
    associatedtype Feed: AnimalFeed where Feed.CropType == Self
    func harvest() -> Feed
}

protocol Animal {
    associatedtype Feed: AnimalFeed
    func eat(_ food: Feed)
}

struct Farm {
    func feed(_ animal: some Animal) {
        let crop = type(of: animal).Feed.grow()
        let produce = crop.harvest()
        animal.eat(produce)
    }
    
    func feedAll(_ animals: [any Animal]) {
        for animal in animals {
            feed(animal)
        }
    }
}

struct Cow: Animal {
    func eat(_ food: Hay) {}
}

struct Hay: AnimalFeed {
    static func grow() -> Alfalfa {
        Alfalfa()
    }
}

struct Alfalfa: Crop {
    func harvest() -> Hay {
        Hay()
    }
}

In general, the part of the program supplying the value for an opaque parameter or result type decides the underlying type, and the part of the program using the value sees the abstract type.

func getValue(Parameter) -> Result

For a local variable, the underlying type is inferred from the value on the right-hand side of assignment. This means local variables with opaque type must always have an initial value; and if you don’t provide one, the compiler will report an error. The underlying type must be fixed for the scope of the variable, so attempting to change the underlying type will also result in an error. For parameters with opaque type, the underlying type is inferred from the argument value at the call site.

var animal: some Animal = Horse()
animal = Chicken() // Error !
func feed(animal: some Animal)

feed(animal: Horse())
feed(animal: Chicken())

For an opaque result type, the underlying type is inferred from the return value in the implementation. A method or computed property with an opaque result type can be called from anywhere in the program, so the scope of this named value is global. This means the underlying return type has to be the same across all return statements; and if it isn’t, the compiler will report an error that the underlying return values have mismatched types.

For an opaque SwiftUI view, the ViewBuilder DSL can transform control-flow statements to have the same underlying return type for each branch.

func makeView(for farm: Farm) -> some View {
    FarmView(farm: farm)
}

Sometimes, a value is small enough to fit inside the box directly. And other values are too large for the box, so the value has to be allocated elsewhere, and the box stores a pointer to that value. The static type any Animal that can dynamically store any concrete animal type is formally called an existential type.

func feedAll(_ animals: [any Animal]) {
    for animal in animals {
        feed(animal)
    }
}
Screenshot 2022-06-09 at 13 13 03

Now, any Animal is a different type from some Animal, but the compiler can convert an instance of any Animal to some Animal by unboxing the underlying value and passing it directly to the some Animal parameter. This capability of unboxing arguments is new in Swift 5.7. You can think of unboxing as the compiler opening the box and taking out the value stored inside.

For the scope of the some Animal parameter, the value has a fixed underlying type, so we have access to all of the operations on the underlying type, including access to associated types.

Screenshot 2022-06-09 at 13 18 38
feed(animal)

some vs any

In general, write “some” by default, and change “some” to “any” when you know you need to store arbitrary values.

Screenshot 2022-06-09 at 13 21 50

With “some,” the underlying type is fixed. This allows you to rely on type relationships to the underlying type in your generic code, so you’ll have full access to the API and associated types on the protocol you’re working with.

Use “any” when you need to store arbitrary concrete types. “any” provides type erasure, which allows you represent heterogeneous collections, represent the absence of an underlying type, using optionals, and make the abstraction an implementation detail.

What’s new in Swift

Screenshot 2022-06-09 at 13 38 33

If a generic parameter is only used in one place, you can now write it with the some keyword as a shorthand.

func addEntries1<Entries: Collection<MailmapEntry>, Map: Mailmap>(_ entries: Entries, to mailmap: inout Map) {
    for entry in entries {
        mailmap.addEntry(entry)
    }
}

func addEntries1<Entries: Collection<MailmapEntry>>(_ entries: Entries, to mailmap: inout some Mailmap) {
    for entry in entries {
        mailmap.addEntry(entry)
    }
}

Unlock existentials for all protocols

From Swift 5.7 onwards, this code is allowed, and now the restrictions are pushed back to situations where you attempt to use the type in a place where Swift must actually enforce its restrictions

let tvShow: [any Equatable] = ["Brooklyn", 99]

However, what we have gained is the ability to do runtime checks on our data to identify specifically what we’re working with. In the case of our mixed array, we could write this:

for parts in tvShow {
    if let item = item as? String {
        print("Found string: \(item)")
    } else if let item = item as? Int {
        print("Found integer: \(item)")
    }
}

Read more