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.
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] <mailto:[email protected]>
https://lists.swift.org/mailman/listinfo/swift-evolution