+1 Sent from my iPhone
> On Jun 12, 2016, at 08:01, Антон Жилин via swift-evolution > <[email protected]> wrote: > > 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
_______________________________________________ swift-evolution mailing list [email protected] https://lists.swift.org/mailman/listinfo/swift-evolution
