tc39 / proposal-optional-chaining

https://tc39.github.io/proposal-optional-chaining/
4.94k stars 75 forks source link

Possibly an even cleaner syntax #29

Closed d4nyll closed 6 years ago

d4nyll commented 6 years ago

I have an idea that incorporates parts of #9, that I wish to discuss here.

Proposed Amendments

Let's say we have this:

const name = user && user.settings && user.settings.account && user.settings.account.lastname;

This will turn into the following with the Optional Chaining Operator:

const name = user?.settings?.account?.lastname

This is a very common use case, and I'd argue the most common use case for the optional chaining operator. Would I be jumping the ship in saying it'll be cleaner to simply have this:

const name = user.settings.account?.lastname

So that everything left of the ?. would be evaluated 'step-by-step' and if any of the steps are null or undefined, then the expression returns undefined

The semantic of the operator would change from:

the operand at the left-hand side of the ?. operator evaluates to undefined or null, the expression evaluates to undefined

To:

all operands to the left-hand side of the ?. operator evaluates to undefined or null, the expression evaluates to undefined

In the of the current proposal, you have the example:

a?.b[3].c?.(x).d
a == null ? undefined : a.b[3].c == null ? undefined : a.b[3].c(x).d
  // (as always, except that `a` and `a.b[3].c` are evaluated only once)

So what if b[3] evaluates to undefined? Then it would throw an error. By having the ?. operator evaluates the entire chain, we can omit having many ?. operators.

Why this might not be a good idea

  1. Performance - with the current proposal, each check is explicit. With this amendment, the engine might have to do checks that are unnecessary. For example, if we know for certain that the user.settings object always exists, then this new proposal would be performing 2 superfluous checks
  2. Weird syntax - you'd have to put the ? after the penultimate operand, which can seem unnatural

Workarounds

  1. Grouping - If we can be sure that user.settings exists, then we can group user.settings.account together so the ?. operator will treat that group as one block. I.e. (user.settings.account)?.lastname
  2. New syntax - maybe something like user.settings.account.lastname ?| 'default', where ?| will essentially be the same as user?.settings?.account?.lastname || 'default'. The 'default' may be omitted to produce the same result as the current proposal - user.settings.account.lastname?| would be the same as user?.settings?.account?.lastname || undefined, which is the same as user?.settings?.account?.lastname

    This will actually allow the operator to be much more powerful, as it can incorporate the Nullary Coalescing operator into the optional chaining operator.

My Thoughts

I feel like the current proposal is good enough, and allows you to be explicit rather than implicit, with the small downside of the code looking more verbose than it could be. I feel this change is unnecessary, as it is, but if it can be combined with the Nullary Coalescing operator, it could be a worthwhile discussion.

Tagging @tvald as he commented in a relevant prior discussion Tagging @gisenberg as he is the author for the Nullary Coalescing operator proposal Tagging @claudepache, @ljharb, @Mouvedia, @nerfpops, @adaptabi, @rattrayalex, @ittledan for their participation on #9.

ljharb commented 6 years ago

Definitely do not agree with this idea.

This is magic and implicit. If I want some of the member accesses in the chain to be required, then the current proposal allows it - whereas yours would force me to separate my chain into multiples.

d4nyll commented 6 years ago

Yes, I agree that the implicit nature of this proposal bugs me too. Thanks for your feedback!

darsain commented 6 years ago

An explicit alternative might be an additional syntax sugar, something like:

// applies optional chaining to all subsequent property lookups
const name = ?user.settings.account.lastname
// an equivalent of
const name = user?.settings?.account?.lastname
meandmycode commented 6 years ago

I think this hints on a real world vs theory thing for me, I think the majority of use cases for optional chaining will be defensive programming, where 95% of the time developers are wanting to deeply access a property as in the example above.

Saying it's magic might be a hint that the implementation may not be possible but I think it's important with new syntax to be open minded.

tvald commented 6 years ago

The proposed chain assumes a common expectation about the entire access chain. It's entirely possible that we might want user.settings to throw if user is undefined.

While the current syntax might be slightly verbose for deep access chains, it's possible to express heterogeneous expectations about that chain.

meandmycode commented 6 years ago

I don't think the concept disagrees or disallows that scenario, it's more about favouring less verbosity for the common case.

FWIW, I'm playing devils advocate as I think decisions should be driven by data, not emotion - a feeling might suggest there's a problem, but should be backed up with some analysis.

For an argument that was well written up I think it deserves that.

tvald commented 6 years ago

I don't think the concept disagrees or disallows that scenario

While it technically doesn't prevent the expression of heterogeneous expectations, it becomes far more verbose:

if (user == null) throw('user is undefined')
const name = user.settings.account?.lastname

By making assumptions about the entire access chain, the proposal greatly reduces the operator's flexibility in order to save just 2 characters in the example. That's a poor tradeoff.

ljharb commented 6 years ago

user.settings.account?.lastname clearly says to me that i should get an exception if user or user.settings isn't an object, but that user.settings.account doesn't have to exist - in which case name would be null or undefined.

d4nyll commented 6 years ago

After reading through the feedback, I agree with @tvald's comment that having ?. evaluate everything to the left of it would reduce the flexibility of the operator, and thus would not be good.

But @meandmycode makes a good point that many of the use cases would be for defensive programming, and thus having something that caters for the (arguably) main use case would be good.

I'd bet that once this proposal is accepted, this shorthand would be one of the first thing many people would be asking for.

So in that light, what do you think about @darsain's idea of having an operator that does allow for that:

// applies optional chaining to all subsequent property lookups
const name = ?user.settings.account.lastname
// an equivalent of
const name = user?.settings?.account?.lastname
Mouvedia commented 6 years ago

what do you think about @darsain's idea of having an operator that does allow for that

It would be great but that would require to support the parentheses version as well:


// applies optional chaining to all subsequent property lookups
const name = ?user.settings.account.lastname

// check first 3 only (being explicit about the part of the chain that we are unsure about)
const city = ?(user.settings.account).address.city
levithomason commented 6 years ago

Optional chaining and nullary coalescing are already syntactic sugar. I would propose no sugar be added to the sugar. Too much sugar.

darsain commented 6 years ago

@levithomason imho, the whole point of these things is to make code more concise and comfortable to write, and this:

?user.settings.account.lastname

Is the 99% use case we should be focusing on. Just imagine how annoying it is going to be typing the question mark before each dot:

user?.settings?.account?.lastname
levithomason commented 6 years ago

I just prefer explicit and a little more verbosity.

I don't think it is a good thing to make things more concise for the sake of being concise. I subscribe to the code is for humans not machines idea, and I believe it applies to syntax as well. It is the same reason I've come to reject almost all forms of abbreviation in code. We have minifiers for making things concise, outside of that let's make it human readable and explicit.

Just my 0.02.

ljharb commented 6 years ago

Annoying, or clear and explicit?

levithomason commented 6 years ago

Someone will write a babel-plugin for it anyway ;)

claudepache commented 6 years ago

... 95% ... ... 99% ...

Completely wrong.

According to statistics gathered from CoffeeScript usage in https://github.com/tc39/proposal-optional-chaining/issues/17:

... Total soak operations: 4627 ... Total soak operations chained on top of another soak: 564

564/4627 is about 12%, not >90%.

d4nyll commented 6 years ago

@claudepache is right.

My initial thoughts was that the repositories used by coffeescript-soak-stats are mostly libraries and simple applications. Maybe usage by more complex applications (with more complicated data structures) would be different.

But this is not the case. When I ran the same tests with the most popular CoffeeScript applications on GitHub (sorted by stars), as opposed to libraries and applications, the percentage that used "soak operations chained on top of another soak" was around 11%.

2426/2426
Total files: 2425
Total lines: 418841
Total soak operations: 3879
Total soaked member accesses: 3224
Total soaked dynamic member accesses: 185
Total soaked function applications: 467
Total soaked new invocations: 0
Total soak operations using short-circuiting: 1157
Total soak operations using short-circuiting (excluding methods): 170
Total soaked assignments (including compound assignments): 34
Total soaked deletes: 1
Total cases where parens affected the soak container: 0
Total soak operations chained on top of another soak: 434

The following repositories were used:

Interesting to note that in the yakyak repository, usage was at 33%, whereas others are as low as 5.2%. It implies that for applications with heavily nested data structures, they will use consecutive optional chaining frequently, and would benefit heavily with the ?user.settings.account.lastname syntax. Other applications which do not have heavily-nested structures would not benefit from this, however.

I still think ?user.settings.account.lastname is a good idea. You can be explicit and verbose with the user?.settings.account?.lastname syntax if you want to define how to group the checks. But by offering an additional sugar with ?user.settings.account.lastname, you can help those developers who primarily uses it for deep access of object properties to keep their code more concise and readable.

11 - 12% isn't a large percentage, but it's still significant enough.

meandmycode commented 6 years ago

Great to see some real data 👍 the stats seem to disagree that this would be the common case, but I'm not sure they accurately say that developers would rather be explicit.

The stat only counts chained use, but it's not considered against if the developer even has the opportunity (i.e., it would be better to see counts for deep property access a.b.c.d vs chained a?.b?.c?.d, or for x deep access that are n deep, y used soak).

claudepache commented 6 years ago

I'm not sure they accurately say that developers would rather be explicit.

But, as language designer, I want to force them to be explicit (unless strong reason against that), for their own good and for the sake of those that will read their code...

The current Optional Chaining proposal is carefully designed, so that the developer has the opportunity to express exactly what they mean, not less and (importantly) not more. That is, when I write:

a.b?.c.d(x).e

I express that that a.b may be null for some valid reason. But I don’t expect that a is null, and, in case a.b is not null, I don’t expect that either of a.b.c, a.b.c.d or a.b.c.d(x) is null; and if my expectation is wrong, I want a TypeError to be thrown in my face rather than swept under the rug, so that I am aware that there is some logic error earlier in my code.

As shown by the stats, cases where there are at least two property accesses in the chain and you want optional chaining at each level, is not the general case, not even the majority case.

Optional chaining and nullary coalescing are already syntactic sugar. I would propose no sugar be added to the sugar. Too much sugar.

