[swift-evolution] [Draft] Allow multiple conformances to the same protocol
Антон Жилин
antonyzhilin at gmail.com
Sun Jun 12 13:28:26 CDT 2016
Yes, everything that works on generic types should work for generic
protocols. I'll add that.
What won't work is declaring that MyComparable : Comparable iff T ==
Self. The same won't work for current non-generic protocols as well.
Although that feature is highly requested, it is discussed in a separate
proposal.
- Anton
2016-06-12 21:16 GMT+03:00 Xiaodi Wu <xiaodi.wu at gmail.com>:
>
>
> 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/71661230/attachment.html>
More information about the swift-evolution
mailing list