[swift-evolution] [Proposal] Explicit Synthetic Behaviour

Vladimir.S svabox at gmail.com
Wed Sep 13 10:40:46 CDT 2017


On 13.09.2017 7:14, Xiaodi Wu via swift-evolution wrote:
> 
> On Tue, Sep 12, 2017 at 22:07 Tony Allevato <tony.allevato at gmail.com 
> <mailto:tony.allevato at gmail.com>> wrote:
> 
>     On Tue, Sep 12, 2017 at 7:10 PM Xiaodi Wu <xiaodi.wu at gmail.com
>     <mailto: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 <mailto: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.

Actually, not taking into account a question of explicit marker for auto-generated 
methods, this is IMO a great point.

Codable, which can auto-generate methods, *had* these CodingKeys from the moment of 
birth. Currently, we have a proposal for auto-generating of methods for 
Equatable/Hashable. Why we don't have a EquatingKeys/HashingKeys option for them in 
symmetry with Codable? Why Codable already has a method to exclude fields, but for 
Equatable/Hashable we are discussing some future esoteric '@transient' modifier(which 
should describe the behaviour and destination of the property in details for compiler 
and conformed protocols so all will "just work") ?
How this future '@transient' will live together with current CodingKeys ?

IMO the right solution will be:
1. introduce 'deriving'-like keyword to explicitly express that you request an 
auto-synthesize of protocol requirements
2. introduce EquatingKeys/HashingKeys to be able to say which properties should be 
included in generated requirements
3. Think what kind of '@transient' marker could be introduced in future to replace 
the using of CodingKeys/EquatingKeys/HashingKeys.

Vladimir.

> 
>             -Thorsten
> 
>             Am 12.09.2017 um 16:38 schrieb Tony Allevato via swift-evolution
>             <swift-evolution at swift.org <mailto:swift-evolution at swift.org>>:
> 
>>
>>
>>             On Mon, Sep 11, 2017 at 10:05 PM Gwendal Rouц╘ <gwendal.roue at gmail.com
>>             <mailto: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 <mailto:swift-evolution at swift.org>
>>             https://lists.swift.org/mailman/listinfo/swift-evolution
> 
>             _______________________________________________
>             swift-evolution mailing list
>             swift-evolution at swift.org <mailto: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
> 


More information about the swift-evolution mailing list