Agreed. Healthy diet also includes salt.

meandmycode commented 6 years ago

Whilst I'm personally not sure this additional syntax is needed (an implied syntax version), I'm not sure it's as simple as this is bad language design because its not explicit, look at statically typed languages for example that have evolved to imply types in various places, I think a more accurate argument might be that it feels unusual for current ecmascript and it's value add over the explicit syntax is too minor.

But consider operators like typeof, instanceof or even regex options, which change the context of what the next/prior expression mean, there's already the ability to change execution aspects of sibling expressions today.

ljharb commented 6 years ago

@meandmycode typeof only avoids a ReferenceError on its RHS, otherwise, it and instanceof are just binary operators. There's not anything prior to this proposal that I can think of that changes how member expressions are evaluated.

meandmycode commented 6 years ago

Yes certainly not the context I was suggesting.

victornpb commented 6 years ago

I'm following this thread for some time now, and I'm not convinced yet that this is a good idea. And the lack of consensus seems to confirm that

Mouvedia commented 6 years ago

What's wrong with one of the listed "workarounds"?

(user.settings.account)?.lastname

Seems explicit enough to me.

tvald commented 6 years ago

Sure, the workaround is explicit, but it's the proposed default behavior that's being criticized.

This line isn't explicit:

user.settings.account?.lastname

Mouvedia commented 6 years ago

Well then I propose that the parentheses variant be the "default behavior". To be clear, that would be it, nothing else.

d4nyll commented 6 years ago

The story so far...

For those who don't like reading


I think the consensus here is that the original proposal to have user.settings.account?.lastname be equivalent to user?.settings?.account?.lastname is a bad idea, purely on the grounds that it does not allow the user to be explicit. As stated in the My Thoughts section in my proposal, I wasn't sure it was a good idea, except when combined with the Nullary Coalescing operator. Having read everyone's comments, it's clear that my original concern was valid.

What me and @meandmycode are still arguing for is the additional syntax of having ?a.b.c.d.e be equivalent to a?.b?.c?.d?.e on the grounds that it is much easier to read the former than the latter.

The counter-argument (or part of it) is that statistically, there's not enough demand for that use to justify the additional sugar. My counter argument is that for some projects, the usage is still high enough that this additional syntax would be valuable.

meandmycode commented 6 years ago

Ultimately I wanted to ensure we were open minded about syntax, to avoid a so-called baby duck syndrome - I think it's important to challenge your own gut-feeling (which I think it has* been).

For what it adds I personally think the additional syntax is conceptually OK but statistically questionable if people would use it and it does feel like something that could only be reasoned about properly after optioning chaining in its current form is the norm in the ecosystem.

levithomason commented 6 years ago

TL;DR In short, I just don't think it is a good idea 😕 Just type a few question marks when needed.


What me and @meandmycode are still arguing for is the additional syntax of having ?a.b.c.d.e be equivalent to a?.b?.c?.d?.e on the grounds that it is much easier to read the former than the latter.

Over the years I've found most readability issues, both mine and others', to be resolved with only a few weeks exposure. After that, we learn to read differently and it feels normal. In my experience, resistance or unwillingness to change is often labeled a readability issue for convenience.

That said, I don't believe there is anything more readable about either one of these syntaxes given some time and syntax highlighting.

The counter-argument (or part of it) is that statistically, there's not enough demand for that use to justify the additional sugar. My counter argument is that for some projects, the usage is still high enough that this additional syntax would be valuable.

For me, this is hardly the main case. I do not think that because something is valuable that it also must be a good thing. I also don't believe that popularity is necessarily an indicator of a good thing either.


I have many exceptions to the proposal that I haven't shared. Here are some of my main gripes:

  1. Sometimes saving a few characters does not justify adding syntax. Syntax is evil. Adding more of it should be done with care.
  2. I believe verbose > concise. Code is for humans.
  3. There is no functional gain. The drive is purely to save few characters in rare cases. Compare something like functions vs arrow functions, there was a functional gain added to the language.
  4. The need for this feature itself is code smell to me. I would question why this is needed and consider fixing it rather than build a feature to support it.
  5. I see no precedence for shorthand repeat operators, and duplicating the pattern feels wrong:

    const foo = first || second || third
    // vs
    const foo = || first second third
    
    const total = 1 + 2 + 3 + 4
    // vs (polish notation)
    const foo = + 1 2 3 4
  6. I see no mirrored syntax to complement or justify the shorthand, such as in object destructuring and object shorthand:

    const foo = { one: 1 }
    const { one } = foo
    
    const one = 1
    const foo = { one }
  7. IMHO, it seems to deviate from the tone of JavaScript. It feels more inline with the ideals of something like CoffeeScript, where shorthand sugar is pervasive.
  8. I have doubts as to how this would shake out in parsing and minification, starting a statement with a question mark. I have not checked all permutations, but between ternaries, object values, and nullary coalescing it feels pretty hairy.
claudepache commented 6 years ago

@ all: Thanks for your input. I’ve added an entry in the FAQ explaining briefly why we won’t do that. If you think that this FAQ entry ought to be improved, do not hesitate to share your suggestion.