Multiple Type Constraints in Swift


In Swift, there are several ways to implement type constraints on types. We will use some common approaches like the where clause, protocol, etc. Generics provide lots of flexibility to write better and more secure code in Swift. We can apply generics to collections, custom types, etc. One of them is generic type constraints. Using type constraints, you can make your generic code behave that matches a certain set of constraints whatever you define.

Swift provides multiple ways to specify type constraints on generic type parameters.

Type constraints using the "where" clause

The "where" clause in Swift is a highly powerful idea for specifying one or more type constraints on a class, struct, enum, and so on. You may establish many sorts of restrictions using the "where" clause to regulate the behaviour of your code dependent on the scenario.

Example 1

Find the sum of all elements of an array

Let's start with a basic example in which you have to find the sum of all elements in an array. But here you have to add constraints to the method to perform the sum operation on the numeric elements. To define the sum function, we will create an extension with a type constraint using the "where" clause. We will conform to the Numeric protocol. Here is the code −

import Foundation
extension Array where Element: Numeric {
   func sum() -> Element {
      return reduce(0, +)
   }
}
let numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
let sum = numbers.sum()
print("Original array: \(numbers)")
print("Sum is: \(sum)")

Output

Original array: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Sum is: 45

In the above example, we created an extension along with a method named "sum". The function should calculate the sum of all numeric elements using the reduce function.

Example 2

Check for equal elements for Equatable types

In this example, we will write a function to check whether elements are equal or not. Here, we will add a type constraint that is the Equatable protocol. Here is the code −

import Foundation
func printIfEqual<T: Equatable>(_ a: T, _ b: T) where T: CustomStringConvertible {
   if a == b {
      print("Values are equal: \(a)")
   } else {
      print("Values are not equal")
   }
}
let a = 10
let b = 10
printIfEqual(a, b)
let c = "Hello"
let d = "World"
printIfEqual(c, d)

Output

Values are equal: 10
Values are not equal

In the above example, the function printIfEqual takes two generic parameters a and b, which must conform to the Equatable protocol and the CustomStringConvertible protocol. The CustomStringConvertible protocol is used to convert the values to a string so they can be printed.

As part of the printIfEqual function, we first check whether the values are similar or not by using the == operator. If both values are equal print the value otherwise print that both values are not equal.

The function (say printIfEqual) is called twice, once with integer values and once with string values. Since the integer values are equal, the function prints the values. However, the string values are not equal, so the function prints a message indicating that the values are not equal.

Type Constraints using Protocols

You can also specify a protocol as a type constraint. This is useful when you want to restrict the type parameter to a specific protocol.

For example, you want to define a common protocol that will be responsible for managing different types of models in the code. We define a protocol called ModelManager which has the following characteristics −

  • Associated Model type

  • A function to get a collection of the Model types where collection has type constraint that is Element has to be a type of Model type.

  • Query type, which can be anything that an implementor wants to use to express a query - for example an enum.

import Foundation
protocol ModelManager {
   associatedtype Model
   associatedtype Collection: Swift.Collection where Collection.Element == Model
   associatedtype Query
   func models(matching query: Query) -> Collection
}

Now, with the above, we are free to implement model managers that all provide the exact same API - but still can use types and collections that match their needs. For example, to implement a ModelManager for a User model, we may choose to use Array as our collection type and a Query enum that lets us match users by name or age −

struct User {
   let name: String
   let age: Int
}
struct Genre: Hashable {
   // declare properties and methods here
}
struct Movie {
   // declare properties and methods here
}
class UserManager: ModelManager {
   typealias Model = User
   
   enum Query {
      case name(String)
      case ageRange(Range)
   }
    
   func models(matching query: Query) -> [User] {
      // write complete code here
      return []
   }
}

For other models, it may be a much better fit to use Dictionary as the collection type. Here's another manager that keeps track of movies based on their genre, and lets us query for movies matching either a name or a director −

class MovieManager: ModelManager {
   typealias Model = (key: Genre, value: Movie)
    
   enum Query {
      case name(String)
      case director(String)
   }
    
   func models(matching query: Query) -> [Genre : Movie] {
      // write complete code here
      return [:]
   }
}

By using constrained associated types we can get access to the power of protocol-oriented programming and enable easier mocking & testability, while still having lots of flexibility when implementing our concrete types.

Conclusion

Swift's generic system makes heavy use of type restrictions. They give you the ability to limit the types that can be used with a generic function or type to only those that comply with particular criteria. Swift has a variety of methods for defining type restrictions, including the usage of protocols, classes, and the where clause. You can develop more type-safe, reusable code with less duplication and less error by employing type constraints

Updated on: 11-Apr-2023

726 Views

Kickstart Your Career

Get certified by completing the course

Get Started
Advertisements