WWDC Quick Look 💓 By SwiftGGTeam
Design protocol interfaces in Swift

Design protocol interfaces in Swift

Watch original video

Highlight

Swift 5.7 allows protocols with associated types to be used with existentialanyTypes interact directly, and opaque result type supports primary association type constraints.whereThe same-type requirement in the clause can accurately model the equivalence relationship between multiple concrete types.

Core Content

Let’s start with type erasure

You have a farm with various animals: chickens and cows. Chickens lay eggs and cows produce milk. Do you want to use a unifiedAnimalprotocol to abstract them, but each animal produces a different type of food.

The previous approach was to use associated types to model this difference.ChickenofCommodityTypeyesEggCowofCommodityTypeyesMilk

But when you put these animals into a heterogeneous arrayany AnimalHere, the problem arises. callproduce()How should the compiler handle the associated type returned by the method?

(01:14) Swift 5.7 introduces associated-type erasure. When an associated type appears in the producing position of a method, the compiler will type erase it to its upper bound.

protocol Food { }
protocol Animal {
    associatedtype CommodityType: Food
    func produce() -> CommodityType
}

struct Farm {
    var animals: [any Animal]

    func produceCommodities() -> [any Food] {
        animals.map { animal in
            animal.produce()  // The return type is erased to any Food
        }
    }
}

Key points:

  • animal.produce()The return type of is an associated typeCommodityType- existany AnimalWhen called, the compiler willCommodityTypeerase asany Food- This is a new capability in Swift 5.7, as associated types appear in producing position

Conversely, type erasure is unsafe if the associated type appears in parameter consuming position.

protocol Animal {
    associatedtype FeedType: AnimalFeed
    func eat(_ food: FeedType)  // consuming position
}

let animal: any Animal = Cow()
// animal.eat(someHay)  // Compiler error!

05:34any AnimalFeedNot safe to convert to concreteHaytype, because the concrete type is not known at compile time. At this time you need to unpack existential into opaquesometype.

Opaque Result Type and main associated type

(07:54) YourshungryAnimalsFor attributeslazy.filterImplemented to avoid temporary array allocation. But the return type exposes implementation details:LazyFilterSequence<Array<any Animal>>

Swift 5.7 introduces constrained opaque result type, which allows you to hide specific types while exposing sufficient type information:

struct Farm {
    var animals: [any Animal]

    var hungryAnimals: some Collection<any Animal> {
        animals.lazy.filter { $0.isHungry }
    }

    func feedAnimals() {
        for animal in hungryAnimals {
            // animal has type any Animal, so Animal protocol methods can be called
            animal.eat(...)
        }
    }
}

Key points:

  • some Collection<any Animal>hiddenLazyFilterSequencespecific type of
  • but the client still knows that the element type isany Animal- This depends onCollectionThe protocol declares a primary associated typeElement

You can also declare the primary association type on your own protocol:

protocol Container<Element> {
    associatedtype Element
    var count: Int { get }
}

Same-Type Requirement modeling type relationship

(14:54) Now the scene is more complicated: before feeding animals, crops must be planted, harvested, and processed into feed. Cow eats Hay, which is processed from Alfalfa.

you definedAnimalFeedandCropTwo protocols, each has the other’s associated type. But there is a problem with writing this way:

protocol AnimalFeed {
    associatedtype CropType: Crop
    static func grow() -> CropType
}

protocol Crop {
    associatedtype FeedType: AnimalFeed
    func harvest() -> FeedType
}

(19:40) The type deduced by the compiler is(some Animal).FeedType.CropType.FeedType,andeat()The method expects(some Animal).FeedType. Type mismatch.

The fundamental reason is that the definition of the agreement is too broad, and there is no guarantee that the original feed type can be obtained after processing of the grown crops.

Swift 5.7’s same-type requirement solves this problem:

protocol AnimalFeed {
    associatedtype CropType: Crop
    static func grow() -> CropType
}

protocol Crop {
    associatedtype FeedType: AnimalFeed
    func harvest() -> FeedType
}

