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

Xiaodi Wu xiaodi.wu at gmail.com
Sun Jun 12 13:16:47 CDT 2016


On Sun, Jun 12, 2016 at 8:01 AM, Антон Жилин <swift-evolution at swift.org>
wrote:

> I've prepared a proper draft:
>
>
> https://github.com/Anton3/swift-evolution/blob/generic-protocols/proposals/NNNN-generic-protocols.md
>
>
When you propose this:
Syntax in protocol extensions

protocol MyComparable<T> {
  func < (left: Self, right: T)
}extension MyComparable {
  func > (left: T, right: Self) {
    return right < left
  }
}


Would it be possible for me to write something like:

```
extension MyComparable<T : SignedNumber> { ... }
```


- Anton
>
> 2016-06-10 17:18 GMT+03:00 Brent Royal-Gordon <brent at architechies.com>:
>
>> > 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
> 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/cb474c31/attachment.html>


More information about the swift-evolution mailing list