[swift-evolution] [Proposal] Explicit Synthetic Behaviour

Tony Allevato tony.allevato at gmail.com
Mon Sep 11 17:14:26 CDT 2017


On Mon, Sep 11, 2017 at 2:43 PM Gwendal Roué via swift-evolution <
swift-evolution at swift.org> wrote:

> Doesn't it escalate pretty quickly into complex and ad-hoc language
> constructs?
>
> Like everybody I like code synthesis. Like some, I'm worried that implicit
> synthesis would hide a few bugs that are hard to debunk. I also agree that
> developers who complain about those bugs would rightfully get the "behaves
> as expected" and "RTFM" classical answers. The problem with those deserved
> answers is that there's not much lesson to learn. Being bitten one, two,
> three times does not reduce the probability of being bitten another time.
> Programmer errors due to carelessness are the most difficult errors to
> prevent, don't you all agree?
>
> People who use Sourcery are quite happy with AutoEquatable and
> AutoHashable. I don't know of anybody who complains of those. People are
> happy. Nobody types `AutoEquatable` by mistake: they get synthesis where
> they ask for it, and move on their next task without thinking much more
> about it. Sounds like a developer's dream, isn't it?
>
> 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.

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.

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?

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.



>
> Gwendal
>
> Le 11 sept. 2017 à 23:05, Vladimir.S via swift-evolution <
> swift-evolution at swift.org> a écrit :
>
> 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
> <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
> <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 <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
> <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 <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 <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
> <swift-evolution at swift.org>>> wrote:
>
>
>            On 7 Sep 2017, at 22:02, Itai Ferber <iferber at apple.com
>            <mailto:iferber at apple.com <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
> <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
> <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
>
> _______________________________________________
> 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/20170911/2e1cab91/attachment-0001.html>


More information about the swift-evolution mailing list