[swift-evolution] Keyword for protocol conformance

Xiaodi Wu xiaodi.wu at gmail.com
Fri Aug 26 12:06:04 CDT 2016


On Fri, Aug 26, 2016 at 6:35 AM, Pyry Jahkola <pyry.jahkola at iki.fi> wrote:

> Hi Xiaodi and Charles,
>
> As I said, this discussion has already happened several times. I'm
> literally just repeating what people said eight months ago, six months ago,
> and four months ago. There's not a good answer to this and perhaps several
> other issues, which is why I don't see a way forward for the proposal.
> After all, I was the one proposing the same idea last winter, so I've had a
> few months to think about it.
>
>
> I've been following the discussion from the side both now and before.
> Without being able to cover everything that's been said, I hope this
> message brings at least a bit of new air into the discussion.
>
> — — —
>
> Here's one more approach that, AFAICT, would enable both retroactive
> modelling and fairly good error diagnostics: *bring the full protocol
> conformance together with a dedicated definition block* which can both
> define implementations and refer to implementations defined elsewhere.
>

This is a very well explained proposal. Thanks for taking the time to write
it out. But I don't think it solves the problem, and I'll respond inline.


> *Details:*
>
> 1. Allow marking the conformance to a given protocol with a block of code.
> This could be nested inside a struct/enum/class body or its extension:
>
>     *struct* Vector {
>       *conformance* Hashable { /* TBD, read on… */ }
>     }
>     *extension* Vector {
>       *conformance* CustomStringConvertible { /* … */ }
>     }
>

In general, I think there is an opposition to organizing nested blocks of
members like this (there was strong opposition to proposals for access
level grouping, for instance). But besides that somewhat subjective
opinion, there might also be difficulty in terms of `private` scoping.
These are minor, more speculative points.

Mainly, I think it is confusing because it looks like a nested type and
invites proposals to namespace protocol conforming members (i.e.
`Vector.Hashable.hashValue`), which is not what is going on and definitely
not the direction in which I would want to take this proposal.

or at the top level, as a clearly marked *single-conformance extension*:
>
>     *conformance* Vector : CustomDebugStringConvertible { /* … */ }
>

This is an improvement. But, you are introducing a second keyword,
`conformance` without justification. The same purpose that you outline
later could be served by the existing spelling `extension Vector :
CustomDebugStringConvertible`, and this has been proposed earlier in this
thread and also in previous threads.


> 2. Inside the body of that block, all protocol (non-extension) methods
> *must* be marked with `override`. That includes both the required
> interface and the customisable interface with library-provided default
> implementations:
>
>     *struct* Vector {
>       *conformance* Hashable {
>         *override static func* ==(lhs: *Self*, rhs: *Self*) -> Bool { /*
> … */ }
>         // Ok.
>
>         *var* hashValue: Int { /* … */ }
>         // *error:* property 'hashValue' is required by Hashable.
>         // (Fixit: mark with 'override')
>
>         *override static func* < (lhs: *Self*, rhs: *Self*) -> Bool { /*
> … */ }
>         // *error:* protocol Hashable does not require 'static func <'.
>         // (Fixit #1: remove override. Etc.)
>       }
>     }
>

This is similar to a previous proposal that any member declared in
extensions that add a conformance must, if that member has the same or
greater visibility than the lesser of the type and protocol, implement a
protocol requirement. However, in your proposal, there's a pervasive
requirement for `override`, which adds noise but doesn't provide an
advantage over that alternative proposal. Moreover, I have a problem with
your proposed spelling: if there is no default implementation, why should
an implementation be said to `override` anything?


> 3. When conformance is made explicit as described above, no other part of
> code is allowed to interfere with that type's conformance to the protocol
> under question:
>
>     *conformance* Vector : Collection {
>       *override var *startIndex: Int { *return* 0 }
>       *override **var* endIndex: Int { *return* _count }
>       *override **subscript*(index: Int) -> Double { *return*
> _elements[index] }
>       *override **func* index(after i: Int) -> Int { *return* i + 1 }
>     }
>     *extension* Vector {
>       *var* count: Int { *return* _count }
>       // *error:* property 'count' was introduced by explicit conformance to
> Collection.
>       // (Fixit: Add explicit override to the conformance block.)
>     }
>

I'm not sure what safety is gained by this particular rule. The language
should make the most simple cases the most straightforward. In the scenario
where a struct S, with only a small number of methods, conforms to protocol
P, which has only a small number of requirements and no default
implementations, it is not confusing at all to put all requirements in the
same block of code. Your proposed syntax, if made mandatory [see below
about mandatory vs. optional], would greatly increase the visual clutter
and difficulty of correctly conforming one simple struct to one simple
protocol.

4a. When using a `conformance` block for retroactive modelling, or to
> explicitly include methods, properties etc. defined elsewhere, it is enough
> to list those in the body of the `conformance` block:
>
>     *protocol* CountHaving {
>       *associatedtype* Count : Integer
>       *var* count: Count
>     }
>     *conformance* Array : HasCount {
>       *override var* count: Int // Ok, explicit retroactive modelling.
>     }
>

This might be unworkable for certain types of retroactive modeling. If I
add a new default implementation to a requirement of `Collection`, I would
have to write out conformance statements for *every single Collection type*
in the standard library, probably Foundation and Dispatch also, plus any
other libraries I use, in addition to my own Collection types. The only way
to solve this issue, as far as I can tell, is to say that none of these
keywords are required across module boundaries, which carves out an
exception (which might be justifiable, but it's still an exception to the
rule).


> 4b. Any override declarations without a body within an explicit
> `conformance` block are requirements for the definitions to exist
> elsewhere. That includes overridden properties without getters and setters.
> For example, here's an alternative way of fixing the error in #4:
>
>     *conformance* Vector : Collection {
>       *override var *startIndex: Int { *return* 0 }
>       *override **var* endIndex: Int { *return* _count }
>       *override **subscript*(index: Int) -> Double { *return* _elements[index]
> }
>       *override **func* index(after i: Int) -> Int { *return* i + 1 }
>       *override var* count: Int // No body; defined elsewhere.
>     }
>     *extension* Vector {
>       *var* count: Int { *return* _count } // Ok.
>     }
>

This is clever, but also confusing (IMO) because you've now introduced more
than one way of splitting up code that fulfills protocol requirements, when
your stated goal is to improve the situation by putting all requirements in
the same place.

Although you've insisted on a central location where there's a catalog of
overrides, it might increase rather than decrease confusion about where
these requirements are being implemented, because now there's at least two
declarations. For derived classes that conform to protocols but inherit
their implementation from a base class, there are additional difficulties:
Does `override` mean you're overriding the base class implementation, or
are you using `override` to say that the base class implementation (which
you're not overriding in the derived class) should override the protocol's
default implementation? Also, even though I see a declaration of a protocol
requirement with no body, I might not find the implementation anywhere in
the derived class, because it's in another file, and I may not even have
access to that source code. In general, it's exceedingly weird that the
derived class would have to re-declare an inherited method with the word
`override`, then never override or mention that method again.

5. Just like any other extensions, `conformance` blocks could introduce
> other non-`override` methods etc. to its needs, but they probably should
> keep it at minimum.
>

As mentioned earlier, I think a more preferable and terse rule was proposed
earlier, where extensions that add conformance are not allowed to have
public members (or members equal or more visible than the protocol or type)
that don't implement a requirement.

— — —
>
> *Downsides. *Now, there are obvious reasons not to introduce this
> complexity to the language:
>
> − It adds quite a bit of boilerplate to the conformance definition. (OTOH,
> programmers in a hurry would still be able to use the existing implicit
> conformance syntax, thereby avoiding the boilerplate on the one hand, and
> the better diagnostics on the other.)
>

It does add quite a bit of boilerplate; the idea of having this boilerplate
has been pitched before, though not with the `conformance` keyword. It's
not great, but it might not in and of itself be a deal-breaker.

However, the key weakness here IMO is that you are trying to address the
weaknesses above by saying that this whole new syntax is _optional_. This
is, IMO, a critical flaw. I could _maybe_ get behind a single opt-in
keyword or attribute; but I think a whole new alternative syntax for
something as fundamental as conformance is a non-starter. This is a very,
very essential part of Swift; there should be *one* syntax for protocol
conformance, not two. We should find the one best syntax, though obviously
we are now constrained by backwards compatibility issues.


> − It would make it harder to add new required methods with default
> implementations without breaking code elsewhere. Because explicitly
> conforming types which already happened to have a matching function,
> property, or subscript would need to include it in the explicit conformance
> block. (OTOH, maybe that's what they indeed asked for by making the
> conformance explicit!)
>
> − It would add yet another keyword to the language, context-specific but
> regardless. And the amount of `override` noise isn't looking very pretty.
>

Yes, these two weaknesses are definitely issues. In general, from the
perspective of library evolution, there's a key question to be answered: if
the library vendor adds a new default implementation to a protocol, should
they need to be concerned that it could cause existing code using a
previous version of the library to stop compiling?

Currently, the answer is no, and I think it is the right answer most (if
not all) of the the time. I have this opinion because types that conform to
the protocol using a previous version of the library cannot possibly change
their behavior if a new default implementation is supplied, since these
types necessarily have their own implementation of the same requirement and
since protocol requirements are dynamically dispatched and there's no way
to break that encapsulation. Therefore, it is unnecessarily disruptive to
stop compiling that code in order to force the end user to acknowledge a
change that cannot have any effect on their own code. I conclude that any
keyword should be _optional_ (if you _want_ your code to break
unnecessarily, I guess that can be up to you...), but then see above about
my reasoning regarding an optional syntax for protocol conformance.

− It possibly complicates some other convenient use of protocols that I
> can't think of right now. Discuss.
>
> *On the plus sides,*
>
> + There are precedents of explicit conformances in other languages: Haskell's
> type classes
> <https://en.wikibooks.org/wiki/Haskell/Classes_and_types#Classes_and_instances>
>  and Clojure's protocols
> <http://clojure.github.io/clojure/clojure.core-api.html#clojure.core/extend> come
> to mind as most similar.
>
> + It better documents how exactly the type conforms to the protocol,
> without leaving any missing "magic" elsewhere in the codebase. And it would
> become a compiler-enforced way to the already existing practice of keeping
> a conformance interface together within an extension block.
>
> *Alternatives to consider:*
>
> A. Instead of nesting `conformance ProtocolName`, we could say
> `conformance Self : ProtocolName`.
>
> B. Instead of introducing a new keyword `conformance` for top-level
> single-conformance extensions, we could state that
> `private/internal/public` in front of the `extension` keyword is used to
> define an explicit conformance of one protocol:
>
> *    internal extension* Vector : Collection { /* just Collection things
> */ }
>
> — Pyry
>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20160826/8dc75da2/attachment.html>


More information about the swift-evolution mailing list