Skip to content

Instantly share code, notes, and snippets.

@pitt500
Last active June 15, 2022 02:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pitt500/9116ebebcc2272ee3267d14660f14d7c to your computer and use it in GitHub Desktop.
Save pitt500/9116ebebcc2272ee3267d14660f14d7c to your computer and use it in GitHub Desktop.
This is a demo code extracted from https://developer.apple.com/wwdc22/110353
import Foundation
protocol Animal {
associatedtype CommodityType: Food
associatedtype FeedType: AnimalFeed
var isHungry: Bool { get }
func produce() -> CommodityType
func eat(_: FeedType)
}
struct Chicken: Animal {
var isHungry: Bool = Bool.random()
func produce() -> Egg {
Egg()
}
func eat(_ animalFeed: Scratch) {
print(animalFeed)
}
}
struct Cow: Animal {
var isHungry: Bool = Bool.random()
func produce() -> Milk {
Milk()
}
func eat(_ animalFeed: Hay) {
print(animalFeed)
}
}
protocol Food {}
struct Egg: Food {}
struct Milk: Food {}
protocol AnimalFeed {
// Without where clause, this protocol is too general to accurately model
// the desired relationship between our concrete types.
// Now Swift will break the loop type and understand the relationship between
// AnimalFeed and Crop.
associatedtype CropType: Crop where CropType.FeedType == Self
static func grow() -> CropType
}
struct Hay: AnimalFeed {
static func grow() -> Alfalfa { Alfalfa() }
}
struct Scratch: AnimalFeed {
static func grow() -> Millet { Millet() }
}
protocol Crop {
// Without where clause, this protocol is too general to accurately model
// the desired relationship between our concrete types.
// Now Swift will break the loop type and understand the relationship between
// AnimalFeed and Crop.
associatedtype FeedType: AnimalFeed where FeedType.CropType == Self
func harvest() -> FeedType
}
struct Alfalfa: Crop {
func harvest() -> Hay { Hay() }
}
struct Millet: Crop {
func harvest() -> Scratch { Scratch() }
}
// MARK: - Type erasure - 03:16
// We have erased the relationship between the concrete animal type
// and associated commodity type by replacing them with any Animal
// and any Food
// Type erasure happens when an associated type appears in the result of a function
// Declaration.
struct Farm {
var animals: [any Animal]
func produceCommodities() -> [any Food] {
let foodList = animals.map { animal in
animal.produce()
}
return foodList
}
}
let animals: [any Animal] = [Cow(), Chicken(), Chicken()]
let farm = Farm(animals: animals)
print(farm.produceCommodities())
// But it doesn't work when associated type appears in the parameter list. The concrete
// type for the parameter is unknown. There's no guarantee that right concrete type is provided.
let animalFeed: [any AnimalFeed] = [Hay(), Scratch()]
animals.map { animal in
/*
animal.eat(???)
*/
// error: Associated type 'FeedType' can only be used with a concrete type or generic parameter base
// error: Member 'eat' cannot be used on value of type 'any Animal'; consider using a generic constraint (or opaque type) instead
}
// MARK: - Hiding implementation details - 8:35
extension Farm {
// Too much details exposed!
// var hungryAnimals: LazyFilterSequence<[any Animal]> {
// animals.lazy.filter(\.isHungry)
// }
// Now it hides too much info about the element types !
// var hungryAnimals: some Collection {
// animals.lazy.filter(\.isHungry)
// }
// Great balance ✅ not exposing too much, but not hiding important info!
var hungryAnimals: some Collection<any Animal> {
animals.lazy.filter(\.isHungry)
}
func feedAnimals() {
// animals is just read once and then discarded. This is inefficient for
// large data. Let's use a lazy collection instead.
for animal in animals {
print("\(animal) is eating")
}
}
}
// In Swift 5.7 you can declare primary associated types between brackets.
// The associated types that work best as primary associated types are those that are
// usually provided by the caller, such as an Element type of a collection, as opposed
// to implementation details, such as collection's iterator type.
protocol SomeProtocol<Element> {
associatedtype Element
}
// You can read this as "SomeProtocol of Element"
// You can also use any in the collection to represent multiple concrete types conforming to a protocol:
extension Farm {
var isLazy: Bool {
Bool.random()
}
var hungryAnimals2: any Collection<any Animal> {
if isLazy {
return animals.lazy.filter(\.isHungry)
} else {
return animals.filter(\.isHungry)
}
}
}
// MARK: - Identify type relashionships
let alfalfa = Hay.grow()
let hay = alfalfa.harvest()
let cow = Cow()
cow.eat(hay)
let millet = Scratch.grow()
let scratch = millet.harvest()
let chicken = Chicken()
chicken.eat(scratch)
extension Farm {
func feedAnimals2() {
for animal in hungryAnimals {
feedAnimal(animal)
}
}
// Since feedAnimal() needs to work with eat() method of the Animal protocol,
// Which as an associated type in consumer position (as parameter), I'm going
// to unbox the existential type by declaring that the feedAnimal() method takes
// 'some Animal' (a concrete type conforming animal) as paramater.
private func feedAnimal(_ animal: some Animal) { // equivalent to feedAnimal<A>(_ animal: A) where A: Animal
// Without where clause, crop will be a value of type:
// (some Animal).FeedType.CropType. See definitions of Crop and AnimalFeed.
let crop = type(of: animal).FeedType.grow()
// and feed a value of type: (some Animal).FeedType.CropType.FeedType
let feed = crop.harvest()
// eat() method expect (some Animal).FeedType only
animal.eat(feed)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment