Highlight
Swift 5.7 allows protocols with associated types to be used with existential
anyTypes 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.ChickenofCommodityTypeyesEgg,CowofCommodityTypeyesMilk。
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:34)any 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 is
any 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:00)any 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()returnCropType,harvest()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 likeContainer、DataSourceFor 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
Related Sessions
- 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, including
any/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