Constraints

For those of you looking for help on autolayout, this is not the place. Constraints, the constraints defined in Swift Standard Library, are an advanced topic that builds on other advanced topics, so beware.

Everything in this article center around the where keyword. The where keyword allows us to make new components conditional or add extra conditions onto switches!

Type Constraints

When working with parametric polymorphism it makes sense to add functionality in certain cases.

For the following examples, we will use this implementation of a binary tree:

enum BinaryTree<T> {
    case leaf
    indirect case node(left: BinaryTree, value: T, right: BinaryTree)
}

Let's say we want to add a function to tell if our binary tree contains a value. To do this, we need == to be implemented. Thus, we make a function that will exist if and only if T conforms to Equatable:

extension BinaryTree where T: Equatable {
    
    func contains(_ element: T) -> Bool {
        switch self {
            case .leaf: return false
            case let .node(left, value, right):
                if value == element {
                    return true
                }
                return left.contains(element) || right.contains(element)
        }
    }
    
}

For those of you acquainted with binary trees, you may look at this and want to throw up–sorry! This is quite inefficient. The point of a binary tree is that we can reduce runtime by organizing it. And to be fair, that fact kind of nullifies this whole conditional function business, but we can think of this as more of an exercise than an actual use case.

But, I digress. Let's fix our inefficiency. There is another protocol we can use Comparable. Comparable guarantees ==, <, and >. With these, it also guarantees <= and >=. Perfect! Let's implement it.

extension BinaryTree where T: Comparable {
    func contains(_ element: T) -> Bool {
        switch self {
            case .leaf: return false
            case let .node(left, value, right):
                if value == element {
                    return true
                } else if element < value {
                    return left.contains(element)
                } else {
                    return right.contains(element)
                }
        }
    }
}

You may not like having two extensions because it just gets kind of messy, but do not fear, we can combine these by putting the constraints on the functions instead of the extensions:

extension BinaryTree {

    func contains(_ element: T) -> Bool where T: Equatable {
        switch self {
            case .leaf: return false
            case let .node(left, value, right):
                if value == element {
                    return true
                }
                return left.contains(element) || right.contains(element)
        }
    }
    
    func contains(_ element: T) -> Bool where T: Comparable {
        switch self {
            case .leaf: return false
            case let .node(left, value, right):
                if value == element {
                    return true
                } else if element < value {
                    return left.contains(element)
                } else {
                    return right.contains(element)
                }
        }
    }

}

Switch Constraints

Some of you may still be grossed out. I promised you that switch statements were this wonderful feature that would figure out all of the cases necessary to process something, but here I need extra if statements 😤! And you'd be right, except for that last part... We can cover these cases as well.

We can add conditionality to our cases with the where keyword.

func contains(_ element: T) -> Bool {
    switch self {
        case .leaf: return false
        case let .node(_, value, _) where value == element: return true
        case let .node(left, value, _) where value < element: return left.contains(element)
        case let .node(_, _, right): return right.contains(element)
    }
}

Note that since the last case will always be triggered for nodes, it must be last. I wanted to add a where clause to fix that, but Swift would not recognize that as covering all of the cases. This was the cleanest solution we found. Credits to Hanzheng Li '23 for fixing that one.

Last updated