[swift-evolution] [Proposal] Explicit Synthetic Behaviour

Vladimir.S svabox at gmail.com
Mon Sep 11 16:04:58 CDT 2017


On 11.09.2017 21:55, Thorsten Seitz via swift-evolution wrote:
> I think I do understand Haravikk's argument (actually it seems quite straightforward 
> to me).
> 
> An example should be:
> 
> struct Foo : Equatable {
>      var x: Int
>      var cachedLabel: String? = nil
> 
>      init(x: Int) {
>          self.x = x
>      }
> 
>      mutating func label() {
>          if let label = cachedLabel {
>              return label
>          }
>          let label = calculateLabel()
>          cachedLabel = label
>          return cachedLabel
>      }
> }
> 
> var foo1 = Foo(x: 1)
> var foo2 = Foo(x: 1)
> foo1 == foo2 // true
> var label = foo1.label()
> foo1 == foo2 // now false, due to cachedString being falsely included in the comparison
> 
> The problem is that the developer was not required to implement the protocol and so 
> might forget it.
> The difference to other default implementations is that those use the protocol itself 
> as building blocks and so are correct with regards to the protocol's semantics, 
> whereas the synthesized equality reaches deeply into the private innards of a struct 
> and therefore is much more likely to be wrong as in the example above.
> 
> Why not just write
> 
> *struct* Foo : *deriving* Equatable {...}
> 
> to request the synthesized implementation?

FWIW, +100. The same should be required for Codable. I support the opinion that 
'synthesized' methods differs from protocol-default-implementation in what 'kind' of 
data they use: defined by protocol itself or internals of the conformed type. And 
this can lead to more un-expected problems.

If protocol is able to synthesize its requirements, it should require a 
'deriving'-like marker when type conforms to it to make it absolutely clear what 
happens here. It would be not a confusion point, but clarify the intention to better 
understand the code.

Thinking about *future* custom protocols that could implement requirements in default 
implementation by using macros/reflection, for me it seems like such protocol should 
*also* somehow explicitly state that some requirements are auto-synthesized, probably 
by conforming(derive) to some compiler-magic protocol 'AutoSynthesize'.
(i.e. 'protocol MySynthesizeable: AutoSynthesize {...}')

So each built-in protocol like Equatable/Hashable/Codable will conform to it, and 
also, each custom "auto-synthesizeable" protocol - also should explicitly conform to 
AutoSynthesize. So, when type conforms to it - such type should use 'deriving'-like 
marker if auto-generation of methods is expected.

I also have a question regarding future direction of 'exclusion' of fields from being 
included into auto-generated implementation of Equatable/Hashable/Codable/other.

If we'll have this 'deriving'-like marker, it seems naturally if we mark some member 
with some kind of '@noderiving' marker, like here:

struct Foo : deriving Equatable {
       var x: Int
       var y: Int
       var z: Int
       @noderiving var cachedLabel: String? = nil
}

this @noderiving directive will work for protocols based on AutoSynthesize magic 
protocol. I.e., if you construct your own protocol with auto-synthesizeable methods, 
to be able to *know* which members should be 'excluded' for your implementation, you 
should base your protocol on AutoSynthesize protocol.

I hope this makes any sense :-)

Vladimir.

