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 = array.map{ "\($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:
https://github.com/apple/swift-evolution/blob/master/proposals/0157-recursive-protocol-constraints.md
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
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:
https://github.com/apple/swift/blob/master/docs/Generics.rst
https://github.com/apple/swift/blob/master/docs/GenericsManifesto.md
https://developer.apple.com/videos/play/wwdc2018/406/
https://www.youtube.com/watch?v=ctS8FzqcRug
https://medium.com/@vhart/protocols-generics-and-existential-containers-wait-what-e2e698262ab1
That's all. See you on the
course .