Power of Swift Generics — Part 2

Associated types, where clauses, subscripts and more…

Aaina jain
Swift India
5 min readNov 28, 2018

--

Power of Generics — Part1 explains about generic function, generic type and type constraints. If you are new to this series, I would recommend you to read Part 1 first for better understanding.

While defining a protocol, it’s sometimes useful to declare one or more associated types as part of the protocol’s definition. An associated type gives a placeholder name to a type that is used as part of the protocol. The actual type to use for that associated type isn’t specified until the protocol is adopted. Associated types are actually specified with the associatedtype keyword.

We can define a protocol for Stack we created in Part 1 article.

protocol Stackable {
associatedtype Element
mutating func push(element: Element)
mutating func pop() -> Element?
func peek() throws -> Element
func isEmpty() -> Bool
func count() -> Int
subscript(i: Int) -> Element { get }
}

The Stackable protocol defines required capabilities that any stack must provide.

Any type that conforms to the Stackable protocol must be able to specify the type of values it stores. It must ensure that only items of the right type are added to the stack, and it must be clear about the type of the items returned by its subscript.

Let’s modify our generic stack to conform to protocol:

Extending an Existing Type to Specify an Associated Type

You can extend an existing type to add conformance to a protocol.

protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}
extension Array: Container {}

Adding Constraints to an Associated Type:

You can add type constraints to an associated type in a protocol to require that conforming types satisfy those constraints.

Let’s modify Stackable protocol.

protocol Stackable {
associatedtype Element: Equatable
mutating func push(element: Element)
mutating func pop() -> Element?
func peek() throws -> Element
func isEmpty() -> Bool
func count() -> Int
subscript(i: Int) -> Element { get }
}

Now, Stack’s Element type needs to conform to Equatable else it will give compile time error.

Recursive Protocol Constraints:

A protocol can appear as part of its own requirements.

protocol SuffixableContainer: Container {
associatedtype Suffix: SuffixableContainer where Suffix.Item == Item
func suffix(_ size: Int) -> Suffix
}

Suffix has two constraints: It must conform to the SuffixableContainer protocol (the protocol currently being defined), and its Item type must be the same as the container’s Item type.

Swift standard library shows best example of this in Protocol Sequence .

Recursive protocol constraints proposal: https://github.com/apple/swift-evolution/blob/master/proposals/0157-recursive-protocol-constraints.md

Extending Generic Type:

When you extend a generic type, you don’t provide a type parameter list as part of the extension’s definition. Instead, the type parameter list from the original type definition is available within the body of the extension, and the original type parameter names are used to refer to the type parameters from the original definition.

extension Stack {
var topItem: Element? {
return items.isEmpty ? nil : items[items.count - 1]
}
}

Generic Where Clauses:

It can also be useful to define requirements for associated types. You do this by defining a generic where clause. A generic where clause enables you to require that an associated type must conform to a certain protocol, or that certain type parameters and associated types must be the same. A generic where clause starts with the wherekeyword, followed by constraints for associated types or equality relationships between types and associated types. You write a generic where clause right before the opening curly brace of a type or function’s body.

func allItemsMatch<C1: Container, C2: Container>
(_ someContainer: C1, _ anotherContainer: C2) -> Bool
where C1.Item == C2.Item, C1.Item: Equatable {
}

Extensions with a Generic Where Clause

You can also use a generic where clause as part of an extension. The example below extends the generic Stack structure from the previous examples to add an isTop(_:)method.

extension Stack where Element: Equatable {
func isTop(_ item: Element) -> Bool {
guard let topItem = items.last else {
return false
}
return topItem == item
}
}

The extension adds the isTop(_:)method only when the items in the stack are equatable. You can use a generic where clause with extensions to a protocol too. Multiple requirements can be added in where clause by separating with a comma.

Associated Types with a Generic Where Clause:

You can include a generic where clause on an associated type.

protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
associatedtype Iterator: IteratorProtocol where Iterator.Element == Item
func makeIterator() -> Iterator
}

For a protocol that inherits from another protocol, you add a constraint to an inherited associated type by including the generic where clause in the protocol declaration. For example, the following code declares a ComparableContainer protocol that requires Item to conform to Comparable:

protocol ComparableContainer: Container where Item: Comparable { }

Generic typealiases:

Typealiases could be allowed to carry generic parameters. They would still be aliases (i.e., they would not introduce new types).

typealias StringDictionary<Value> = Dictionary<String, Value>var d1 = StringDictionary<Int>()
var d2: Dictionary<String, Int> = d1 // okay: d1 and d2 have the same type, Dictionary<String, Int>
typealias DictionaryOfStrings<T : Hashable> = Dictionary<T, String>
typealias IntFunction<T> = (T) -> Int
typealias Vec3<T> = (T, T, T)
typealias BackwardTriple<T1,T2,T3> = (T3, T2, T1)

It does not allow additional constraints to be added to type parameters.

It won’t work:

typealias ComparableArray<T where T : Comparable> = Array<T>

Generic Subscripts:

Subscripts can be generic, and they can include generic where clauses. You write the placeholder type name inside angle brackets after subscript, and you write a generic where clause right before the opening curly brace of the subscript’s body.

extension Container {
subscript<Indices: Sequence>(indices: Indices) -> [Item]
where Indices.Iterator.Element == Int {
var result = [Item]()
for index in indices {
result.append(self[index])
}
return result
}
}

Generic Specialization:

Generic specialization means that the compiler clones a generic type or function, such as Stack<T>, for a concrete parameter type, such as Int. This specialized function can then be optimized specifically for Int, removing all indirection. The process of replacing type parameters with type arguments at compile time is known as specialization.

By specializing the generic function for these types, we can eliminate the cost of the virtual dispatch, inline calls when appropriate, and eliminate the overhead of the generic system.

Operator Overloading:

Generic types doesn’t work with operators by default, so you need a protocol for that.

func ==<T: Equatable>(lhs: Matrix<T>, rhs: Matrix<T>) -> Bool {
return lhs.array == rhs.array
}

Interesting thing about Generics:

Why you cannot define a static stored property on a generic type?

This would require separate property storage for each individual specialization of the generic placeholder(T).

--

--