> FWIW they're marked as 'unlikely' here: 
> https://github.com/apple/swift/blob/master/docs/GenericsManifesto.md#generic-protocols
> 
> It would probably be useful to have counterarguments against the points 
> raised in that document if you want to prepare a proposal.

Here's my counterargument.

        * * *

Firstly, I think they're underestimating the feature's utility. Generic 
protocols (real generic protocols, not Sequence<Element>) are already needed to 
make several existing or likely future features work better. For instance:

* Pattern matching

Currently, if you want to customize your type's behavior in a `switch` 
statement, you do it in an ad hoc, almost Objective-C-like way: You define a 
free `~=` operator and the compiler resolves the overloads to magically find 
and use it. There is no way to constrain a generic parameter to "only types 
that can pattern match against type X", which seems like a pretty useful thing 
to offer. For instance, in the past people have suggested some sort of 
expression-based switch alternative. The lack of a pattern matching protocol 
makes this impossible to implement in either the standard library or your own 
code.

If we had generic protocols, we could define a protocol for this matching 
operator and fix the issue:

        protocol Matchable<MatchingValue> {
                func ~= (pattern: Self, value: MatchingValue) -> Bool
        }
        
        protocol Equatable: Matchable<Self> {
                func == (lhs: Self, rhs: Self) -> Bool
        }
        func ~= <T: Equatable>(lhs: T, rhs: T) -> Bool {
                return lhs == rhs
        }
        
        extension Range: Equatable, Matchable<Bound> {}
        func ~= <Bound: Comparable>(pattern: Range<Bound>, value: Bound) -> 
Bool {
                return pattern.lowerBound <= value && value < pattern.upperBound
        }

Then you could write, for instance, a PatternDictionary which took patterns 
instead of keys and, when subscripted, matched the key against each pattern 
until it found a matching one, then returned the corresponding value.

* String interpolation

Currently, StringInterpolationConvertible only offers an 
`init<T>(stringInterpolationSegment: T)` initializer. That means you absolutely 
*must* permit any type to be interpolated into your type's string literals. 
This blocks certain important use cases, like a `LocalizedString` type which 
requires all strings it interacts with to pass through a localization API, from 
being statically checked. It also would normally require any type-specific 
behavior to be performed through runtime tests, but just as in `~=`, the Swift 
compiler applies compile-time magic to escape this restriction—you can write an 
`init(stringInterpolationSegment:)` with a concrete type, and that will be 
preferred over the generic one.

In theory, it should be possible in current Swift to redefine 
StringInterpolationConvertible to allow you to restrict the interpolatable 
values by doing something like this:

        protocol StringInterpolationConvertible {
                associatedtype Interpolatable = Any
                init(stringInterpolation: Self...)
                init(stringInterpolationSegment expr: Interpolatable)
        }

(This is no longer generic because I believe Interpolatable would have to be 
somehow constrained to only protocol types to make that work. But you get the 
idea.)

However, in many uses, developers will want to support interpolation of many 
custom types which do not share a common supertype. For instance, 
LocalizedString might want to support interpolation of any LocalizedString, 
Date, Integer, or FloatingPoint number. However, since Integer and 
FloatingPoint are protocols, you cannot use an extension to make them 
retroactively conform to a common protocol with LocalizedString. 

With generic protocols, we could define StringInterpolationConvertible like 
this:

        protocol StringInterpolationConvertible<Interpolatable> {
                init(stringInterpolation: Self...)
                init(stringInterpolationSegment expr: Interpolatable)
        }

And then say:

        extension LocalizedString: 
StringInterpolationConvertible<LocalizedString>, 
StringInterpolationConvertible<Integer>, 
StringInterpolationConvertible<FloatingPoint> {
                init(stringInterpolationSegment expr: LocalizedString) {
                        self.init()
                        self.components = expr.components
                }
                init(stringInterpolationSegment expr: Integer) {
                        self.init()
                        self.components.append(.integer(expr))
                }
                init(stringInterpolationSegment expr: FloatingPoint) {
                        self.components.append(.floatingPoint(expr))
                }
                init(stringInterpolation strings: LocalizedString...) {
                        self.init()
                        self.components = strings.map { $0.components 
}.reduce([], combine: +)
                }
        }

This example shows an interesting wrinkle: A generic protocol may have 
requirements which don't use any of the generic types, so that each of the 
multiple conformances will require members with identical signatures. When this 
happens, Swift must only allow the member to be implemented once, with that 
implementation being shared among all conformances.

* Subtype-supertype relationships

Though not currently implemented, there are long-term plans to permit at least 
value types to form subtype-supertype relationships with each other. A protocol 
would be a sensible way to express this behavior:

        protocol Upcastable {
                associatedtype Supertype
                
                init?(attemptingCastFrom value: Supertype)
                func casting() -> Supertype
        }

