[swift-evolution] [Pitch] Moving where Clauses Out Of Parameter Lists

Pyry Jahkola pyry.jahkola at iki.fi
Thu Apr 7 03:44:50 CDT 2016


Your comments brought up a few more closely related ideas that have been bubbling under.

To everyone,

Sorry for going beyond topic here. The discussion of the further proposals below should be taken into their own threads if there's interest. I'm just trying to motivate what else moving out the `where` clause would make possible.

1. My counter-proposal against making generic parameters public

> Another factor here is that we've been planning for a while for generic parameters to types to be exposed as implicit member typealiases, since they already conflict with the namespace for member typealiases. Therefore it's important to name generic parameters to types well, but less important to do so for generic parameters for functions. (They're also much less likely to be ad hoc for types; there has to be something that describes the relation between the Self type and the parameter, while the function might not have anything more interesting than "operand".)

I disagree with that. I think it's more natural to restrict generic type parameters to the immediate local scope of the class/struct/enum definition or extension, and simply allow making the type public with `typealias`. For example, this would make `Element` a public member of `Array`:

    public enum Result<T, Error> {
        public typealias Value = T // This makes `Value` public for extensions and everyone

I would even allow publishing the otherwise local name by repeating it in `typealias` like this:

    public struct Array<Element> { // This `Element` is only available in (the missing) where clause and the scope below.
        public typealias Element = Element // This line makes `Element` available everywhere, see below.

    extension Array<T> { // <- Regardless of the pattern (`T`) used here,
        var mid: Element? { // the type `Element = T` is available here
            return ...      // because it was made public elsewhere.
    extension Array<Optional<T>> { // <- An example of pattern matching (see further below).
        // Ditto, `Element = T?`

    typealias Ints = [Int]
    let x: Ints.Element = ... // Int

Next, I propose how to extend this syntax with pattern matching. The above thinking is a natural progression from the use of pattern matching for introducing generic type parameters in type `extension`s.

2. Proposal to enable pattern matching of generic types in generic parameters

> This is also a minor point against declaring generic parameters for extensions, since they would have to match up. We also do need a syntax some day to represent new parameters:
> // For expository purposes only:
> extension <T> Array where Element == Optional<T>
> But that deserves its own proposal. I just don't want us to paint ourselves into a corner.

I agree that we need that feature. But instead of your proposed syntax, I'd take influence from the already existing pattern matching that we have at value level, such that:

1. any generic type expressions within the angle brackets `<...>` are taken as patterns to match against  (e.g. `Array<Element>`, `Optional<Foo>`, `Dictionary<S, S>`), and
2. all non-generic type identifiers in those expressions are taken as generic parameters and not concrete types (`Element`, `Foo`, or `S`, respectively).

Your example would translate into the first of these two:

    extension Array<Optional<T>> { // extending [T?] doesn't even need any `where` clause
        func unwrapAll() -> [T] { ... }

    extension Dictionary<K, Dictionary<K, V>> { // This pattern match requires the same `K` twice.
        subscript(x: K, y: K) -> V { ... }

The generic parameters would shadow any existing names in the outer scope, i.e. `extension Array<String>` would mean the same as `extension Array<Element>`. But it would be a good idea to add a warning (or linter error) at least when stdlib types such as `Swift.String` are shadowed like that. I think the benefits from pattern matching outweigh the possible confusion, especially since you can't use any members of `String` if you mistakenly wrote `extension Array<String> { ... }` instead of `extension Array<T> where T == String`.

With this syntax, we could also allow extending Array, Dictionary, and Optional in their more natural notation:

    extension [T?] {
        func unwrapAll() -> [T] { /* ... */ }

    extension [K: [K: V]] {
        subscript(x: K, y: K) -> V { /* ... */ }

Here are a few more (somewhat realistically needed) examples with further `where` clauses:

    extension [T] where T : Hashable { /* ... */ }
    extension T? where T : Comparable { /* ... */ }
    extension [K: V] where V : Equatable { /* ... */ }
    extension [T] where T == String { /* ... */ }

I think pattern matching is a very expressive, intuitive, and readable technique that works quite well for this purpose, better than what we currently have.

3. Future directions

Brent already pointed out that the `where` constraint syntax could be used for dependent types (i.e. type-level values as generic parameters). Four more possible directions come to my mind:

(1) Adding conditional protocol conformances:

    extension [T]: Equatable where T : Equatable { /* ... */ }
    extension [T]: Comparable where T : Comparable { /* ... */ }
    extension [K, V]: Equatable where V : Equatable { /* ... */ }
    extension Foo<X, Bar<Y>> : Bazzable where X : Baz, Y : Baz { /* ... */ }

(2) Extending non-nominal types:

    extension (A, B) { var tail: B { /* ... */ }}
    extension (A, B, C) { var tail: (B, C) { /* ... */ }}

(3) Using variadic patterns (shamefully borrowing the `...` notation from C++ here to mean an arbitrary-length list of patterns to match or expressions to expand):

    extension (T...) : Equatable where T : Equatable... {}

    func == <T...>(lhs: (T...), rhs: (T...)) -> Bool where T : Equatable... { /* ... */ }

(4) Since all type constraints with `:` fly out to of the generic parameter list into the `where` clause, we could enable the use of colons to give labels to generic parameters (and thus even make some of them have default values):

    struct Parser<encoding: Encoding, input: Input>
        where Encoding : EncodingProtocol /*, ... */
        // ...

    let parser = Parser<encoding: UTF8, input: String>()

— Pyry

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20160407/dd845200/attachment.html>

More information about the swift-evolution mailing list