WWDC Quick Look 💓 By SwiftGGTeam
Embrace Swift generics

Embrace Swift generics

Watch original video

Highlight

Swift 5.7 makes generic code easier to write and read. Holly used an example of a farm simulator to demonstrate the complete abstraction process from concrete types to function overloading, class inheritance, to protocols and generics, and introducedsomeandanyTwo new syntaxes for different uses with parameters and collections.

Core Content

Generics are an essential tool for managing code complexity. Holly used a farm simulator throughout to demonstrate the complete evolution of code from concrete to abstract. (00:17)

The starting point is simple: aCowstruct, aHaystruct, aAlfalfastruct, aFarm struct。Cow.eatacceptHayHay.growreturnAlfalfaAlfalfa.harvestreturnHayFarm.feedFirst plant alfalfa, then harvest hay, and finally feed the cattle. (01:48)

When you want to add more animals, the most direct way is to overload the function: giveFarmaddfeed(horse:)feed(chicken:). But the implementation of each overload is almost the same, just the type is different. Too many overloads and it becomes boilerplate code. (02:46)

Next try class inheritance: introductionAnimalbase class,CowHorseChickeninherit it. But problems soon arise: each animal eats a different food,Animal.eatIt’s awkward to write anything about the parameter type. WriteAnyType safety will be lost, and the wrong food may be passed at runtime; writing generic class parameters will cause all references toAnimalFood type parameters must be provided everywhere, which is very cumbersome. (05:04)

Protocols are better abstraction tools.AnimalThe protocol has an associated typeFeedand a methodeat(_ food: Feed). Each specific animal type matchesAnimalProtocol, automatically inferred by the compilerFeedAssociation type.CowofeatacceptHay, the compiler will knowCow.Feed == Hay。(08:44

Swift 5.7 introducedsomeKeywords make generic parameter declarations more concise.func feed(_ animal: some Animal)Equivalent to explicit generic parameters with where clause, but with cleaner syntax.someRepresents “a specific type that conforms to the protocol”. The underlying type is determined at the time of call and remains unchanged thereafter. (12:52)

If you need to store different types of animals (for example, there are both cows and horses in an array), usesomeNo, becausesomeRequires the underlying type to be fixed. Use this timeany[any Animal]Represents “any type that conforms to the Animal protocol”.anyIs an existential type, and the underlying type can change at runtime. (21:04)

any Animalandsome Animalcan be automatically converted between. existfeedAllIn the method, traverse[any Animal], each element will be automatically unboxed intosome Animalpass tofeedmethod, like thisfeedAssociated types can still be used internally. (23:00)

Holly’s suggestion is: write by defaultsome, only use it instead if you need to store any typeany. This is the same idea as “let is used by default, and var is used when modification is needed”. (26:12)

Detailed Content

From Concrete to Abstract: Identifying Repeating Patterns

(01:48) The initial code only has specific types:

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

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

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

struct Farm {
    func feed(_ cow: Cow) {
        let crop = Hay.grow()
        let produce = crop.harvest()
        cow.eat(produce)
    }
}

Key points:

  • Each type has clear responsibilities: Cow eats, Hay grows, Alfalfa harvests, Farm feeds
  • ButfeedOnly Cow can be fed, adding Horse and Chicken needs to be rewritten

An attempt at function overloading:

struct Farm {
    func feed(_ cow: Cow) { ... }
    func feed(_ horse: Horse) { ... }
    func feed(_ chicken: Chicken) { ... }
}

Key points:

  • The implementation of each overload is almost the same
  • Adding new animal types requires adding new overloads
  • This is “ad-hoc polymorphism”, not a general solution

Why class inheritance is not suitable for this scenario

(05:04) Try using class inheritance:

class Animal {
    func eat(_ food: Any) {} // Problem: what should the parameter type be?
}

class Cow: Animal {
    override func eat(_ food: Any) {
        guard let hay = food as? Hay else { ... }
        // Feed hay
    }
}

Key points:

  • Enforce reference semantics, but animal instances do not need to share state
  • Subclasses must override. If you forget, you will only find it at runtime. -AnyParameters lose type safety and are checked at runtime
  • If using generic class parametersAnimal<Food>, all references must have type parameters

Use protocols to define capability interfaces

(08:44) The protocol abstracts “animals eat food” into an interface:

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

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
}

