Power of Swift Generics — Part 1

Generic function, generic type and type constraints

Generics?

When they work you love them, when they don’t you hate them. 😀😀

In real life everyone knows power of generics: Waking up in the morning, deciding what to drink, filling up your cup ☕️

unsplash.com

Swift is a type safe language. Whenever we work with types, we need to specify them. For instance, we need a function that can handle more than one type. Swift already provides Any and AnyObject but it is not a good practice to use them unless we have to. Using Any and AnyObject will make our code fragile as we will not be able to catch type mismatching during compile time. Generics are the solution to our requirement.

Generic code allows you to write reusable functions and data types that can work with any type that matches the constraints you define while providing compile-time type safety. It allows you to write code that avoids duplication and expresses its intent in a clear, abstracted manner. For example, types such as Array, Set and Dictionary types are generic over there elements.

Let’ s say, we have to print array of integers and strings. I can create 2 functions to do this work.

let intArray = [1, 2, 3, 4]
let stringArray = [a, b, c, d]
func printInts(array: [Int]) {
print(intArray.map { $0 })
}
func printStrings(array: [String]) {
print(stringArray.map { $0 })
}

Now I have to print array of floats, or array of custom objects. If we look at above functions, only difference is in type used. So instead of repeating code we can write reusable generic function.

Swift Generics history:

Generic Functions

Generic function can work with any type, identified by the placeholder type T. The placeholder type name doesn’t say anything about what T must be, but it does say that both array must be of the type T, whatever T represents. The actual type to use in place of T is determined each time the print(_:) function is called.

func print<T>(array: [T]) {
print(array.map { $0 })
}

Placeholder Types or Parametric polymorphism

The placeholder type T in above example is a type parameter. You can provide more than one type parameter by writing multiple type parameter names within the angle brackets, separated by commas.

If we take a look at Array<Element> and Dictionary<Key, Element> they have a named type parameters i.e. Element & Key, Value which tells about the relationship between the type parameter and the generic type or function it’s used in.

Note: Always give type parameters upper camel case names (such as T and TypeParameter) to indicate that they’re a placeholder for a type, not a value.

Generic Types

These are custom classes, structures, and enumerations that can work with any type, in a similar way to Array and Dictionary.

Let’s create stack

Currently this stack is capable of holding only Integer elements, if I have to store other type of elements I need to either create another stack or convert this into generic one.

Generic Type Constraints

Since a generic can be of any type, we can’t do a lot with it. It’s sometimes useful to enforce type constraints on the types that can be used with generic functions and generic types. Type constraints specify that a type parameter must conform to a particular protocol or protocol composition.

For example, Swift’s Dictionary type places a limitation on the types that can be used as keys for a dictionary. Dictionary needs its keys to be hashable so that it can check whether it already contains a value for a particular key.

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

In above gist, we created stack of type T but we can’t compare two stack. As all types doesn’t conform to Equatable. We need to modify that to use Stack<T: Equatable>.

How Generics work?

Let’s take an example.

func min<T: Comparable>(_ x: T, _ y: T) -> T {
return y < x ? y : x
}

Compiler lacks two essential pieces of information it needs to emit code for function:

  • The sizes of the variables of type T
  • The address of the specific overload of the < function that must be called at runtime.

Whenever the compiler encounters a value that has a generic type, it boxes the value in a container. This container has a fixed size to store the value; if the value is too large to fit, Swift allocates it on the heap and stores a reference to it in the container.

The compiler also maintains a list of one or more witness tables per generic type parameter: one is value witness table, plus one protocol witness table for each protocol constraint on the type. The witness tables are used to dynamically dispatch function calls to the correct implementations at runtime.


Thanks for reading article. Part 2 of series is available here : https://medium.com/swift-india/power-of-generics-part2-b39f412a1d54

You can catch me at:

Linkedin: Aaina Jain

Twitter: __aainajain