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

Антон Жилин antonyzhilin at gmail.com
Sun Jun 12 08:01:49 CDT 2016


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 <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
>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20160612/19b76cb5/attachment.html>


More information about the swift-evolution mailing list