[swift-evolution] [Proposal] Explicit Synthetic Behaviour
Xiaodi Wu
xiaodi.wu at gmail.com
Tue Sep 12 23:12:41 CDT 2017
On Tue, Sep 12, 2017 at 22:07 Tony Allevato <tony.allevato at gmail.com> wrote:
> On Tue, Sep 12, 2017 at 7:10 PM Xiaodi Wu <xiaodi.wu at gmail.com> wrote:
>
>> On Tue, Sep 12, 2017 at 9:58 AM, Thorsten Seitz via swift-evolution <
>> swift-evolution at swift.org> wrote:
>>
>>> Good arguments, Tony, you have convinced me on all points. Transient is
>>> the way to go. Thank you for your patience!
>>>
>>
>> On many points, I agree with Tony, but I disagree that "transient"
>> addresses the issue at hand. The challenge being made is that, as Gwendal
>> puts it, it's _unwise_ to have a default implementation, because people
>> might forget that there is a default implementation. "Transient" only works
>> if you remember that there is a default implementation, and in that case,
>> we already have a clear syntax for overriding the default.
>>
>
> Right—I hope it hasn't sounded like I'm conflating the two concepts
> completely. The reason I brought up "transient" is because nearly all of
> the "risky" examples being cited so far have been of the variety "I have a
> type where some properties happen to be Equatable but shouldn't be involved
> in equality", so my intention has been to show that if we have a better
> solution to that specific problem (which is, related to but not the same as
> the question at hand), then there aren't enough risky cases left to warrant
> adding this level of complexity to the protocol system.
>
>
>>
>> As others point out, there's a temptation here to write things like
>> "transient(Equatable)" so as to control the synthesis of implementations on
>> a per-protocol basis. By that point, you've invented a whole new syntax for
>> implementing protocol requirements. (Ah, you might say, but it's hard to
>> write a good hashValue implementation: sure, but that's adequately solved
>> by a library-supplied combineHashes() function.)
>>
>
> I totally agree with this. A design that would try to annotate "transient"
> with a protocol or list of protocols is missing the point of the semantics
> that "transient" is supposed to provide. It's not a series of switches to
> that can be flipped on and off for arbitrary protocols—it's a semantic tag
> that assigns additional meaning to properties and certain protocols (such
> as Equatable, Hashable, and Codable, but possibly others that haven't been
> designed yet) would have protocol-specific behavior for those properties.
>
> To better explain what I've been poking at, I'm kind of extrapolating this
> out to a possible future where it may be possible to more generally (1)
> define custom @attributes in Swift, like Java annotations, and then (2) use
> some metaprogramming constructs to generate introspective default
> implementations for a protocol at compile-time just as the compiler does
> "magically" now, and the generator would be able to query attributes that
> are defined by the same library author as the protocol and handle them
> accordingly.
>
> In a world where that's possible, I think it's less helpful to think in
> terms of "I need to distinguish between conforming to X and getting a
> synthesized implementation and conforming to X and avoiding the synthesized
> implementation because the default might be risky", but instead to think in
> terms of "How can I provide enough semantic information about my types to
> remove the risk?"
>
> In other words, the switches we offer developers to flip shouldn't be
> about turning on/off entire features, but about giving the compiler enough
> information to make it smart enough that we never need to turn it off in
> the first place. As I alluded to before, if I have 10 properties in a type
> and only 1 of those needs to be ignored in ==/hashValue/whatever, writing
> "Equatable" instead of "derives Equatable" isn't all that helpful. Yes, it
> spits out an error message where there wouldn't have been one, but it
> doesn't reduce any of the burden of having to provide the appropriate
> manual implementation.
>
> But all that stuff about custom attributes and metaprogramming
> introspection is a big topic of it's own that isn't going to be solved in
> Swift 5, so this is a bit of a digression. :)
>
That said, we could have enums EquatingKeys and HashingKeys, a la
CodingKeys... That may not be a huge leap to propose and implement.
>
>>
>>
>>> -Thorsten
>>>
>>> Am 12.09.2017 um 16:38 schrieb Tony Allevato via swift-evolution <
>>> swift-evolution at swift.org>:
>>>
>>>
>>>
>>> On Mon, Sep 11, 2017 at 10:05 PM Gwendal Roué <gwendal.roue at gmail.com>
>>> 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
>>> swift-evolution at swift.org
>>> https://lists.swift.org/mailman/listinfo/swift-evolution
>>>
>>>
>>> _______________________________________________
>>> 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/20170913/f0004c53/attachment.html>
More information about the swift-evolution
mailing list