• Skip to main content
  • Skip to footer

Khawer Khaliq

  • Home

Better Generic Types in Swift With the Numeric Protocol

Share
Tweet
Share
Pin

*** Updated for Swift 4.1 ***

In this tip, we will see how the Numeric protocol can be used as a constraint on type parameters of generic types to ensure that certain type parameters can only be used with numeric types, without having to specify the specific numeric type until we create an instance. We will also look at how protocol composition can be used to provide additional related functionality as required.

Contents

Generic types and type constraints
The Numeric protocol as a type constraint
Comparing for Equality
Performing basic arithmetic
Using protocol composition
Adding functionality to sequences and collections
Conclusion

Generic types and type constraints

Generic types in Swift are classes, structs and enums that can work with any type. A generic type specifies one or more type parameters, which provide placeholder names for types to be specified when the generic type is instantiated. These placeholder names can be used within the definition of the generic type, and any of its extensions, wherever the name of a type would be expected, e.g., the type of a parameter or return value of a method, the type of a property, etc.

It is desirable in many situations to enforce certain constraints on the types that can be used with a generic type. A type constraint requires that a type parameter must inherit from a certain class or conform to a certain protocol. A type constraint may include more than one protocol using protocol composition, which we will cover a bit later on.

The Numeric protocol as a type constraint

By using the Numeric protocol as a constraint on type parameters, we can ensure that those type parameters can only be used with numeric types. However, we don’t need to specify the actual type to be used until we create an instance.

Let’s consider a scenario where we want to define a value type to represent the stock level of items in an inventory control application. We could have items whose quantities are measured in discrete units but we may also have items whose quantities may be in fractional amounts. We want to ensure that quantities are always numeric, but we want to be flexible about which numeric type to use for each item.

We start by defining a generic struct with a stored property for the available quantity of the stock item, which is set in the initializer. The stored property can be of any type that conforms to the Numeric protocol.

struct StockLevel<T: Numeric> {
    init(_ available: T) {
        self.available = available
    }
    
    var available: T
}

This allows us to create instances of StockLevel with quantities that are integers as well as fractional numbers.

var intStockLevel = StockLevel(5)
var doubleStockLevel = StockLevel(2.5)

If we try to create a StockLevel instance with a non-numeric argument, we get a compiler error.

let invalidStockLevel = StockLevel("level")
// Compiler error: Argument type 'String' does not conform to expected type 'Numeric'

It is noteworthy that instances created from the same generic type may actually be instances of different types, depending on which type(s) are used in place of the type parameter(s). The value assigned to the variable intStockLevel in the example above is of type StockLevel<Int>. Similarly, the value assigned to the variable doubleStockLevel is of type StockLevel<Double>.

We can use the type(of:) function to verify that the variables intStockLevel and doubleStockLevel have been inferred to be of the correct types.

print(type(of: intStockLevel))      // StockLevel<Int>
print(type(of: doubleStockLevel))   // StockLevel<Double>

If we try to assign the value of the variable doubleStockLevel to the variable intStockLevel, the compiler stops us dead in our tracks.

intStockLevel = doubleStockLevel
// Compiler error: Cannot assign value of type 'StockLevel<Double>' to type 'StockLevel<Int>'

This gets the Swift type system working for us to ensure that we don’t mix types and values in an inappropriate way, reducing the need for manual validation and preventing an entire class of errors.

Comparing for Equality

Attribute-based equality is one of the defining characteristics of values. Accordingly, all values types we define should conform to the Equatable protocol. When value types are composed from other value types, as most custom value types are, the Equatable conformance of the constituent types is used to implement Equatable conformance for the type being composed.

A Swift struct can automatically synthesize conformance to Equatable if all of its members are Equatable. This is done by declaring Equatable conformance without implementing any of its requirements. This conformance must be part of the original type definition and not in an extension. Since the Numeric protocol inherits from Equatable, all we need to do for our StockLevel type is declare Equatable conformance.

struct StockLevel<T: Numeric>: Equatable { ... }

Here is a quick check to make sure this works.

let level = StockLevel(3)
let equalToLevel = StockLevel(3)
let notEqualToLevel = StockLevel(5)

print(level == equalToLevel)        // true
print(level == notEqualToLevel)     // false

Performing basic arithmetic

One of the common uses of numeric types is to perform basic arithmetic. The Numeric protocol declares binary operators for addition, subtraction and multiplication (+, –, *) and also their mutating counterparts (+=, -=, *=).

