[swift-evolution] [Draft] Allow multiple conformances to the same protocol

Thorsten Seitz tseitz42 at icloud.com
Sun Jun 12 09:15:11 CDT 2016


Looks good to me!

-Thorsten



> Am 12.06.2016 um 15:01 schrieb Антон Жилин via swift-evolution <swift-evolution at swift.org>:
> 
> I've prepared a proper draft:
> 
> https://github.com/Anton3/swift-evolution/blob/generic-protocols/proposals/NNNN-generic-protocols.md <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 <brent at architechies.com <mailto:brent at architechies.com>>:
> > FWIW they're marked as 'unlikely' here: https://github.com/apple/swift/blob/master/docs/GenericsManifesto.md#generic-protocols <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
> swift-evolution at swift.org
> https://lists.swift.org/mailman/listinfo/swift-evolution

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


More information about the swift-evolution mailing list