Generics in Swift: Expanding Flexibility and Code Reusability
Tags: swift, mobile, programming, ios
Published: 2024-02-06 21:04:09

## Introduction

Generics are a powerful feature in Swift that allow for the creation of flexible, reusable, and type-safe code, avoiding duplication and enhancing clarity. In this article, I will explore the motivation behind using generics, introduce their syntax, and fundamental concepts such as generic functions, classes, and structs, type constraints, and share an interesting curiosity of the language: the relationship between generics and optionals.

## Motivation

You might already know that Swift is a strongly typed language, meaning you must explicitly define the type of each variable, constant, parameter, and return value in your code.

Although Swift also has type inference functionality (where the compiler automatically deduces the type of a variable based on the value assigned to it), the types are still well defined. If you declare that a function receives a value of type `String`, you must pass a string and nothing else.

See below a function that returns the first element of an array:

```swift
let names = ["Paul", "Steve", "Anna"]

func firstElement(_ array: [String]) -> String? {
	return array.first
}

if let firstName = firstElement(names) {
	print(firstName)
}
```

In the example above, we are defining that the type of elements that should compose the array is `String`. And if I want to get the first element of an array of numbers? I would need to create another function:

```swift
let numbers = [5, 10, 15]

func firstElement(_ array: [Int]) -> Int? {
	return array.first
}

if let firstNumber = firstElement(numbers) {
	print(firstNumber)
}
```

A lot of code repetition, right? Well, without the use of generics, we would have to write multiple functions, one for each type, which leads to unnecessary code duplication.

## Understanding Generics

Generics allow writing functions and types that can work with any type, maintaining the type safety that Swift brings us. The same problem of returning the first element of the array can be solved with generics, as follows:

```swift
let names = ["Gi", "Daniel", "Ana"]
let numbers = [5, 10, 15]

func firstElement<T>(_ array: [T]) -> T? {
	return array.first
}

if let firstName = firstElement(names) {
	print(firstName)
}

if let firstNumber = firstElement(numbers) {
	print(firstNumber)
}
```

Here, `T` is a type placeholder, representing **any type** that will be provided when the function is called. You can use any placeholder name you wish, but `T` is commonly used to represent a "type" generic.

## Generic Functions

The example you saw above is a generic function. Shall we see another example?

Consider the following problem: you need to create a function that will create and return an array containing a certain value repeated a specific number of times. This can be useful in various situations, such as initializing an array with a default value or generating test data with repeated values.

This value can be any type. Here's an example of how you can create this function using a generic type:

```swift
func makeArray<T>(repeating item: T, numberOfTimes: Int) -> [T] {
	var result: [T] = []

    for i in 0..<numberOfTimes {
        result.append(item)
    }

    return result
}

print(makeArray(repeating: 3, numberOfTimes: 4)) // [3, 3, 3, 3]
print(makeArray(repeating: "Hello", numberOfTimes: 3)) // ["Hello", "Hello", "Hello"]
```

What's happening here? I'm creating a generic function that takes a generic type called `T` and returns an array of type `T`. Inside this function, I create an array of type `T`, initially empty, and fill it with the item that was received as a parameter. Simple, right?

## Generic Classes and Structs

Swift also allows defining generic classes, structs, and even enums. See an example below of a generic class that implements a stack:

```swift
class Stack<T> {
    public var items = [T]()

    func push(_ item: T) {
        self.items.append(item)
    }

    func pop() -> T? {
        return self.items.popLast()
    }
}

let stack = Stack<Int>()
stack.push(3)
stack.push(10)
stack.push(12)
stack.pop()
print(stack.items) // [3, 10]
```

Notice the instance of this class `Stack`, realized by the constant `stack`. We cannot simply instantiate with a `Stack()` as we would with a normal class; we need to define the type `T`, hence we use `Stack<Int>()`. If we opted for the type `String` instead of `Int`, we would do `Stack<String>()`.

The implementation with

 `struct` would be the same way.

## Type Constraints

Swift allows you to specify type constraints on generic definitions, which restricts the types that can be used with a generic type or function. See the example below:

```swift
func findIndex<T: Equatable>(from valueToBeFound: T, in array: [T]) -> Int? {
    for (index, value) in array.enumerated() {
        if value == valueToBeFound {
            return index
        }
    }
    return nil
}

if let index = findIndex(from: 3, in: [1, 2, 3, 4]) {
    print(index) // 2
}
```

In the code above, `T: Equatable` ensures that `T` implements the `Equatable` protocol, allowing the values to be compared using `==`.

If you did not include the type constraint `T: Equatable` in the definition of the function `findIndex`, the Swift compiler would generate an error because it would not be able to guarantee that the types passed to the function support the equality operator (`==`). Swift is a strongly typed language and requires that the operations performed on types are known and safe at compile time.

Therefore, without the constraint `T: Equatable`, Swift has no way of knowing if `T` can be compared with `==`. The `Equatable` protocol declares the equality operator `==`, meaning that any type that conforms to `Equatable` can be compared for equality. Without this conformity, there's no guarantee that `T` supports this operation.

Let's see another example below:

```swift
func encode<T: Encodable>(_ item: T) {
    JSONEncoder().encode(item)
}
```

In the example, the function `encode` is a generic function that accepts any type `T` as its parameter, with the condition that `T` must conform to the `Encodable` protocol.

Remember, the `Encodable` protocol is part of Swift's encoding and decoding data library, known as `Codable`.

To ensure that the function `JSONEncoder().encode(item)` works, our parameter `item` needs to be a type that conforms to the `Encodable` protocol. For this reason, the function has a type constraint, to ensure that this function works as expected and does not cause a compilation error.

## The Relationship Between Optionals and Generics

Did you know that optionals are actually a generic enum? That's right! The definition of `Optional` is something similar to:

```swift
enum Optional<Wrapped> { 
    case none 
    case some(Wrapped) 
}
```

This means that when you declare a variable as `Int?`, it's shorthand for `Optional<Int>`.

```swift
var optionalInt: Int? = 42
// Is equivalent to:
var anotherOptionalInt: Optional<Int> = .some(42)

switch anotherOptionalInt {
case .none:  
    print("nil")
case .some(let value):  
    print(value) // 42
}
```

## Conclusion

Generics are fundamental in Swift, allowing you to write cleaner, more flexible, and reusable code. They help us avoid duplication and maintain type safety. By learning and applying generics, you can take full advantage of Swift's powerful type features. Generics, therefore, are not just a tool for creating efficient code, but also a bridge to deeply understanding how Swift operates with types and values.

Thank you so much for reading this far.