Concrete types conform to the protocol:

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

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

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

Key points:

  • Association typeFeedDepends on the specific conforming type -Cow.eatacceptHay, the compiler infersCow.Feed == Hay- The protocol is not limited to class, struct, enum, and actor can all comply
  • The compiler checks that each conforming type implements all requirements

some keyword: simplify generic parameters

(12:52) Traditional generic writing:

func feed<A: Animal>(_ animal: A) {
    let crop = type(of: animal).Feed.grow()
    let produce = crop.harvest()
    animal.eat(produce)
}

Simplified writing for Swift 5.7:

func feed(_ animal: some Animal) {
    let crop = type(of: animal).Feed.grow()
    let produce = crop.harvest()
    animal.eat(produce)
}

Key points:

  • some AnimalEquivalent to explicit generic parametersA: Animal
  • someRepresents “a concrete, protocol-conforming type”
  • The underlying type is fixed, and the compiler can use associated types in the function body
  • Applies to parameter position and return position (such as SwiftUI’ssome View

When are explicit type parameters required? When the same type needs to be referenced multiple times in a signature:

func buildHabitat<A: Animal>(for animal: A) -> A.Habitat {
    // Both the parameter type and return type depend on A
}

any keyword: store any type

21:04someThe underlying type is required to be fixed, so[some Animal]The array can only store the same animal. To save mixed types, useany

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) // Automatically opens the existential
        }
    }
}

Key points:

  • any AnimalIt is an existing type and can store any specific type that conforms to Animal
  • Type erasure eliminates static type distinctions between concrete types
  • Butany AnimalThe associated type cannot be accessed directly because the underlying type is not fixed
  • Swift 5.7 supports automatic unpacking:any AnimalParameters can be passed tosome Animalparameters
  • After unpacking, infeedWithin the function body, the underlying type is fixed and associated types can be used normally.

someandanyComparison of:

Featuressomeany
underlying typefixedvariable
association typeavailablenot available (unless unpacked)
Heterogeneous collectionsNot supportedSupported
Optional valueThe underlying type must be specifiedCan be used directlyany Animal?
PerformanceUntyped erasure overheadTyped erasure overhead

Complete example

(27:10) The complete code that combines all the concepts:

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()
    }
}

Key points:

  • AnimalFeedandCropThe associated types constrain each other to ensure that feed and crops match -Farm.feedusesome AnimalFeeding individual animals -Farm.feedAlluse[any Animal]Feeding mixed animal groups
  • The compiler guarantees against feeding the wrong food: you can’t putHaypass to eatCornofChicken

Core Takeaways

  • What to do: Review function overloading in the project and change duplicate implementations to generic functions.
    Why It’s Worth Doing: Holly’s Farm Example, 3 AnimalsfeedThe overloaded implementation is almost the same. Change tofunc feed(_ animal: some Animal)After that, add aSheepThe type does not need to be changedFarmany code.
    How ​​to start: Search for similar overloaded functions in the project and find out the common capabilities of their operations (such as callingeatmethod), extract it into a protocol, usesomeWrite a generic version of the parameter.

  • What to do: UsesomeReplace explicit generic parameters to make code more readable.
    Why it’s worth doing:func feed(_ animal: some Animal)Comparefunc feed<A: Animal>(_ animal: A)Half the time, and the semantic information appears directly in the parameter declaration. Holly emphasized that this is a common pattern in Swift 5.7.
    How ​​to start: Traverse the generic functions in the project. If the type parameter only appears once in the parameter list, change it tosomegrammar. If type parameters appear in both parameter and return types, explicit parameters are retained.

  • What to do: UseanyBuild heterogeneous collections but unpack back as quickly as possiblesome.
    Why it’s worth doing:[any Animal]It is possible to store mixed types, but when internal operations require related types, type erasure will get in your way. Holly’sfeedAllDemonstrates best practices: collections withany, passed to the receiver when traversing elementssomefunction that restores full type capabilities within the function body.
    How ​​to start: Used in checking projectsany(or where the type exists implicitly), find the code that needs to access the associated type, and extract the operation into acceptsomefunction to let the compiler unpack it automatically.

Comments

GitHub Issues · utterances