I've prepared a proper draft: https://github.com/Anton3/swift-evolution/blob/generic-protocols/proposals/NNNN-generic-protocols.md
- Anton 2016-06-10 17:18 GMT+03:00 Brent Royal-Gordon <[email protected]>: > > 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
