Skip to content
This repository has been archived by the owner on Jan 25, 2022. It is now read-only.

"Short-circuiting" feature #3

Closed
littledan opened this issue Jun 6, 2017 · 41 comments
Closed

"Short-circuiting" feature #3

littledan opened this issue Jun 6, 2017 · 41 comments

Comments

@littledan
Copy link
Member

In this proposal, as the explainer says,

a?.b.c().d   // undefined if a is null/undefined

The "short-circuiting" semantics received some criticism at the previous TC39 meeting where the proposal was discussed. A few committee members (including Waldemar Horwat) expressed a preference that this would throw (on the access to .c) to keep the semantics simple and more locally understandable, and refrain from changing .. The explainer talks about the "right hand side", but where does the right hand side end exactly?

If you don't plan to remove this feature, it would be useful to extend the explainer with some more motivating use cases.

@xtuc
Copy link
Member

xtuc commented Jun 6, 2017

This is related to #2. The question is when does the Nil reference ends its propagation.

@littledan
Copy link
Member Author

@xtuc Yes, it's related; I had missed that bug. I meant this bug to track a more basic question: do we need Nil at all, or can we just say that ?. always outputs an ordinary JavaScript value? But you can think of this as "Nil never propagating at all".

@claudepache
Copy link
Collaborator

to keep the semantics simple and more locally understandable, and refrain from changing ..

I’ve planned to give an alternative informal description of my envisioned semantics in terms of abrupt completions instead of Nil reference; I’ve not done it yet due to some IRL concerns, but I promise to do it the next few days. I hope that that alternative description will convince that there is more locality than what is apparent at first glance.

The explainer talks about the "right hand side", but where does the right hand side end exactly?

In that context, the “right-hand side” means basically “everything that is on the right of ? inside a chain of property accesses, method invocations, etc.”, where “property accesses, method invocations, etc. ” is approximately what is found in Section LeftHandSide Expression of the spec. That is not an exact characterisation, by I hope it gives an idea in simple cases.

@littledan
Copy link
Member Author

@claudepache Sorry, I guess I meant that "where does the right hand side end exactly" question a bit more rhetorically--you definitely defined things clearly, but from a user perspective, is . so different from other operators? I don't see why it should be.

@xtuc
Copy link
Member

xtuc commented Jun 6, 2017

Can you look at the comments in #2, there are example of that.

@claudepache
Copy link
Collaborator

but from a user perspective, is . so different from other operators?

The short answer: the precise place where the short-circuit ends is where it is the most useful for the user, while remaining relatively simple to understand. It is a balance between usefulness and simplicity.

