Type constraints

It is great that our function works with any type, but what if our API user tries to use the calculate function on types that cannot be used in arithmetic calculations?

To mitigate this problem, we can use type constraints. Using type constraints, we will be able to enforce the usage of a certain type. Type constraints specify that a type parameter must inherit from a specific class or conform to a particular protocol or protocol composition. Collections are examples of type constraints that we are already familiar with in the Swift programming language. Collections are generics in Swift, so we can have arrays of Int, Double, String, and so on.

Unlike Objective-C, where we could have different types in a collection, in Swift we need to have the same type that complies to the type constraint. For instance, the keys of a dictionary must conform to the Hashable protocol.

We can specify type constraints with either of the following two syntaxes:

<T: Class> or <T: Protocol>

Let's go back to our calculate example and define a numerical type constraint. There are different protocols such as Hashable and Equatable. However, none of these protocols are going to solve our problem. The easiest solution would be defining our protocol and extending the types that we want to use by conforming to our protocol. This is a generic approach that can be used to solve similar problems:

protocol NumericType {
    func +(lhs: Self, rhs: Self) -> Self
    func -(lhs: Self, rhs: Self) -> Self
    func *(lhs: Self, rhs: Self) -> Self
    func /(lhs: Self, rhs: Self) -> Self
    func %(lhs: Self, rhs: Self) -> Self
}

We define a protocol for numeric types with related basic math operators. We will require the types that we want to use to conform to our protocol. So we extend them as follows:

extension Double : NumericType { }
extension Float  : NumericType { }
extension Int    : NumericType { }
extension Int8   : NumericType { }
extension Int16  : NumericType { }
extension Int32  : NumericType { }
extension Int64  : NumericType { }
extension UInt   : NumericType { }
extension UInt8  : NumericType { }
extension UInt16 : NumericType { }
extension UInt32 : NumericType { }
extension UInt64 : NumericType { }

Finally, we need to define the type constraint in our function as follows:

func calculate<T: NumericType>(a: T,
                               b: T,
                           funcA: (T, T) -> T,
                           funcB: (T) -> T) -> T {

    return funcA(funcB(a), funcB(b))
}

print("The result of adding two squared values is: (calcualte(a: 2, b: 2,
  funcA: addTwoValues, funcB: square))") // prints "The result of adding
  two squared value is: 8"

As a result, we have a function that accepts only numerical types.

Let's test it with a non-numeric type to ensure its correctness:

func format(a: String) -> String {
    return "formatted (a)"
}

func appendStrings(a: String, b: String) -> String {
    return a + b
}

print("The result is: (calculate("2", b: "2", funcA:
  appendStrings, funcB: format))")

This code example does not compile because of our type constraint, which can be seen in the following screenshot:

Type constraints

Where clauses

The where clauses can be used to define more complex type constraints, for instance, to conform to more than one protocol with some constraints.

We can specify additional requirements on type parameters and their associated types by including a where clause after the generic parameter list. A where clause consists of the where keyword, followed by a comma-separated list of one or more requirements.

For instance, we can express the constraints that a generic type T inherits from a C class and conforms to a V protocol as <T where T: C, T: V>.

We can constrain the associated types of type parameters to conform to protocols. Let's consider the following generic parameter clause:

<Seq: SequenceType where Seq.Generator.Element: Equatable>

Here, it specifies that Seq conforms to the SequenceType protocol and the associated Seq.Generator.Element type conforms to the Equatable protocol. This constraint ensures that each element of the sequence is Equatable.

We can also specify that two types should be identical using the == operator. Let's consider the following generic parameter clause:

<Seq1: SequenceType, Seq2: SequenceType where 
  Seq1.Generator.Element == Seq2.Generator.Element>

Here, it expresses the constraints that Seq1 and Seq2 conform to the SequenceType protocol and the elements of both sequences must be of the same type.

Any type argument substituted for a type parameter must meet all the constraints and requirements placed on the type parameter.

We can overload a generic function or initializer by providing different constraints, requirements, or both on the type parameters in the generic parameter clause. When we call an overloaded generic function or initializer, the compiler uses these constraints to resolve which overloaded function or initializer to invoke.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset