Protocols and generics
Interfaces in Swift: protocols that name what a type can do, default implementations in extensions, the standard protocols behind printing and comparing, and generic functions and types.
- CS61A
- Object Examples (interfaces) · Composing Programs 2.7
- Swift book
- Protocols · Generics · Extensions · Opaque and Boxed Protocol Types
CS61A describes an interface as a set of shared messages: if several
kinds of object all respond to area, code can ask any of them for its area
without knowing which kind it has. Python relies on convention for this.
Swift writes the interface down as a protocol, and the compiler checks
that every type claiming to follow it really does.
Protocols
A protocol lists requirements: properties and methods a conforming type must provide.
protocol Shape {
var name: String { get }
var area: Double { get }
}
struct Square: Shape {
var side: Double
var name: String { "square" }
var area: Double { side * side }
}
struct Circle: Shape {
var radius: Double
var name: String { "circle" }
var area: Double { Double.pi * radius * radius }
}
let shapes: [any Shape] = [Square(side: 2), Circle(radius: 1)]
for shape in shapes {
print(shape.name, shape.area)
}
{ get } means the property must be readable; a stored or computed
property both satisfy it. [any Shape] is an array whose elements can be
any type that conforms to Shape. If Circle forgot area, the program
would not compile.
Default implementations
An extension adds methods to an existing type, including a protocol. Methods added to a protocol extension are available to every conforming type:
protocol Animal {
var name: String { get }
func sound() -> String
}
extension Animal {
func speak() -> String {
"\(name) says \(sound())"
}
}
struct Dog: Animal {
let name: String
func sound() -> String { "woof" }
}
print(Dog(name: "Rex").speak()) // Rex says woof
Dog only had to supply name and sound; speak came for free. This is
how Swift shares behavior without inheritance, and it works for structures
and enumerations as well as classes.
What would Swift print?
protocol Animal {
var name: String { get }
func sound() -> String
}
extension Animal {
func speak() -> String { "\(name) says \(sound())" }
}
struct Cat: Animal {
let name: String
func sound() -> String { "meow" }
}
struct Cow: Animal {
let name: String
func sound() -> String { "moo" }
}
let animals: [any Animal] = [Cat(name: "Tom"), Cow(name: "Bess")]
print(animals.map { $0.speak() })Show answer and explanation
Answer["Tom says meow", "Bess says moo"]
Each element uses its own name and sound, and the shared speak from the
extension puts them together. map collects the two strings into an array.
Extensions work on types you did not write, too:
extension Int {
var isEven: Bool { self % 2 == 0 }
}
print(4.isEven, 7.isEven) // true false
The standard protocols
Much of what you have used so far is protocols from the standard library.
CustomStringConvertible: adescriptionproperty thatprintuses.Equatable:==. For a structure whose properties are all equatable, Swift writes==for you.Comparable:<, which also givessorted(),max(), and friends.Hashable: needed to be a dictionary key or set element; also synthesized for simple structures.
struct Money: CustomStringConvertible, Comparable, Hashable {
let cents: Int
var description: String {
let remainder = cents % 100
return "$\(cents / 100)." + (remainder < 10 ? "0" : "") + String(remainder)
}
static func < (a: Money, b: Money) -> Bool {
a.cents < b.cents
}
}
print(Money(cents: 1234)) // $12.34
print([Money(cents: 300), Money(cents: 100)].sorted()) // [$1.00, $3.00]
print(Money(cents: 5) == Money(cents: 5)) // true
static func < defines the operator for this type; == and hashing were
generated because cents is an Int.
What would Swift print?
struct Point: Equatable {
var x: Int
var y: Int
}
let a = Point(x: 1, y: 2)
print(a == Point(x: 1, y: 2), a == Point(x: 2, y: 1))Show answer and explanation
Answertrue false
Declaring Equatable is enough: Swift compares the properties one by one.
The second point has the same numbers in a different order, so it is not
equal.
Generics
A generic function works for any type that meets some requirement, written as a type parameter in angle brackets:
func largest<T: Comparable>(_ values: [T]) -> T? {
guard var best = values.first else {
return nil
}
for value in values where value > best {
best = value
}
return best
}
print(largest([3, 9, 2]) ?? 0) // 9
print(largest(["pear", "apple"]) ?? "") // pear
T: Comparable says: T can be any type, as long as it conforms to
Comparable, which is exactly what > needs. One definition serves
integers, strings, and Money, and Swift still checks types: a call on an
array of shapes would not compile, because shapes cannot be compared.
Types can be generic too. Array<Int> is the full name of [Int]. Here is
a stack that holds any element type:
struct Stack<Element> {
private var items: [Element] = []
var isEmpty: Bool { items.isEmpty }
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element? {
items.popLast()
}
}
var stack = Stack<String>()
stack.push("a")
stack.push("b")
print(stack.pop()!, stack.pop()!, stack.isEmpty) // b a true
Put the lines in order
Put the lines in order so the program prints 2 3.
print(count([1, 2, 3, 4], where: { $0 > 2 }), count("banana", where: { $0 == "a" }))func count<S: Sequence>(_ items: S, where test: (S.Element) -> Bool) -> Int {total += 1var total = 0return total}for item in items where test(item) {}
Show the correct program
func count<S: Sequence>(_ items: S, where test: (S.Element) -> Bool) -> Int {
var total = 0
for item in items where test(item) {
total += 1
}
return total
}
print(count([1, 2, 3, 4], where: { $0 > 2 }), count("banana", where: { $0 == "a" }))Sequence is the protocol behind every for-in loop, and S.Element is
whatever type it yields: Int for the array, Character for the string.
Lesson 9’s “sequence abstraction” is this protocol.
Lab 15: Interfaces
Q1: A generic queue
Write Queue<Element>, a structure with enqueue(_:), dequeue() (returning
an optional, first in first out), and a count property.
Show solution
struct Queue<Element> {
private var items: [Element] = []
var count: Int { items.count }
mutating func enqueue(_ item: Element) {
items.append(item)
}
mutating func dequeue() -> Element? {
items.isEmpty ? nil : items.removeFirst()
}
}
var queue = Queue<Int>()
queue.enqueue(1)
queue.enqueue(2)
assert(queue.count == 2)
assert(queue.dequeue() == 1)
assert(queue.dequeue() == 2)
assert(queue.dequeue() == nil)removeFirst() shifts every remaining element, so this queue gets slow when
large. Lesson 18 discusses measuring that; a faster queue keeps two stacks.
Q2: Describable shapes
Make Square and Circle from this lesson conform to
CustomStringConvertible with a single extension on the Shape protocol,
so that print(Square(side: 2)) prints square (area 4.0).
Show solution
protocol Shape: CustomStringConvertible {
var name: String { get }
var area: Double { get }
}
extension Shape {
var description: String { "\(name) (area \(area))" }
}
struct Square: Shape {
var side: Double
var name: String { "square" }
var area: Double { side * side }
}
print(Square(side: 2))
assert(Square(side: 2).description == "square (area 4.0)")protocol Shape: CustomStringConvertible makes every shape describable, and
the extension supplies description for all of them at once.
Q3: Smallest by a key
Write a generic function smallest(_:by:) that returns the element for which
a key function gives the smallest value, or nil for an empty array. For
example, the shortest word in an array of strings.
func smallest<T, Key: Comparable>(_ items: [T], by key: (T) -> Key) -> T? {
// your code here
}
Show solution
func smallest<T, Key: Comparable>(_ items: [T], by key: (T) -> Key) -> T? {
guard var best = items.first else {
return nil
}
for item in items where key(item) < key(best) {
best = item
}
return best
}
assert(smallest(["pear", "fig", "banana"], by: { $0.count }) == "fig")
assert(smallest([3, -7, 5], by: { abs($0) }) == 3)
assert(smallest([String](), by: { $0.count }) == nil)Two type parameters: T for the elements, which need nothing, and Key for
the key’s type, which must be comparable. The standard library’s version is
items.min(by:).
What’s next
Next lesson: iterators and lazy sequences, and sequences that never end.