However, this would require a type to have only one supertype, which isn't 
necessarily appropriate. For instance, we might want a UInt8 to be a subtype of 
both Int16 and UInt16. For that to work, Upcastable would have to be generic:

        protocol Upcastable<Supertype> {
                init?(attemptingCastFrom value: Supertype)
                func casting() -> Supertype
        }
        
        extension UInt8: Upcastable<Int16>, Upcastable<UInt16> { … }

Without generic protocols, the only way to offer sufficiently flexible 
subtyping is to offer it as a one-off, ad-hoc feature with special syntax.

        * * *

Secondly, I think the concerns about people trying to use Sequence as a generic 
protocol aren't that big a deal. To put it simply: Sequence is *not* a generic 
protocol. The Swift team controls the definition of Sequence, and we define it 
to not be generic. If people complain, we explain that generic protocols don't 
actually do the right thing for this and that they should use existentials 
instead. We put it in a FAQ. It's just not that big a deal.

The real concern is not that people will try to use Sequence as a generic 
protocol, but that they will try to inappropriately make their own protocols 
generic. I see this as a more minor issue, but if we're worried about it, we 
can address it by changing the mental model to one which doesn't make it look 
like a generics feature.

Basically, rather than thinking of this feature as "generic protocols", it 
could instead be thought of as "associated type overloading": a particular 
associated type can be overloaded, and you can use a `where` clause to select a 
particular overload. This would have a different syntax but handle the same use 
cases.

For instance, rather than saying this:

        protocol Matchable<MatchingValue> {
                func ~= (pattern: Self, value: MatchingValue) -> Bool
        }
        
        protocol Equatable: Matchable<Self> {
                func == (lhs: Self, rhs: Self) -> Bool
        }
        func ~= <T: Equatable>(lhs: T, rhs: T) -> Bool {
                return lhs == rhs
        }
        
        extension Range: Equatable, Matchable<Bound> {}
        func ~= <Bound: Comparable>(pattern: Range<Bound>, value: Bound) -> 
Bool {
                return pattern.lowerBound <= value && value < pattern.upperBound
        }
        
        struct PatternDictionary<Matching, Value>: DictionaryLiteralConvertible 
{
                typealias Key = Matchable<Matching>
                typealias Value = OutValue
                
                var patterns: DictionaryLiteral<Key, Value>
                init(dictionaryLiteral pairs: (Key, Value)...) { patterns = 
DictionaryLiteral(pairs) }
                
                subscript(matchingValue: Matching) -> Value? {
                        for (pattern, value) in patterns {
                                if pattern ~= matchingValue {
                                        return value
                                }
                        }
                        return nil
                }
        }

You could instead say:

        protocol Matchable {
                @overloadable associatedtype MatchingValue
                func ~= (pattern: Self, value: MatchingValue) -> Bool
        }
        
        protocol Equatable: Matchable where MatchingValue |= Self {
                func == (lhs: Self, rhs: Self) -> Bool
        }
        func ~= <T: Equatable>(lhs: T, rhs: T) -> Bool {
                return lhs == rhs
        }
        
        extension Range: Equatable, Matchable {
                typealias MatchingValue |= Bound
        }
        func ~= <Bound: Comparable>(pattern: Range<Bound>, value: Bound) -> 
Bool {
                return pattern.lowerBound <= value && value < pattern.upperBound
        }

        struct PatternDictionary<Matching, Value>: DictionaryLiteralConvertible 
{
                typealias Key = Any<Matchable where .MatchingValue & Matching>
                typealias Value = Value
                
                var patterns: DictionaryLiteral<Key, Value>
                init(dictionaryLiteral pairs: (Key, Value)...) { patterns = 
DictionaryLiteral(pairs) }
                
                subscript(matchingValue: Matching) -> Value? {
                        for (pattern, value) in patterns {
                                if pattern ~= matchingValue {
                                        return value
                                }
                        }
                        return nil
                }
        }

(Is `MatchingValue |= Bound` a union type feature? I'm not sure. It does have 
the syntax of one, but there's a separate overload for each type, so I don't 
think it really acts like one.)

This is very nearly the same feature, but presented with different 
syntax—effectively with a different metaphor. That should prevent it from being 
abused the way the core team fears it will be.

(One difference is that this version permits "vacuous" conformances: in theory, 
there's no reason you couldn't conform to a protocol with an `@overloadable 
associatedtype` and define zero types. On the other hand, that's not 
necessarily *wrong*, and might even be useful in some cases.)

-- 
Brent Royal-Gordon
Architechies

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

Reply via email to