Protocol Associated Type Constraints (where clause and <>)

This pitch floats the idea of giving protocols where clauses and angle brackets, in a similar fashion to how generics use them.

Motivation

This idea came up when I was trying to use a protocol type for a function's parameter. Normally, this is how one does it:

func function1(foo: SomeProtocol) {
    // do something here
}

However, if the protocol has associated types, then it can not be used as the parameter's type directly. The solution is to use generics:

func function2<T: SomeProtocol>(foo: T) {
    // do something here
}

And one step further to add constraints to the protocol's associated types:

func function3<T: SomeProtocol>(foo: T) 
    where T.Type1 == SomeType, T.Type2 = AnotherType {
    // do something here
}

The above works, but with a few problems:

  1. foo is of SomeProtocol type in function1, but not in function2 and function3.
  2. If function3 doesn't need to know the "underlying" type of foo, then it's a somewhat long-winded way of expressing a simple idea. This also renders the generic T practically unused.
  3. If function2 and function3 don't need to know the "underlying" type of foo, then by using generic, they break the visual consistency from function1, and make code readers think unnecessarily in terms of generics instead of protocols.

Proposed Solution

1. Give protocols where clauses.

Where clauses are applied directly after protocols to constrain their associated types.

// similar to function3, but doesn't expose the parameter's underlying type
func function4(foo: SomeProtocol where Type1 == SomeType && Type2 == AnotherType) {
    // ...
}

function4 can be synthesized by the compiler as function5:

func function5<T: SomeProtocol>(foo: T) 
    where T.Type1 == SomeType, T.Type2 = AnotherType {
    foo = foo as! SomeProtocol
    // ...
}

2. Give protocols angle brackets.

Angle brackets can be used when there is only 1 associated type in a protocol.

func function6(foo: SomeProtocol<SomeType>) {
    // ...
}

function6 can be synthesized by the compiler as function7:

func function7<T: SomeProtocol>(foo: T) where T.Type == SomeType {
    foo = foo as! SomeProtocol
    // ...
}

An Example

// print any sequence of integers
// could be a set or dictionary keys
// or any type that conforms to Sequence and has Int as associated type
func printIntegers(in integerSequence: Sequence<Int>) {
    for integer in integerSequence {
        print(integer)
    }
}

A Side Note

There is a syntactical difference between a protocol where clause and a generic where clause. A protocol where clause uses && instead of , between constraints. I will make a separate pitch to suggest allowing logic operators such as &&, ||, and ! in where clauses.

EDIT

found a related discussion

You might find this to be an interesting read: Improving the UI of generics

Specifically the parts about the difference between existentials and generics, and about how we could improve the notation.

1 Like