extension AnimalFeed where Self.CropType.FeedType == Self { }
extension Crop where Self.FeedType.CropType == Self { }

Key points:

  • Self.CropType.FeedType == SelfGuarantee: Crops grown from a certain kind of feed will produce the same kind of feed after processing.
  • This limits which specific types can conform to the protocol -feedAnimal()Methods can now be safely chainedgrow() -> harvest() -> eat()

Detailed Content

Directionality of type erasure

Swift 5.7’s associated-type erasure has clear direction rules:

// Producing position: safe
func produce() -> CommodityType  // Returns any Food

// Consuming position: unsafe
func eat(_ food: FeedType)       // Cannot directly pass any AnimalFeed

(04:41) The type conversion direction of producing position is “subtype to parent type”, which is always safe. consuming position requires a “parent type rotor type”, which the compiler cannot guarantee.

usesomeUnpack existential

When the associated type is in consuming position, you need to unwrap it using a generic function or an opaque type:

// Use a generic function
func feedAnimal<AnimalType: Animal>(_ animal: AnimalType, with feed: AnimalType.FeedType) {
    animal.eat(feed)
}

// Or use some
func feedAnimal(_ animal: some Animal, with feed: (some Animal).FeedType) {
    // More precise type matching is needed here
}

Constrained Existential Types

Prior to Swift 5.7, to express “a Collection with elements of type X”, you needed to write a type erasure wrapper yourself. Now you can directly write:

// Before
struct AnyCollection<Element>: Collection { /* Dozens of lines of boilerplate */ }

// Swift 5.7
func process(_ items: any Collection<MailmapEntry>) {
    for item in items {
        // item is MailmapEntry
    }
}

25:00any Collection<MailmapEntry>is a built-in language feature and does not require a handwritten type erasure wrapper.

Complete example of Same-Type Requirement

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

protocol AnimalFeed {
    associatedtype CropType: Crop
    static func grow() -> CropType
}

protocol Crop {
    associatedtype FeedType: AnimalFeed
    func harvest() -> FeedType
}

// Key constraints
extension AnimalFeed where Self.CropType.FeedType == Self { }
extension Crop where Self.FeedType.CropType == Self { }

// Now feedAnimal compiles safely
func feedAnimal(_ animal: some Animal) {
    let crop = type(of: animal).FeedType.grow()
    let feed = crop.harvest()
    animal.eat(feed)  // Types match!
}

Key points:

  • type(of: animal).FeedTypeGet the feed type corresponding to the animal -grow()returnCropTypeharvest()returnFeedType- same-type requirement guaranteeCropType.FeedType == Self, so the final type isanimal.FeedType

Core Takeaways

1. UseanyAlternative handwriting type erasure wrapper

Previously written for the agreementAnyXxxWrappers are a common pain point in Swift development. Swift 5.7anyGrammar makes this a thing of the past. Check the type erasure classes in your project to see which ones can be directly replacedany SomeProtocol<SomeType>

Entrance:any Collection<Element>any Publisher<Output, Failure>

2. Declare the main association type for the custom collection protocol

If you define something likeContainerDataSourceFor such protocols, add a primary associated type declaration to them:

protocol DataSource<Item> {
    associatedtype Item
    func item(at index: Int) -> Item
}

This way the caller can writesome DataSource<User>orany DataSource<User>, both hiding the implementation and preserving type information.

3. Use same-type requirement to model two-way relationships

When there is a cyclic relationship of “A is associated with B, and B is associated back to A” in your data model, use the same-type requirement to ensure consistency. Typical scenario:

  • Database relational model (Table -> Column -> Table)
  • Edges and nodes in the graph structure
  • AST node relationships in the compiler

Entrance:where Self.Other.AssociatedType == Self

  • Embrace Swift generics — The preparatory content of this session, explaining the basics of Swift generics
  • What’s new in Swift — Overview of new features in Swift 5.7, includingany/someSyntax and primary association types
  • Meet Swift Regex — New tools for string processing in Swift 5.7, also benefiting from language improvements

Comments

GitHub Issues · utterances