> 
> -Thorsten
> 
> 
> Am 09.09.2017 um 19:42 schrieb Xiaodi Wu via swift-evolution 
> <swift-evolution at swift.org <mailto:swift-evolution at swift.org>>:
> 
>>
>> On Sat, Sep 9, 2017 at 06:41 Haravikk via swift-evolution 
>> <swift-evolution at swift.org <mailto:swift-evolution at swift.org>> wrote:
>>
>>>     On 9 Sep 2017, at 09:33, Xiaodi Wu <xiaodi.wu at gmail.com
>>>     <mailto:xiaodi.wu at gmail.com>> wrote:
>>>
>>>
>>>     On Sat, Sep 9, 2017 at 02:47 Haravikk via swift-evolution
>>>     <swift-evolution at swift.org <mailto:swift-evolution at swift.org>> wrote:
>>>
>>>
>>>>         On 9 Sep 2017, at 02:02, Xiaodi Wu <xiaodi.wu at gmail.com
>>>>         <mailto:xiaodi.wu at gmail.com>> wrote:
>>>>
>>>>         On Fri, Sep 8, 2017 at 4:00 PM, Itai Ferber via
>>>>         swift-evolution<swift-evolution at swift.org
>>>>         <mailto:swift-evolution at swift.org>>wrote:
>>>>
>>>>
>>>>
>>>>>             On Sep 8, 2017, at 12:46 AM, Haravikk via swift-evolution
>>>>>             <swift-evolution at swift.org <mailto:swift-evolution at swift.org>> wrote:
>>>>>
>>>>>
>>>>>>             On 7 Sep 2017, at 22:02, Itai Ferber <iferber at apple.com
>>>>>>             <mailto:iferber at apple.com>> wrote:
>>>>>>
>>>>>>             |protocol Fooable : Equatable { // Equatable is just a simple
>>>>>>             example var myFoo: Int { get } } extension Fooable { static func
>>>>>>             ==(_ lhs: Self, _ rhs: Self) -> Bool { return lhs.myFoo ==
>>>>>>             rhs.myFoo } } struct X : Fooable { let myFoo: Int let myName:
>>>>>>             String // Whoops, forgot to give an implementation of == }
>>>>>>             print(X(myFoo: 42, myName: "Alice") == X(myFoo: 42, myName: "Bob"))
>>>>>>             // true|
>>>>>>             This property is/necessary/, but not/sufficient/to provide a
>>>>>>             correct implementation. A default implementation might be able
>>>>>>             to/assume/ something about the types that it defines, but it does
>>>>>>             not necessarily know enough.
>>>>>
>>>>>             Sorry but that's a bit of a contrived example; in this case the
>>>>>             protocol should*not* implement the equality operator if more
>>>>>             information may be required to define equality. It should only be
>>>>>             implemented if the protocol is absolutely clear that .myFoo is the
>>>>>             only part of a Fooable that can or should be compared as equatable,
>>>>>             e.g- if a Fooable is a database record and .myFoo is a primary key,
>>>>>             the data could differ but it would still be a reference to the same
>>>>>             record.
>>>>>
>>>>>             To be clear, I'm not arguing that someone can't create a regular
>>>>>             default implementation that also makes flawed assumptions, but that
>>>>>             synthesised/reflective implementations*by their very nature have
>>>>>             to*, as they cannot under every circumstance guarantee correctness
>>>>>             when using parts of a concrete type that they know nothing about.
>>>>             You can’t argue this both ways:
>>>>
>>>>               * If you’re arguing this on principle, that in order for
>>>>                 synthesized implementations to be correct, they/must/ be able to
>>>>                 —/under every circumstance/ — guarantee correctness, then you
>>>>                 have to apply the same reasoning to default protocol
>>>>                 implementations. Given a default protocol implementation, it is
>>>>                 possible to come up with a (no matter how contrived) case where
>>>>                 the default implementation is wrong. Since you’re arguing this/on
>>>>                 principle/, you cannot reject contrived examples.
>>>>               * If you are arguing this/in practice/, then you’re going to have
>>>>                 to back up your argument with evidence that synthesized examples
>>>>                 are more often wrong than default implementations. You can’t
>>>>                 declare that synthesized implementations are/by nature/incorrect
>>>>                 but allow default implementations to slide because/in practice/,
>>>>                 many implementations are allowable. There’s a reason why
>>>>                 synthesis passed code review and was accepted: in the majority of
>>>>                 cases, synthesis was deemed to be beneficial, and would provide
>>>>                 correct behavior. If you are willing to say that yes, sometimes
>>>>                 default implementations are wrong but overall they’re correct,
>>>>                 you’re going to have to provide hard evidence to back up the
>>>>                 opposite case for synthesized implementations. You stated in a
>>>>                 previous email that "A synthesised/reflective implementation
>>>>                 however may return a result that is simply incorrect, because it
>>>>                 is based on assumptions made by the protocol developer, with no
>>>>                 input from the developer of the concrete type. In this case the
>>>>                 developer must override it in to provide *correct* behaviour." —
>>>>                 if you can back this up with evidence (say, taking a survey of a
>>>>                 large number of model types and see if in the majority of cases
>>>>                 synthesized implementation would be incorrect) to provide a
>>>>                 compelling argument, then this is something that we should in
>>>>                 that case reconsider.
>>>>
>>>>
>>>>         Well put, and I agree with this position 100%. However, to play devil's
>>>>         advocate here, let me summarize what I think Haravikk is saying:
>>>>
>>>>         I think the "synthesized" part of this is a red herring, if I understand
>>>>         Haravikk's argument correctly. Instead, it is this:
>>>>
>>>>         (1) In principle, it is possible to have a default implementation for a
>>>>         protocol requirement that produces the correct result--though not
>>>>         necessarily in the most performant way--for all possible conforming
>>>>         types, where by conforming we mean that the type respects both the
>>>>         syntactic requirements (enforced by the compiler) and the semantic
>>>>         requirements (which may not necessarily be enforceable by the compiler)
>>>>         of the protocol in question.
>>>>
>>>>         (2) However, there exist *some* requirements that, by their very nature,
>>>>         cannot have default implementations which are guaranteed to produce the
>>>>         correct result for all conforming types. In Haravikk's view, no default
>>>>         implementations should be provided in these cases. (I don't necessarily
>>>>         subscribe to this view in absolute terms, but for the sake of argument
>>>>         let's grant this premise.)
>>>>
>>>>         (3) Equatable, Hashable, and Codable requirements are, by their very
>>>>         nature, such requirements that cannot have default implementations
>>>>         guaranteed to be correct for all conforming types. Therefore, they should
>>>>         not have a default implementation. It just so happens that a default
>>>>         implementation cannot currently be written in Swift itself and must be
>>>>         synthesized, but Haravikk's point is that even if they could be written
>>>>         in native Swift through a hypothetical reflection facility, they should
>>>>         not be, just as many other protocol requirements currently could have
>>>>         default implementations written in Swift but should not have them because
>>>>         they cannot be guaranteed to produce the correct result.
>>>>
>>>>         My response to this line of argumentation is as follows:
>>>>
>>>>         For any open protocol (i.e., a protocol for which the universe of
>>>>         possible conforming types cannot be enumerated a priori by the protocol
>>>>         designer) worthy of being a protocol by the Swift standard ("what useful
>>>>         thing can you do with such a protocol that you could not without?"), any
>>>>         sufficiently interesting requirement (i.e., one for which user ergonomics
>>>>         would measurably benefit from a default implementation) either cannot
>>>>         have a universally guaranteed correct implementation or has an
>>>>         implementation which is also going to be the most performant one (which
>>>>         can therefore be a non-overridable protocol extension method rather than
>>>>         an overridable protocol requirement with a default implementation). 
>>>
>>>         You're close, but still missing key points:
>>>
>>>          1. I am not arguing that features like these should*not* be provided, but
>>>             that they should*not* be provided implicitly, and that the developer
>>>             should actually be allowed to request them. That is exactly what this
>>>             proposal is about, yet no matter what I say everyone seems to be
>>>             treating me like I'm against these features entirely; *I am not*.
>>>
>>>
>>>     You are entirely against Equatable having a default implementation for ==.
>>>     This is unequivocally stated. Others favor such a default implementation and
>>>     feel that in the absence of a way to spell this in Swift itself, it should be
>>>     magic for the time being. For the purposes of this argument it really is not
>>>     pertinent that you are not also against something else; you're asking us to
>>>     discuss why you are against a particular thing that others are for.
>>
>>     FFS, how much clearer can I make this? *I AM NOT AGAINST THE FEATURE.*
>>     *
>>     *
>>     What I am against is the way in which it is being provided implicitly rather
>>     than explicitly, in particular as a retroactive change to existing protocols in
>>     a way that introduces potential for bugs that are currently impossible, but
>>     also in general.
>>
>>
>> You are against a default implementation for ==, i.e. an implementation that is 
>> provided for you if you conform a type to the protocol and do nothing else 
>> ("implicitly rather than explicitly"), and you are against the default 
>> implementation being on the existing protocol Equatable ("retroactive change"). So, 
>> to summarize, what you are against is precisely a default implementation for the == 
>> requirement on Equatable.
>>
>> This is the topic of discussion here; I am attempting to convince you that you 
>> should be for rather than against these things.
>>
>>
>>>     As repeatedly answered by others, nothing here is specific to synthesized
>>>     default implementations, as more powerful reflection will gradually allow them
>>>     to be non-synthesised.
>>
>>     And as repeatedly stated by me; I am not treating synthesised vs. run-time
>>     reflection any differently, I specifically included both in the original proposal.
>>
>>>     As pointed out very cogently by Itai, you assert but offer no evidence, either
>>>     in principle or empirically, that going too far by reflection is worse than
>>>     going not far enough without reflection in terms of likelihood of a default
>>>     implementation being inappropriate for conforming types.
>>
>>     As I have also repeatedly pointed out it is not an issue of "not going far
>>     enough" vs. "going too far"; if a default implementation lacks information then
>>     it should not be provided, doing so regardless is a flaw in the protocol design
>>     and not something that this proposal attempts to address (as such a thing is
>>     likely impossible).
>>
>>
>> Right, one must consider the semantics of the specific protocol requirement and ask 
>> whether a reasonable default can be provided for it.
>>
>>     Reflective implementations *necessarily* go too far, because they literally
>>     know *nothing* about the concrete type with any certainty, except for the
>>     properties that are defined in the protocol (which do not require reflection or
>>     synthesis in the first place).
>>
>>
>> I am confused why you are trying to argue in general terms about the universe of 
>> all possible default implementations that use reflection. This is necessarily a 
>> more difficult argument to make, and if it is to be convincing for all default 
>> implementations it must also be convincing for the two specific protocol 
>> requirements we are talking about here. Start small:
>>
>> We have agreed, as a community, that there is a reasonable default implementation 
>> for Equatable.== when certain conditions are met (for value types only at the 
>> moment, I believe). Namely, given two values of a type that has only Equatable 
>> stored properties, those values are equal if their stored properties are all equal. 
>> The author of a new value type who wishes to make her type Equatable but chooses 
>> not to implement a custom == then benefits from this default when all stored 
>> properties are Equatable.
>>
>>     And precisely what kind of "evidence" am I expected to give? This is a set of
>>     features that *do not exist yet*, I am trying to argue in favour of an explicit
>>     end-developer centric opt-in rather than an implicit protocol designer centric
>>     one. Yet no-one seems interested in the merits of allowing developers to choose
>>     what they want, rather than having implicit behaviours appear potentially
>>     unexpectedly.
>>
>>
>> Both options were examined for Codable and for Equatable/Hashable. The community 
>> and core team decided to prefer the current design. At this point, new insights 
>> that arise which could not be anticipated at the time of review could prompt 
>> revision. However, so far, you have presented arguments already considered during 
>> review.
>>
>>>     Therefore, your argument reduces to one about which default implementations
>>>     generally ought or ought not to be provided--that is, that they ought to be
>>>     provided only when their correctness can be guaranteed for all (rather than
>>>     almost all) possible conforming types. To which point I sketched a rebuttal above.
>>
>>     If a protocol defines something, and creates a default implementation based
>>     only upon those definitions then it must by its very nature be correct. A
>>     concrete type may later decided to go further, but that is a feature of the
>>     concrete type, not a failure of the protocol itself which can function
>>     correctly within the context it created. You want to talk evidence, yet there
>>     has been no example given that proves otherwise; thus far only Itai has
>>     attempted to do so, but I have already pointed out the flaws with that example.
>>
>>     The simple fact is that a default implementation may either be flawed or not
>>     within the context of the protocol itself; but a reflective or synthetic
>>     implementation by its very nature goes beyond what the protocol defines and so
>>     is automatically flawed because as it does not rely on the end-developer to
>>     confirm correctness, not when provided implicitly at least.
>>
>>
>> Again, if it applies generally, it must apply specifically. What is "automatically 
>> flawed" about the very reasonable synthesized default implementation of ==?
>>
>>>         And all of this continues to be a side-issue to the fact that in the
>>>         specific case of Equatable/Hashable, which thus far has gone ignored, is
>>>         that bolting this on retroactively to an existing protocol*hides bugs*.
>>>         The issue of reflective default implementations is less of a concern on
>>>         very clearly and well defined*new* protocols, though I still prefer more,
>>>         rather than less, control, but in the specific case of*existing* protocols
>>>         this fucking about with behaviours is reckless and foolish in the extreme,
>>>         yet no-one on the core teams seems willing or able to justify it, which
>>>         only opens much wider concerns (how am I to have any faith in Swift's
>>>         development if the core team can't or won't justify the creation of new
>>>         bugs?).
>>>
>>>
>>>     This has emphatically not gone ignored, as I have myself responded to this
>>>     point in an earlier thread in which you commented, as well as many others.
>>>     Crucially, no existing conforming type changes its behavior, as they have all
>>>     had to implement these requirements themselves. And as I said to you already,
>>>     the addition of a synthesized default implementation no more "hides bugs"
>>>     going forward than the addition of a non-synthesized default implementation to
>>>     an existing protocol, and we do that with some frequency without even Swift
>>>     Evolution review.
>>
>>     Feel free to a supply a non-synthesised default implementation for Equatable
>>     without the use of reflection. Go-on, I'll wait.
>>     You insist on suggesting these are the same thing, yet if you can't provide one
>>     then clearly they are not.
>>
>>
>> That is not the argument. The argument is that they are indistinguishable in the 
>> sense that the author of a type who intends to supply a custom implementation but 
>> neglects to do so will have a default implementation supplied for them. It is 
>> plainly true that this is no more or less likely to happen simply because the 
>> default implementation is synthesized.
>>
>>>     Put another way, what the proposal about synthesizing implementations for
>>>     Equatable and Hashable was about can be thought of in two parts: (a) should
>>>     there be default implementations; and (b) given that it is impossible to write
>>>     these in Swift, should we use magic? Now, as I said above, adding default
>>>     implementations isn't (afaik) even considered an API change that requires
>>>     review on this list. Really, what people were debating was (b), whether it is
>>>     worth it to implement compiler-supported magic to make these possible. Your
>>>     disagreement has to do with (a) and not (b).
>>
>>     Wrong. The use of magic in this case produces something else entirely; that's
>>     the whole point. It is *not the same*, otherwise it wouldn't be needed at all.
>>     It doesn't matter if it's compiler magic, some external script or a native
>>     macro, ultimately they are all doing something with a concrete type that is
>>     currently not possible.
>>
>>     And once again; *I am not arguing against a default implementation that cuts
>>     boilerplate*, I am arguing against it being implicit. What I want is to be the
>>     one asking for it, because it is not reasonable to assume that just throwing it
>>     in there is always going to be fine, because it quite simply is not.
>>
>>
>> If you have to ask for it, then it's not a default. You *are* against a default 
>> implementation.
>>
>>     _______________________________________________
>>     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