We can use this to define the following method in our StockLevel type to receive new stock.

mutating func receive(_ quantity: T)  {
    available += quantity
}

Here is the new method in action.

var stockLevel = StockLevel(2)
stockLevel.receive(4)
print(stockLevel.available)   // 6

It is noteworthy that the Numeric protocol does not specify a binary division operator. This appears to be a conscious decision by the designers of the standard library. It fits well with the notion of closure of operations.

A given set is said to be closed under an operation if that operation, when performed on any members of the set, will result in another member of the same set. This is an important property of many mathematical operations. Numeric protocols in the standard library in general specify arithmetic operators in terms of Self. Here is how the non-mutating binary addition, subtraction and multiplication operators are specified in the Numeric protocol.

static func +(Self, Self) -> Self
static func -(Self, Self) -> Self
static func *(Self, Self) -> Self

This means that the types conforming to Numeric are expected to be closed under these operations, i.e., both of the parameters as well as the return value should be of the same concrete type. This is true for addition, subtraction and multiplication but not for division, as dividing an integer by another integer may yield a non-integer value. If this value is returned as is, the operation will not be closed under the set of integers. Forcing an integer return value, on the other hand, may result in loss of information.

A high-level protocol like Numeric should not concern itself with these kinds of details. They are handled in protocols dealing with more specific sets of numbers.

If the division operation is required, depending on the context and the type of behaviour desired, one of the following protocols can be used:

  • The BinaryInteger protocol, which is the basis for all the integer types provided by the standard library. It specifies integer division, which returns just the integer quotient and discards the remainder. The remainder, if required, is returned by the % operator.
  • The FloatingPoint protocol, which is the basis of types used to represent floating-point numbers, or the BinaryFloatingPoint protocol, which extends the FloatingPoint protocol with operations specific to floating-point binary types, as defined by the IEEE 754 specification.

Using protocol composition

Another operation often used with numeric types is comparing for inequality. The Numeric protocol, however, does not support inequality operators. To make our generic type support inequality comparisons, we turn to protocol composition.

Protocol composition combines multiple protocols into a single requirement. Although a protocol composition does not define a new protocol type, it has the same effect as defining a temporary protocol which combines the requirements of all the protocols included in the composition. This encourages use of the Interface Segregation Principle, so protocols can be defined more narrowly but requirements from various protocols can be combined on the fly as and when required.

We modify the definition of our StockLevel type, replacing the Numeric protocol in the type constraint with a composition of the Numeric and Comparable protocols.

struct StockLevel<T: Numeric & Comparable>: Equatable { ... }

We can use the additional functionality provided by the Comparable protocol to add to our StockLevel type a method to requisition stock. Since a reasonable business rule would be that a request to requisition more stock than is available should fail, we define the following error type with the single case to reflect a situation where sufficient quantity is not available.

enum StockLevelError: Error {
    case insufficientQuantityAvailable
}

We then add the following method to the StockLevel type to requisition stock. If the quantity requisitioned exceeds the available quantity, the error defined above is thrown.

mutating func requisition(_ quantity: T) throws {
    guard quantity <= available else {
        throw StockLevelError.insufficientQuantityAvailable
    }
    available -= quantity
}

Here is some code to test our handiwork.

func requisitionTester(stockLevel: inout StockLevel<Int>, quantity: Int) {
    do {
        try stockLevel.requisition(quantity)
        print("\(quantity) unit(s) successfully issued. \(stockLevel.available) unit(s) available")
    } catch {
        print("Amount requisitioned exceeds available quantity")
    }
}

var level = StockLevel(5)
requisitionTester(stockLevel: &level, quantity: 3)   // 3 unit(s) successfully issued. 2 unit(s) available
requisitionTester(stockLevel: &level, quantity: 3)   // Amount requisitioned exceeds available quantity

Adding functionality to sequences and collections

Another use of the Numeric protocol is to extend the functionality of the sequence and collection types included in the standard library with methods that only apply when the elements of the sequence or collection are numbers. Here is an example of a method defined through an extension on the Sequence protocol to calculate the sum of all elements of a given sequence provided the elements are numeric.

extension Sequence where Element: Numeric {
    func sum() -> Element {
        return reduce(0, +)
    }
}

print([1, 2, 3, 4].sum())   // 10
print((1...4).sum())        // 10

As shown, this can be used to calculate the sum of a numeric array, range, etc.

Conclusion