For the long answer... I expect to write it soon in my repo (also, after I’ll have read #2).

@xixixao
Copy link

xixixao commented Jun 6, 2017

Note that you can get the "other" desired semantics by doing (a?.b).c().d, but if you make that the default semantics, you will force users to do a?.b?.c?.()?.d. So the two options for expressing these two different expressions are:

// proposed
x = a?.b.c().d
y = (a?.b).c().d

// vs
x = a?.b?.c?.()?.d
y = a?.b.c().d

where y is something that you almost never want to do, and x is what you almost always want. On this alone you would choose the proposed semantics, but there is even more important consideration:

What if a.b is null.

if I want x to throw when a.b is null, I can no longer express x in the "other" semantics. I would have to do:

// proposed
x = a?.b.c().d

// vs
x = a == null ? null : a.b.c().d

which, clearly, is not using the operator at all, completely defeating it's purpose. This is why the current proposal is more "local", as it incentivize users to put ?. only after the one nullable object in a chain.

@rattrayalex
Copy link

rattrayalex commented Jun 6, 2017

While @xixixao 's case should be sufficient to show that "short-circuiting" is the only usable form of this feature, here is another argument in its favor:

given

type User = {
  id: string,
  // ...
  address: {
    street: string,
    // ...
  }
}
function getUserIfExists(id): ?User {
  if (users[id]) return users[id];
  return null;
};
const user = getUserIfExists('user_abc123');

// as proposed:
const street = user?.address.street; 
// without "short-circuiting":
const street = user?.address?.street; 

The latter version doesn't make much sense; a user should always have an address. Given a user, we would want to throw if its address == null.

However, without short-circuiting, we would be forced to write user?.address?.street because of the (valid) possibility of user == null.

Thus, the genuinely incorrect case of user = {} would not throw, even though we would want it to.

@ljharb
Copy link
Member

ljharb commented Jun 6, 2017

Short-circuiting on member access is fine, and obviously the proposal is almost useless without that.

I'm suggesting that extending that short-circuiting to function invocation is problematic.

@rattrayalex
Copy link

rattrayalex commented Jun 6, 2017

Ah, perhaps I misunderstood what exactly is meant by "short-circuiting" (judging by discussions in the babel slack channel #proposal-opt-chaining).

eg; one committee member seems to desire:

  • given a?.b.c and a == null, no error
  • given a?.b.c() and a == null, throw error

...which seems like very surprising behavior to me.

EDITED EDIT:

I believe @ljharb was responding as if we were on another thread (#2) as it's a bit unclear what the distinction between them are. It's my impression that @ljharb is in accord with @xixixao , @jridgewell, myself, and others on this particular thread, but in disagreement on the question of #2.

Apologies for the confusion 😄

@jridgewell
Copy link
Member

@rattrayalex: That particular issue is being discussed in #2. This one is concerned with:

a?.b.c;     // a = {b: { c: 42 } }
a?.b.c;     // a = null

From OP:

A few committee members (including Waldemar Horwat) expressed a preference that this would throw (on the access to .c)

Specifically, the second case should throw (you have to use ?. all the way down). I hate this.

@ljharb
Copy link
Member

ljharb commented Jun 6, 2017

@jridgewell to clarify, in both cases, a = {} would throw on .c, yes?

@rattrayalex
Copy link

rattrayalex commented Jun 6, 2017

@jridgewell @ljharb does everyone on TC39 agree with:

Short-circuiting on member access is fine, and obviously the proposal is almost useless without that.

If so can this issue be closed?

Pardon my confusion 😄

@jridgewell
Copy link
Member

Lol, I guess there are 3 cases:

a?.b.c;     // a = {b: {} }
a?.b.c;     // a = {}
a?.b.c;     // a = null

OP would have us throw in case 2 and 3. I argue that only case 2 should throw (because b.c isn't using optional chaining).

@ljharb
Copy link
Member

ljharb commented Jun 6, 2017

@rattrayalex no, I don't think the committee has consensus on that either.

@jridgewell thanks for clarifying.

@rattrayalex
Copy link

@ljharb thanks for clearing that up! I'll amend my example here back to its original, non-method-specific state. I may move the method-specific version to #2.

@littledan
Copy link
Member Author

OK, from the above thread, it sounds like short circuiting is important specifically in the case where you do want to throw an exception if the implicit JSON schema is not being followed. Is that the idea?

@claudepache
Copy link
Collaborator

Here is the main reason I want short-circuit. When I write:

a?.b.c().d

I assert that a may be null/undefined, but, if it is not, neither of a.b, a.b.c or a.b.c() shall be null/undefined. If any of those last assertions is false, a TypeError will be thrown, making it easier to debug than if I was forced to write ?. at each level.

I gave the same argument one year ago on es-discuss with a more illustrative (although synthetic) example:

https://esdiscuss.org/topic/optional-chaining-aka-existential-operator-null-propagation#content-12

A second reason is expressibility: I want to write ?. for saying explicitly that there is a branching at this point, in contrast with just propagating undefined. Since propagating undefined at least until the end of the chain is the only reasonable semantics, it should really be automatic. — That last sentence applies only to the undefined value produced explicitly by ?.: any undefined value produced earlier or later shall be considered as a bug until statement of the contrary.

@rattrayalex
Copy link

@littledan that's one major reason. Verbosity is another - notable because the primary intent of the feature, as I understand it, is to reduce the verbosity of x == null ? ... and thus encourage safer, more correct code.

@marlun78
Copy link

If a?.b.c throws on .c, how about ?a.b.c not to throw?

@Mouvedia
Copy link

@marlun78 what you want is ?(…)

@littledan
Copy link
Member Author

@claudepache Thanks for explaining; that makes sense to me.

@marlun78 If you don't want it to throw, you could use a?.b?.c--would that work for you?

@marlun78
Copy link

So is ?(a.b.c) an alternative way to write a?.b?.c?

@littledan
Copy link
Member Author

@marlun78 I haven't seen a proposal for this use of ?. It's not clear to me how it could work without syntactic ambiguities.

@marlun78
Copy link

Okay, so I misunderstood @Mouvedia’s comment...

@littledan
Copy link
Member Author

@marlun78 I don't know what to make of @Mouvedia 's comment either; I think they're making a new proposal.

@claudepache
Copy link
Collaborator

Some comments about the original objection (#3 (comment))

In this proposal, as the explainer says,

a?.b.c().d   // undefined if a is null/undefined

The "short-circuiting" semantics received some criticism at the previous TC39 meeting where the proposal was discussed. A few committee members (including Waldemar Horwat) expressed a preference that this would throw (on the access to .c) to keep the semantics simple and more locally understandable, and refrain from changing .. The explainer talks about the "right hand side", but where does the right hand side end exactly?

  • Making the language locally understandable is important. But it should not trump “structured goto” features (such as the “break” statement and the short-circuiting of the “||” operator) when it makes sense. You won’t argue that the “break” statement should skip only the following instruction instead of all instructions until the end of the corresponding block for the sake of locality.

  • From a user point-of-view, in expressions like a?.b.c, when a is null/undefined, without short-circuiting (more properly, without long short-circuiting), .b is skipped, and evaluation continues with .c. With “long” short-circuiting, .b.c is skipped and the evaluation continues after the whole chain. In both cases, the part of the expression that is not evaluated is easily understandable by humans, and the “long” jump is not significantly more difficult to understand than the “short” one, with possible exception of few edge cases that I’ll discuss in a separate issue.

  • What is exactly meant by “refrain from changing .”? The only “change” of . is that it may be skipped (because of short-circuiting). (Spec-wise, it may need amendments in order to handle abrupt completion, Nil reference, or whatever is needed to express the “short-circuit” signal.)

@victornpb
Copy link

I'm not sure how mature the feature is. But what I want to see is a operator like typeof that could "see" down the path. Like

if(operator obj.foo.bar.baz) obj.foo.bar.baz = "hello world";

This kind of solves the problem of

if(obj && obj.foo && obj.foo.bar && obj.foo.bar.baz) ...

It wouldn't introduce new syntax.

The operator would return the value of the property itself or undefined if it can't reach the last property.

There's also de benefit that you can use it in conjunction with typeof and instance of.

@ljharb
Copy link
Member

ljharb commented Aug 8, 2017

An operator is new syntax; that's what this proposal is. You're describing new syntax that has automatic short circuiting all the way down the chain.

@claudepache
Copy link
Collaborator

Also, note that the current proposal is more precise. That is, you have to write:

obj?.foo?.bar?.baz

in order to get:

obj != null && obj.foo != null && obj.foo.bar != null ? obj.foo.bar.baz : undefined

with a one-to-one correspondance between ? in the expression and != null in its desugaring. This is intended: so that we gain shortness, but don’t lose precision. (This feature is orthogonal from short-circuiting.)

@victornpb
Copy link

victornpb commented Aug 8, 2017

The typeof already have if(IsUnresolvableReference( V ))

if(x){ //ReferenceError: Can't find variable: x
}

if(typeof x){ //undefined (doesn't throw no try-catch block needed)
}

var bar = dip obj.foo.bar;
var bar = dig obj.foo.bar;

What I'm proposing is something equivalent to this, but in a operator.
https://gist.github.com/victornpb/4c7882c1b9d36292308e

What I meant about syntax, it is indeed a new reserved word, but it doesn't introduce new symbols, it would be just another UnaryOperator.

@claudepache
Copy link
Collaborator

@victornpb

What I'm proposing is something equivalent to this, but in a operator.
https://gist.github.com/victornpb/4c7882c1b9d36292308e

Yes, and this is what I don’t want: I want the more precise ?. operator, which does only one local test for nullity. I’ve written this justification some time ago.

@garygreen
Copy link

Instead of:

obj?.foo?.bar?.baz

Would it be possible to support?:

(obj.foo.bar)?.baz

@ljharb
Copy link
Member

ljharb commented Jun 6, 2019

@garygreen that should just work, i'd expect - with or without the parens.

@shannon
Copy link

shannon commented Jun 6, 2019

I'm not sure but I think @garygreen was suggesting that with parenthesis it would be equivalent.

If I understand correctly they aren't equivalent and this will error when obj or foo is undefined.

obj.foo.bar?.baz
//or
(obj.foo.bar)?.baz

Where as this won't:

obj?.foo?.bar?.baz

I'm not for or against this suggestion but I just read it differently so I think it needs clarifying to avoid confusion.

@garygreen
Copy link

garygreen commented Jun 6, 2019

I'm not sure but I think @garygreen was suggesting that with parenthesis it would be equivalent.

Yes exactly, if your wrapping in parenthesis I personally would expect it to mean everything in the parenthesis is optional. So:

(obj.foo.bar)?.baz

Should (IMO) be the equivalent of:

obj?.foo?.bar?.baz

I understand specifying the optional chaining ? at every point gives you complete control, but I think the parenthesis would be the preferred way of expressing a deeply nested optional chain as it's easier to understand and visually more pleasing.

It's possible this has already been considered though, so I'm just throwing the idea out to see if it's feasible.

@claudepache
Copy link
Collaborator

@garygreen
Instead of:

obj?.foo?.bar?.baz

Would it be possible to support?:

(obj.foo.bar)?.baz

Please just give me the time to find out where I’ve written that, so that I repost it here... Ah, it was in #82 (comment):

By design, you have indeed to write foo?.bar?.baz, i.e. put the question mark precisely at each place where you expect to possibly get null/undefined. We don’t think that reducing the number of question marks is worth the lost of precision.

That was already discussed in #29; see in particular #29 (comment) and #29 (comment), and the FAQ entry on the README that begins with ”In a deeply nested chain ...”

@ljharb
Copy link
Member

ljharb commented Jun 6, 2019

@garygreen the paren version, if it parses, needs to be the same as:

const o = obj.foo.bar;
o?.baz

or else I’d consider that a design flaw.

@claudepache
Copy link
Collaborator

@garygreen It is not a design flaw, it is a design feature.

What you are asking, is that the meaning of . inside (obj.foo.bar) is modified (basically, taking the semantics of ?.), because the group is followed by ?..

Like other operators, ?. has a local effect; it does not change the way its LHS (or its RHS, BTW) is evaluated. (It just makes the evaluation of its RHS optional.)

You are asking that ?. have a more complicated semantics than that; but we don’t see that saving a few question marks is worth the complication of the mental model. (Worse, in the most common case of less than four ?. in a row, your proposal wouldn’t even save any character.)

sendilkumarn pushed a commit to sendilkumarn/proposal-optional-chaining that referenced this issue Jun 22, 2019
@claudepache
Copy link
Collaborator

Closing this issue, since we attained stage 3, and short-circuiting is an important part of the semantics.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests