Alexito's World

A world of coding 💻, by Alejandro Martinez

Swift where clause in generic contexts

Swift type system is quite powerful and the where clause is a very important part of how developers interface with it. This keyword allows developers to constraint code to a set of conditions. Swift 5.3 frees this keyword to be used in more contexts.

Subscribe to my channel to be notified when new videos about Swift Evolution are released.

Generic constraints

To understand the changes introduced by Swift 5.3 we first need to understand the power of the where clause. This keyword is mostly used to apply constraints to generic types. So let's start by defining one of such types.

struct Wrapper<T> {
    let value: T
}

Wrapper is a simple type that wraps a value of any type. The wrapper itself is generic over the type of the wrapped value.

In Swift you can add methods to a struct without any problem:

struct Wrapper<T> {
    let value: T
  
    func someFunc() { ... }
}

This method is gonna be available to all instances of that type, doesn't matter which specific type we use. But more often than not, the ability of having a generic type opens the doors to offer functions only when the wrapped value is of a specific type.

For example, lets suppose we wanted to have a function that requires Equatable conformance. We can add this functionality to our type only when the wrapped value is equatable.

extension Wrapper where T: Equatable {
    func equatable(other: T)  {
        // value == other
    }
}

In the body of that function you're free to use == because the type system knows that self.value and other are equatable, because T is constrained to conform to that protocol.

That's what where T: Equatable does.

Of course we can do the same with another protocol.

extension Wrapper where T: Comparable {
    func compare(other: T)  {
        // value < other
    }
}

The limitations

One of the limitations of this system is already visible in the previous snippets. The where clause can't be applied to the function definition itself. We need to create an extension that contains our function and apply the constraint to that extensions. This is not a major limitation in most cases, but conceptually it would be very nice if you could express exactly what you want.

extension Wrapper  {
    func equatable(other: Wrapper) where T: Equatable {
        // value == other
    }

    func compare(other: Wrapper) where T: Comparable {
        // value < other
    }
}

The above snippet is equivalent to the previous ones. The difference is that we moved the generic constraints to the function definitions themselves. So we can have all the functions in one extensions with their requirements specified individually.

Another advantage of this is that you can still add the constraints to the extension itself, which gives a lot of flexibility in terms of reusing them.

For example we could add a third constraint to the above system just by adding it to the extension:

extension Wrapper where T: Hashable  {
  ...

Now this constraint applies to the entire extensions and is an added requirement to the individual constraints specified on each function.

Conclusion

As you can see Swift's type system is quite powerful and with this addition on Swift 5.3 it becomes even more flexible. This seminally small addition lets us express our type requirements in the most appropriate way for our use case.

You can watch the video to see more examples of how this can be used and how it impacts the usage side of your types.