The Numeric protocol is a very useful addition to the standard library as it allows programmers to use the power of generics while constraining certain types to be numeric and certain functionality to be available only for numeric types, without having to specify a particular numeric type until an instance is created. This leverages the type safety enforced by Swift, reduces the need for manual validation and makes code simpler, clearer and less prone to bugs.

Thank you for reading! I always appreciate constructive comments and feedback. Please feel free to leave a comment in the comments section of this post or start a discussion on Twitter.

Subscribe to get notifications of new posts

No spam. Unsubscribe any time.

Reader Interactions

Comments

  1. rezwits says

    January 6, 2021 at 9:22 am

    Awesome, thanks! Way further than I needed, for now at least ;). But Swift seems to keep going and going…

    Reply

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Footer

Protocol-Oriented Programming (POP) in Swift

Use protocol-oriented programming to think about abstractions in a completely different way, leveraging retroactive modeling to introduce appropriate abstractions at any point in the development cycle, and creating traits that can let types opt into functionality simply by conforming to a protocol.

Pattern Matching With Optionals in Swift

The optional pattern explained in detail, including how to use it with a variety of conditional statements and loops, and how to add extra conditions when required, with a section on creating more complex pattern matching code involving optionals.

Test-Driven Development (TDD) in Swift

Learn how to use Test-Driven Development (TDD) in Swift which not only enables you to write more reliable and maintainable code but also allows refactoring of code in small increments and with greater ease and confidence.

Unwrapping Optionals With Optional Binding in Swift

Learn how to use optional binding to extract the value wrapped by an optional to a constant or variable, as part of a conditional statement or loop, exploring where optional chaining may be used in place of optional binding, and where these techniques can be used together.

Unit Testing and UI Testing in Swift

Learn how to use unit testing to gain confidence in the correctness of code at the unit level, and use UI testing to ensure that the application fulfills user requirements, explained in detail and illustrated using an example application built using SwiftUI.

When and How to Use the Equatable and Identifiable Protocols in Swift

Detailed coverage of the Equatable and Identifiable protocols, and how they can be used to model not only values but also domain entities with identity using Swift value types, to create code that is more efficient, easier to reason about, easily testable and more concurrency-friendly.

The Power of Optional Chaining in Swift

Learn how to use optional chaining to work safely with optionals, to set and retrieve the value of a property of the wrapped instance, set and retrieve a value from a subscript on the wrapped instance, and call a method on the wrapped instance, all without having to unwrap the optional.

What Are Swift Optionals and How They Are Used

This article explains what Swift optionals are, why Swift has them, how they are implemented, and how Swift optionals can be used to better model real-world domains and write safer and more expressive code.

A Protocol-Oriented Approach to Associated Types and Self Requirements in Swift

Use protocol-oriented programming to avoid having to use associated types in many situations but also to effectively use associated types and Self requirements, where appropriate, to leverage their benefits while avoiding the pitfalls.

Encapsulating Domain Data, Logic and Business Rules With Value Types in Swift

Leverage the power of Swift value types to manage domain complexity by creating rich domain-specific value types to encapsulate domain data, logic and business rules, keeping classes lean and focused on maintaining the identity of entities and managing state changes through their life cycles.

Rethinking Design Patterns in Swift – State Pattern

The State pattern, made simpler and more flexible with the power of Swift, with a detailed worked example to illustrate handling of new requirements, also looking at key design and implementation considerations and the benefits and practical applications of the pattern.

Conditional Logic With and Without Conditional Statements in Swift

Shows how to implement conditional logic using conditional statements as well as data structures, types and flow control mechanisms such as loops, balancing simplicity and clarity with flexibility and future-proofing.

Understanding, Preventing and Handling Errors in Swift

Examines the likely sources of errors in an application, some ways to prevent errors from occurring and how to implement error handling, using the error handling model built into Swift, covering the powerful tools, associated techniques and how to apply them in practice to build robust and resilient applications.

When and How to Use Value and Reference Types in Swift

Explores the semantic differences between value and reference types, some of the defining characteristics of values and key benefits of using value types in Swift, leading into a discussion on how value and reference types play a complementary role in modeling real-world domains and designing applications.

Swift Protocols Don’t Play Nice With Equatable. Or Can They? (Part Two)

Uses type erasure to implement Equatable conformance at the protocol level, allowing us to program to abstractions using protocol types while safely making equality comparisons and using functionality provided by the Swift Standard Library only available to types that conform to Equatable.

Copyright © Khawer Khaliq, 2017-25. All rights reserved.