On 13.09.2017 7:14, Xiaodi Wu via swift-evolution wrote:

On Tue, Sep 12, 2017 at 22:07 Tony Allevato <[email protected] <mailto:[email protected]>> wrote:

    On Tue, Sep 12, 2017 at 7:10 PM Xiaodi Wu <[email protected]
    <mailto:[email protected]>> wrote:

        On Tue, Sep 12, 2017 at 9:58 AM, Thorsten Seitz via swift-evolution
        <[email protected] <mailto:[email protected]>> 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
            <[email protected] <mailto:[email protected]>>:



            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

            _______________________________________________
            swift-evolution mailing list
            [email protected] <mailto:[email protected]>
            https://lists.swift.org/mailman/listinfo/swift-evolution



_______________________________________________
swift-evolution mailing list
[email protected]
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
[email protected]
https://lists.swift.org/mailman/listinfo/swift-evolution

Reply via email to