The power of generics in Swift. Part 2

Good afternoon friends. Especially for students of the course "iOS Developer. Advanced Course ”, we prepared a translation of the second part of the article“ The Power of Generics in Swift ”.

Related types, where clauses, subscripts, and more ...

In the article “The Power of Generics in Swift. Part 1 ” described generic functions, generic types and type restrictions. If you are a beginner, I would recommend that you first read the first part for a better understanding.

When defining a protocol, it is sometimes useful to declare one or more related types as part of the definition. The associated type specifies the stub name for the type that is used as part of the protocol. The actual type used for this related type will not be specified until the protocol is used. Associated types are declared using the associatedtype keyword.

We can define the protocol for the stack that we created in the first part .

 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 the necessary functionality that any stack should provide.

Any type that conforms to the Stackable protocol must be able to specify the type of value it stores. It must ensure that only elements of the correct type are added to the stack, and it should be clear what type of elements are returned by its subscript.

Let's change our stack according to the protocol:

 enum StackError: Error { case Empty(message: String) } 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 } } public struct Stack<T>: Stackable { public typealias Element = T var array: [T] = [] init(capacity: Int) { array.reserveCapacity(capacity) } public mutating func push(element: T) { array.append(element) } public mutating func pop() -> T? { return array.popLast() } public func peek() throws -> T { guard !isEmpty(), let lastElement = array.last else { throw StackError.Empty(message: "Array is empty") } return lastElement } func isEmpty() -> Bool { return array.isEmpty } func count() -> Int { return array.count } } extension Stack: Collection { public func makeIterator() -> AnyIterator<T> { var curr = self return AnyIterator { curr.pop() } } public subscript(i: Int) -> T { return array[i] } public var startIndex: Int { return array.startIndex } public var endIndex: Int { return array.endIndex } public func index(after i: Int) -> Int { return array.index(after: i) } } extension Stack: CustomStringConvertible { public var description: String { let header = "***Stack Operations start*** " let footer = " ***Stack Operation end***" let elements ={ "\($0)" }.joined(separator: "\n") return header + elements + footer } } var stack = Stack<Int>(capacity: 10) stack.push(element: 1) stack.pop() stack.push(element: 3) stack.push(element: 4) print(stack) 

Extending an existing type to indicate a related type

You can extend an existing type to comply with the 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 the associated type:

You can add restrictions to the associated type in the protocol to ensure that related types comply with these restrictions.
Let's change the 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 the type of the stack element should match Equatable , otherwise a compile-time error will occur.

Recursive protocol restrictions:

The protocol may be part of its own requirements.

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

Suffix has two limitations: it must comply with the SuffixableContainer protocol (the protocol is defined here), and its Item type must match the Item type of the container.

There is a good example in the Swift standard library in Protocol Sequence illustrate this topic.

Proposal for the limitations of the recursive protocol:

Generic type extensions:

When you extend a generic type, you are not describing a list of type parameters when defining an extension. Instead, a list of type parameters from the source definition is available in the extension body, and parameter name of the source type is used to refer to type parameters from the source definition.

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

Generic where clause

For related types, it is useful to define requirements. The requirement is described by the generic where clause . The Generic where clause allows you to require that the associated type conform to a specific protocol or that certain type parameters and related types are the same. The Generic where clause begins with the where keyword, followed by constraints for related types or the equality relationship between types and related types. The Generic where clause is written just before the opening brace of the type or function body.

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

Extensions with Generic conditions where

You can use the generic where clause as part of the extension. The following example extends the overall Stack structure of the previous examples by adding the 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 items on the stack support Equatable. You can also use the generic where clause with protocol extensions. You can add several requirements to the where separating them with a comma.

Associated types with the Generic clause where:

You can include the generic where clause in the 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 the inherited bound type, including the generic where clause in the protocol declaration. For example, the following code declares a ComparableContainer protocol that requires Item support Comparable :

 protocol ComparableContainer: Container where Item: Comparable { } 

Generic type aliases:

Type alias can have common parameters. It will still remain an alias (that is, it will not introduce a new type).

 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) 

In this mechanism, additional restrictions to the type parameters cannot be used.
Such a code will not work:

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

Generic superscripts

Subscripts can use the generic mechanism and include the generic where clause. You write the type name in angle brackets after the subscript , and write the generic where clause immediately before the opening brace of the subscript 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 particular type of parameter, such as Int. This specialized function can then be optimized specifically for Int, while all that is superfluous will be removed. The process of replacing type parameters with type arguments at compile time is called specialization .

By specializing the generic function for these types, we can eliminate the costs of virtual dispatching, inline calls, when necessary, and eliminate the overhead of the generic system.

Operator Overload

Generic types do not work with operators by default, for this you need a protocol.

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

An interesting thing about generics

Why can't you define a static stored property for a generic type?

This will require a separate storage of properties for each individual generic (T) specialization.

Resources for in-depth study:

That's all. See you on the course .


All Articles