Generics Part 2 – Type Constraints

Welcome to part 2 of the series where we study the fundamentals of Generics in Swift. If you have not read part 1 or lost your memory, please review it right away while I loiter around. Welcome back. A quick recap of the previous part: We saw how to define type parameters – which are placeholders that will be filled by type arguments when a generic function or type is used.

Generic Type Constraints

Using a type parameter gives us the capability to stipulate that some arguments/return values are of the same Type, whatever that might be. It would be highly useful if we could impose additional requirements on the type parameters.

Let’s try to write a function that returns the index of an item in an array.

func findIndex<T>(ofElement elementToFind: T, inArray array: [T]) -> Int? {
  for (index, element) in array.enumerated() {
    if element == elementToFind {
      return index
    }
    return nil
}

Above, we used the type parameter T, so that we could find an element of type T inside an array containing many such elements. It was necessary to use the same type for both because it doesn’t make sense to find a String in an array of Ints. The above code doesn’t actually compile because of the equality check between element and elementToFind. The equality check is only defined for Equatable (a protocol) types, so it is imperative that T be Equatable.

A naive and non-generic attempt to fix this is:

func findIndex(ofElement elementToFind: Equatable, inArray array: [Equatable]) -> Int? {
  for (index, element) in array.enumerated() {
    if element == elementToFind {
      return index
    }
    return nil
}

The element is something that conforms to Equatable, and the array is also made up of Equatable elements. The compiler throws the infamous “Protocol Equatable can only be used as a generic constraint because it has Self or associated type requirements” error. We will discern this error message in Part 3, when we survey Protocols with Associated Types, but it should not be too hard to understand why this is not legal code. Both String and Int are equatable but it is non-sensical to check for their equality. The equality check only makes sense between two Ints or two Strings.

The solution is generic type constraints.

We saw that there are two requirements for the findIndex function, namely, the element and array types should match AND the type should be Equatable.

The Equatable requirement can be specified as part of the Generic Type Parameter list (also known as the generic parameter clause). If you remember, this list is prescribed between angular brackets immediately after the function or type name.

The syntax is

func something<T: SomeProtocol, U: SomeClass>()

from which the correct solution readily follows as:

func findIndex<T: Equatable>(ofElement elementToFind: T, inArray array: [T]) -> Int? {
  for (index, element) in array.enumerated() {
    if element == elementToFind {
      return index
    }
    return nil
}

In our example, we used the Equatable protocol as the constraint. We can also use protocol compositions and classes as constraints. Why not structs or enumerations? Since structs/enums stand alone and do not support inheritance or adoption, we might as well use the name of a struct in place of a generic type.

func doSomething<T: SomeStruct>(_ a: T, _ b: T) is equivalent to func doSomething(_ a: SomeStruct, _ b: SomeStruct), except that the former does not compile.

Generic Where Clause

There is another way to specify type constraints and that is in the generic where clause. The syntax consists of the where keyword followed by the constraints, just before the opening braces

func doSomething<T, U>(_ a: T, _ b: U) where T: Equatable, U: Codable { }

Why are there two ways for specifying type constraints? The generic where clause is a lot more powerful. We will study the generic where clause in much more detail but only after dissecting Protocols with Associated Types in the next part.

Quiz Time!

  1. Is this valid syntax?
struct MyStruct<T: Equatable> {
   let name: T
   let profession: T
}

2. Is this valid syntax?

struct YourStruct<T> where T: Codable {
    let yes: T
    let no: T
}

3. Is this valid syntax?

func doSomething<T: Equatable & Codable>(_ arg1: T) -> T {
    return arg1
}

4. Is this valid syntax?

func doNothing<T>(_ arg1: T, _ arg2: T) where T: Equatable, T: Codable {
}

5. Is this valid syntax?

func uselessFunction<T: Equatable, T: Codable>(_ arg1: T, _ arg2: T) { }

6. Is this valid syntax?

func iamAMethod<T>(_ a: T, _ b: T) where T: Equatable throws {}

7. Is this valid syntax?

func facelessFunc<T where T: Equatable>(_ a: T) -> T? { return nil }

Answers

  1. Yes, generic types have the same syntax to define constraints
  2. Yes, generic types support the where clause, just as generic methods do.
  3. Yes, Equatable & Codable is a protocol composition, which is supported as a constraint
  4. Yes
  5. No
  6. No, the where clause should always appear immediately before the opening brace.
  7. No, but it was supported in an older version of Swift

Conclusion

This concludes this short and sweet article introducing the necessity and syntax for constraining generic types. In the next part, we will inspect an important construct of generic programming – Protocols with Associated Types.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply