-
Notifications
You must be signed in to change notification settings - Fork 75
How does Nil affect non optional function calls #2
Comments
As I see it, this: a?.b() Should be exactly like (a?.b)() And so, it should throw. But this: a?.b?.() Should behave like so: typeof (a?.b) === 'function' ? (a?.b)() : null; And so should just give up on calling the function. More examples can be found here |
Yes, I agree (with the caveat that it's still an open question if the last conditional check is typeof function or == null) |
I believe that checking if it's a function gives us not only more useful operator, but also it seems more intuitive as the only thing I meant to write is parentheses for function call. I mean, without the new optionals I would write func(); which (on my chrome console) will throw me the error Then iterating on my code trying to fix the problem I will just change it to: func?.() and expect it to not throw me the same error again.. |
I agree, but it makes |
Which brings back the But seriously, I think the way of thinking about it is as another operator really which is made of these I think this fits with the change from "null coalescing" to "optional chaining" as the later is more abstract as to what the |
Optional chaining is a specific usage of a soaked access, which includes soaked method invocation.
|
I'm going to reference several points here. Let's start with regular member access: // Member Access
a.b?.c.d
// Rough ES5
(a.b == null) ? void 0 : a.b.c.d Here, optionality isn't deep. Only // Delete
delete a.b?.c.d
// Rough ES5
(a.b == null) ? void 0 : delete a.b.c.d Notice the // Left hand assignment
a.b?.c.d = 42
// Rough ES5
(a.b == null) ? void 0 : a.b.c.d = 42 Again, we have Call cases: func?.() Should throw for consistency if
Now what should deep calls do? a.b?.c.d()
// Rough ES5
(a.b == null) ? void 0 : a.b.c.d() // Option 1
((a.b == null) ? void 0 : a.b.c.d)() // Option 2 I'm in favor of option 1. It matches the the We can do option 2, but I dislike it. Imagine the ES5 we'd write today to get this same kind of "call this always" (I'm omitting direct translation from option 2 back into let func;
if (a.b) {
func = a.b.c.d;
} else {
func = noop;
}
func(); To keep let context;
if (a.b) {
context = a.b.c;
} else {
context = { d: noop };
}
context.d(); I just don't image people would really write that. Instead, they'd move the call into the if (a.b) {
a.b.c.d();
}
// Let's write that as an expression:
(a.b == null) ? void 0 : a.b.c.d(); That's option 1 above. 😀 |
Looking from a types point-of-view, |
@jridgewell You've laid out the options quite well, I think - thank you. I can accept that However, I strongly disagree with option 1; I think I would not want to see the proposal advance if a single What about |
I'd see it as: a?.b.c().d.e
(a == null) ? void 0 : a.b.c().d.e; I don't understand what "soaking" means, though. If we go back into if (a != null) {
a.b.c().d.e;
} That seems perfectly normal to me (ignoring not doing anything with the expression in the if (a == null) {
throw new Error('why?');
} else {
a.b.c().d.e
} I like @Kovensky's example too. In this code, only |
I do see the logic for your approach. I think that having "add parens" (which is indistinguishable from "store in a var, and reference the var in the next statement") drastically change the behavior/meaning is very problematic. |
In the first transformation implementation, in the context of a optional function call I used This avoid an IfStatement and matches more to the spec (IMO) since i'm propagating the The |
Oh, I think I get what you're thinking now: a?.b.c().d.e
// Option 2 interpretation
((a == null) ? void 0 : a.b.c)().d.e Translating this back into statements: let c;
if (a != null)
c = a.b.c;
}
c().d.e Which again, I don't think people would actually write. Translating it into an optional call, we get something like: a?.b.c?.().d.e
// Option 2 interpretation
(((a == null ? void 0 : c = a.b.c) == null) ? void 0 : c().d.e
// statements
let c;
if (a != null) {
c = a.b.c;
}
if (c != null) {
c().d.e;
} Which is ok, I guess. But man, it's gonna be slow (need a Trying to default with this interpretation kinda sucks, too: // Option 2 interpretation
// With "always" call
(a?.b || { c: () => ({d: { e: "default" } }) }).c().d.e
// With optional call
(a?.b.c?.() || {d: { e: "default" } }).d.e // because `c == null ? void 0 : c()`
// statements
let c, ret;
if (a != null) {
c = a.b.c;
}
if (c == null) {
ret = {d: { e: "default" } };
} else {
ret = c() || {d: { e: "default" } };
}
ret.d.e I really dislike these default approaches because it duplicates so much of the chain. The only way out it to use 3 a?.b.c?.()?.d.e || "default"
// statements
let c, ret;
if (a != null) {
c = a.b.c;
}
if (c != null) {
ret = c();
}
if (ret == null) {
"default"
} else {
ret.d.e || "default"
} Instead, option 1's seems much simpler: // Option 1 interpretation
// With "always" call
a?.b.c().d.e || "default"
// statements
if (a == null) {
"default"
} else {
a.b.c().d.e || "default"
}
// With optional call
a?.b.c?.().d.e || "default"
// statements
let c;
if (a != null) {
c = a.b.c;
}
if (c == null) {
"default"
} else {
c().d.e || "default"
} Sorry if taking over the entire conversation here. I'm really just trying to think through all the possibilities. |
Yes that's why I used b.?().e Assume ((b == null) ? Function : b)().e
But the discussion is more about the syntax and the expected behavior, not about how effectively transform a function call. |
Calling (Also, I think CSP will error out in invoking the Function constructor) |
I'm curious; what is unexpected about In case it's at all helpful, I was not able to find any signs of confusion about this aspect of the feature in CoffeeScript on stackoverflow or github; it might be useful if others could find this (there are plenty of questions about the feature, just none about this aspect). C# also short-circuits, and I was similarly unable to find signs of confusion/surprise. Are there any signs of "Option 1" behavior causing problems in the CoffeeScript or C# communities? If not, are there reasons it would cause problems in EcmaScript? |
In terms of |
@rattrayalex it's that putting arbitrary parens around any part of a chain doesn't change its meaning currently (afaik); this proposal changes that. |
Can you explain further? Where would arbitrary parenthesis change the meaning? |
@jridgewell see @xixixao's comments on #3. I think it's an interesting argument. Personally that seems totally fine to me, as parens have always been available in chains but were never useful, but their addition only does what one would expect given their usage elsewhere in the language (and in virtually all programming languages). But I see where @ljharb is coming from (thanks for the quick, concise explanation!). Hopefully I didn't butcher his reasoning. |
I don't believe Checking coffeescript, they definitely treat it differently. But C# follows what I wrote above If you wanted to accomplish precedence like this, wouldn't you have to do it like: (0, a?.b).c |
Another consistency issue is that |
That' more #3's discussion, |
Should we close this in favor of #3 ? |
I think this is a separate discussion that just has to wait for #3. If we decide |
Personally, I would've expected the chaining/short-circuiting semantics to apply to either both or neither. The midpoint feels odd to me, since it's pretty common to have a chain of property accesses and method calls. |
To answer the original question. Concerning what is specified on claudepache/es-optional-chaining:
I presume you wanted to ask “if
If Concerning short-circuiting: Often, the distinction between property access and method call is not semantically relevant as in |
Note also the notion of Nil is a spec artefact and is not how a user should think the feature. The semantics as I envision is basically:
(“Object construction”, and “tagged template” are here because they are at the same level of precedence in the spec, and they may be assimilated to method/function invocations.) |
With #20, there will be no more Nil reference, so that the title of the issue will be obsolete. I'm closing this issue in favour of #10. As a side note, in |
Initial discussion from Babel's Slack
From claudepache/es-optional-chaining:
In this example what would be the ouput if
b
isnull
?I expect this to throw but as far as I understand
Nil
will be propagated andb
will not be called.In the latter case we're using the optional chaining syntax so that the function call
b()
is conditionalWhat is the difference between the two examples?
What do you think about that?
The text was updated successfully, but these errors were encountered: