On 12.09.2017 17:38, Tony Allevato wrote:
On Mon, Sep 11, 2017 at 10:05 PM Gwendal Roué <[email protected]
<mailto:[email protected]>> wrote:
This doesn't align with how Swift views the role of protocols, though.
One
of the criteria that the core team has said they look for in a protocol
is
"what generic algorithms would be written using this protocol?"
AutoSynthesize doesn't satisfy that—there are no generic algorithms that
you would write with AutoEquatable that differ from what you would write
with Equatable.
And so everybody has to swallow implicit and non-avoidable code
synthesis
and shut up?
That's not what I said. I simply pointed out one of the barriers to getting
a
new protocol added to the language.
Code synthesis is explicitly opt-in and quite avoidable—you either don't
conform to the protocol, or you conform to the protocol and provide your own
implementation. What folks are differing on is whether there should have to
be
*two* explicit switches that you flip instead of one.
No. One does not add a protocol conformance by whim. One adds a protocol
conformance by need. So the conformance to the protocol is a *given* in our
analysis of the consequence of code synthesis. You can not say "just don't adopt
it".
As soon as I type the protocol name, I get synthesis. That's the reason why
the
synthesized code is implicit. The synthesis is explicitly written in the
protocol
documentation, if you want. But not in the programmer's code.
I did use "non-avoidable" badly, you're right: one can avoid it, by
providing its
custom implementation.
So the code synthesis out of a mere protocol adoption *is* implicit.
Let's imagine a pie. The whole pie is the set of all Swift types. Some
slice of
that pie is the subset of those types that satisfy the conditions that allow
one of our protocols to be synthesized. Now that slice of pie can be sliced
again, into the subset of types where (1) the synthesized implementation is
correct both in terms of strict value and of business logic, and (2) the
subset
where it is correct in terms of strict value but is not the right business
logic because of something like transient data.
Yes.
What we have to consider is, how large is slice (2) relative to the whole
pie,
*and* what is the likelihood that developers are going to mistakenly
conform to
the protocol without providing their own implementation, *and* is the added
complexity worth protecting against this case?
That's quite a difficult job: do you think you can evaluate this likelihood?
Explicit synthesis has big advantage: it avoids this question entirely.
Remember that the main problem with slide (2) is that developers can not
*learn*
to avoid it.
For each type is slide (2) there is a probability that it comes into
existence
with a forgotten explicit protocol adoption. And this probability will not
go
down as people learn Swift and discover the existence of slide (2). Why?
because
this probability is driven by unavoidable human behaviors:
- developer doesn't see the problem (a programmer mistake)
- the developper plans to add explicit conformance later and happens to
forget
(carelessness)
- a developper extends an existing type with a transient property, and
doesn't
add the explicit protocol conformance that has become required.
Case 2 and 3 bite even experienced developers. And they can't be improved by
learning.
Looks like the problem is better defined as an ergonomics issue, now.
If someone can show me something that points to accidental synthesized
implementations being a significant barrier to smooth development in Swift,
I'm
more than happy to consider that evidence. But right now, this all seems
hypothetical ("I'm worried that...") and what's being proposed is adding
complexity to the language (an entirely new axis of protocol conformance)
that
would (1) solve a problem that may not exist to any great degree, and (2)
does
not address the fact that if that problem does indeed exist, then the same
problem just as likely exists with certain non-synthesized default
implementations.
There is this sample code by Thorsten Seitz with a cached property which is
quite
simple and clear :
https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20170911/039684.html
This is the sample code that had me enter the "worried" camp.'
I really like Thorsten's example, because it actually proves that requiring explicit
derivation is NOT the correct approach here. (Let's set aside the fact that Optionals
prevent synthesis because we don't have conditional conformances yet, and assume that
we've gotten that feature as well for the sake of argument.)
Let's look at two scenarios:
1) Imagine I have a value type with a number of simple Equatable properties. In a
world where synthesis is explicit, I tell that value type to "derive Equatable".
Everything is fine. Later, I decide to add some cache property like in Thorsten's
example, and that property just happens to also be Equatable. After doing so, the
correct thing to do would be to remove the "derive" part and provide my custom
implementation. But if I forget to do that, the synthesized operator still exists and
applies to that type. If you're arguing that "derive Equatable" is better because its
explicitness prevents errors, you must also accept that there are possibly just as
many cases where that explicitness does *not* prevent errors.
2) Imagine I have a value type with 10 Equatable properties and one caching property
that also happens to be Equatable. The solution being proposed here says that I'm
better off with explicit synthesis because if I conform that type to Equatable
without "derive", I get an error, and then I can provide my own custom
implementation. But I have to provide that custom implementation *anyway* to ignore
the caching property even if we don't make synthesis explicit. Making it explicit
hasn't saved me any work—it's only given me a compiler error for a problem that I
already knew I needed to resolve. If we tack on Hashable and Codable to that type,
then I still have to write a significant amount of boilerplate for those custom
operations. Furthermore, if synthesis is explicit, I have *more* work because I have
to declare it explicitly even for types where the problem above does not occur.
So, making derivation explicit is simply a non-useful dodge that doesn't solve the
underlying problem, which is this: Swift's type system currently does not distinguish
between Equatable properties that *do* contribute to the "value" of their containing
instance vs. Equatable properties that *do not* contribute to the "value" of their
containing instance. It's the difference between behavior based on a type and
additional business logic implemented on top of those types.
So, what I'm trying to encourage people to see is this: saying "there are some cases
where synthesis is risky because it's incompatible with certain semantics, so let's
make it explicit everywhere" is trying to fix the wrong problem. What we should be
looking at is *"how do we give Swift the additional semantic information it needs to
make the appropriate decision about what to synthesize?"*
That's where concepts like "transient" come in. If I have an
Equatable/Hashable/Codable type with 10 properties and one cache property, I *still*
want the synthesis for those first 10 properties. I don't want the presence of *one*
property to force me to write all of that boilerplate myself. I just want to tell the
compiler which properties to ignore.
Imagine you're a stranger reading the code to such a type for the first time. Which
would be easier for you to quickly understand? The version with custom
implementations of ==, hashValue, init(from:), and encode(to:) all covering 10 or
more properties that you have to read through to figure out what's being ignored (and
make sure that the author has done so correctly), or the version that conforms to
those protocols, does not contain a custom implementation, and has each transient
property clearly marked? The latter is more concise and "transient" carries semantic
weight that gets buried in a handwritten implementation.
I prefer version "that conforms to those protocols, does not contain a custom
implementation, and has each transient property clearly marked" AND with explicit
marker saying "requirements for this and this protocol will be synthesized" :-)
I see your points that such marker can't prevent us from making mistakes in some
situation, but such marker will help to pay our attention to this.
For me, the main job of such marker is to clearly say to even me(when I'll review my
code after a couple of months), for some type with a *number* of methods in it, that
requirements for such an important protocols(like Equatable/Hashable/Codable) is
auto-synthesized and I should be careful with this. Or when I'm looking at code of
some problematic type and trying to find out what could be wrong with it.
As for 'transient' properties, IMO it's orthogonal question. We definitely need a way
to exclude properties from participating in auto-generated code, and I think we can
have a situation when we need property to participate in one protocol, but don't for
others. Something like '@transient(for:Equatable,Hashable)' and
'@transient(for:Codable)'. But I suppose this is a subject for future discussions.
Vladimir.
Here's a fun exercise—you can actually write something like "transient" without any
additional language support today:
https://gist.github.com/allevato/e1aab2b7b2ced72431c3cf4de71d306d. A big drawback to
this Transient type is that it's not as easy to use as an Optional because of the
additional sugar that Swift provides for the latter, but one could expand it with
some helper properties and methods to sugar it up the best that the language will
allow today.
I would wager that this concept, either as a wrapper type or as a built-in property
attribute, would solve a significant majority of cases where synthesis is viewed to
be "risky". If we accept that premise, then we can back to our slice of pie and all
we're left with in terms of "risky" types are "types that contain properties that
conform to a certain protocol but are not really transient but also shouldn't be
included verbatim in synthesized operations". I'm struggling to imagine a type that
fits that description, so if they do exist, it's doubtful that they're a common
enough problem to warrant introducing more complexity into the protocol conformance
system.
Gwendal
_______________________________________________
swift-evolution mailing list
[email protected]
https://lists.swift.org/mailman/listinfo/swift-evolution