Jordan,

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

_______________________________________________
swift-evolution mailing list
[email protected]
https://lists.swift.org/mailman/listinfo/swift-evolution

Reply via email to