Blog

[Swift] Generics in Documents

Published 5 min read

Contents

  1. The Problem That Generics Solve
  2. Generic Functions
  3. Type Parameters
  4. Naming Type Parameters
  5. Generic Types
  6. Extending a Generic Type
  7. Type Constraints
  8. Type Constraint Syntax
  9. Type Constraints in Action
  10. Associated Types
  11. Associated in Action
  12. Extending an Existing Type to Specify an Associated Type
  13. Adding Constraints to an Associated Type
  14. Using a Protocol in Its Associated Type's Constraints
  15. Generic Where Caluses
  16. Extensions with a Generic Where Clause
  17. Contextual Where Clauses
  18. Associated Types with a Generic Where Clause
  19. Generic Subscripts
  • The Problem That Generics Solve

Generics are used to generalize types.

  • Generic Functions

A generic function works regardless of type unless there's a separate constraint.

You can generalize the type using an arbitrary type T.

func swapTwoValues<T>(_ a: inout T, _ b: inout T) { let temporaryA = a a = b b = temporaryA }
  • Type Parameters

In the example above, T is called a type parameter.

  • Naming Type Parameter

T is the name. You can also give meaningful names like Key and Value, as in Dictionary<Key, Value>.

  • Generic Types
struct Stack<Element> { var items: [Element] = [] mutating func push(_ item: Element) { items.append(item) } mutating func pop() -> Element { return items.removeLast() } } var stackOfStrings = Stack<String>() stackOfStrings.push("uno") stackOfStrings.push("dos") stackOfStrings.push("tres") stackOfStrings.push("cuatro") // the stack now contains 4 strings

You can write the generic version of the type as Stack<Element>.

  • Extending a Generic Type
extension Stack { var topItem: Element? { return items.isEmpty ? nil : items[items.count - 1] } }

You can extend a generic type with an extension like this.
But you don't need to define the type parameter.
Without writing any extra code, you can just take the type parameter from the Stack struct and use it as is.

  • Type Constraints

Say you want to use Equaltable, i.e. the == operation, and the type parameter is used without any constraint.

The compiler can't know whether that type supports the == operator function, so it can't do the == operation.

(There are types that don't support the == operation.)

So you have to signal that only types conforming to Equaltable will come in, and you do that with a type constraint.

  • Type Constraint Syntax

  • Type Constraints in Action

func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) { // function body goes here }

You can add a constraint like T: SomeClass.
It tells the compiler that only types that conform to or inherit from SomeClass will come in.

  • Associated Types (in Action)

To use generics in a protocol, you have to use associated types.

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

You can't do it like this.

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

You have to support it by declaring an associated value like this.

  • Extending an Existing Type to Specify an Associated Type

Just like how you didn't need to define the type parameter separately above, you don't need to specify the associated value separately either.

  • Adding Constraints to an Associated Type
protocol Container { associatedtype Item: Equatable mutating func append(_ item: Item) var count: Int { get } subscript(i: Int) -> Item { get } }

You can add constraints just like with type parameters.

  • Using a Protocol in Its Associated Type's Constraints
protocol Container { associatedtype Item var items: [Item] { get set } mutating func append(_ item: Item) var count: Int { get } subscript(i: Int) -> Item { get } } protocol SuffixableContainer: Container { associatedtype Suffix: SuffixableContainer where Suffix.Item == Item func suffix(_ size: Int) -> Suffix } struct Stack<Element: Equatable>: Container { var items: [Element] = [] mutating func push(_ item: Element) { items.append(item) } mutating func pop() -> Element { items.removeLast() } mutating func append(_ item: Element) { push(item) } var count: Int { items.count } subscript(i: Int) -> Element { items[i] } } extension Stack: SuffixableContainer { typealias Suffix = Stack func suffix(_ size: Int) -> Stack { var result = Stack() for index in (count-size)..<count { result.append(self[index]) } return result } }
associatedtype Suffix: SuffixableContainer where Suffix.Item == Item

Just roughly know that where is another way to add constraints to an associated value,
and focus on what Suffix.Item == Item does.

The Suffix associated value also has to conform to SuffixableContainer.
And since SuffixalbeContainer inherits from Container, Suffix has an Item associated value.

In the part that extends Stack, for Stack<Element>, Suffix becomes Stack,
and since no type argument was specified for Stack, Suffix.Item becomes Element,
and Item was Element, so they're the same.

In other words, think of it as a constraint that makes the type argument you passed when first defining Stack
the same as the generic type argument of the return value of the suffix(_: ) method.

  • Generic Where Caluses
func allItemsMatch<C1: Container, C2: Container> (_ someContainer: C1, _ anotherContainer: C2) -> Bool where C1.Item == C2.Item, C1.Item: Equatable { // Check that both containers contain the same number of items. if someContainer.count != anotherContainer.count { return false } // Check each pair of items to see if they're equivalent. for i in 0..<someContainer.count { if someContainer[i] != anotherContainer[i] { return false } } // All items match, so return true. return true }

You can add constraints to types with a where clause like this.

  • Extensions with a Generic Where Clause
extension Stack where Element: Equatable { func isTop(_ item: Element) -> Bool { guard let topItem = items.last else { return false } return topItem == item } }

You can make it extend only when Element conforms to Equatable.

  • Contextual Where Clauses
extension Container { func average() -> Double where Item == Int { var sum = 0.0 for index in 0..<count { sum += Double(self[index]) } return sum / Double(count) } func endsWith(_ item: Item) -> Bool where Item: Equatable { return count >= 1 && self[count-1] == item } }
extension Container where Item == Int { func average() -> Double { var sum = 0.0 for index in 0..<count { sum += Double(self[index]) } return sum / Double(count) } } extension Container where Item: Equatable { func endsWith(_ item: Item) -> Bool { return count >= 1 && self[count-1] == item } }

The code above and the code below mean exactly the same thing.

  • Associated Types with a Generic Where Clause
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 }

Saw this earlier

  • Generic Subscripts
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 } }

You can apply it to subscripts the same way you've been using it.

Comments

Leaving a comment stores your nickname (or GitHub profile info). Privacy policy

GitHub