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

Proposal: Drop optional call expressions #59

Closed
jridgewell opened this issue Apr 1, 2018 · 53 comments
Closed

Proposal: Drop optional call expressions #59

jridgewell opened this issue Apr 1, 2018 · 53 comments

Comments

@jridgewell
Copy link
Member

One of the biggest blockers for the "optimal" syntax ?. is the optional chain call ?.(. I hate it, and a few other delegates agree.

I don't, however, share the requirement that all three operators share the same base (the whole ??. operators). I think ?.[ is ugly, but it still means property access to me. Even in deep chaining sequences, it still makes sense to parse (compare this with my rant against ?.(:

obj.func?.[arg];

obj.func?.[arg].property;

obj.func?.[arg.property];

It's obviously not great, but it's not a total deal breaker for me.

So this leads to my proposal. Why not just drop the optional call entirely (to be clear, not the short-circuiting of method calls, just the ?.( optional call)? I don't have any major use cases for it.

@ljharb
Copy link
Member

ljharb commented Apr 1, 2018

If we do ?.[, we'd have to do ?..; argument the consistency only can go away if we drop the 3 operators down to 1 (and it's hopefully been firmly established that both dot and bracket access needs to exist)

(I'm neutral on keeping vs dropping optional call, but I don't think dropping optional call solves any of the issues you're mentioning)

@jridgewell
Copy link
Member Author

If we do ?.[, we'd have to do ?..

I don't, however, share the requirement that all three operators share the same base

To be clear, I don't think it's a hard requirement that any of the operators share the base. I just can't stand ?.(. If we drop that, I'm ok with ?. and ?.[, and the community seems to poll in that direction as well.

@ljharb
Copy link
Member

ljharb commented Apr 1, 2018

I understand; but i, at least, do consider it a hard requirement.

@claudepache
Copy link
Collaborator

claudepache commented Apr 1, 2018

Optional call has significant use cases (~12% (= 576/4627) of all soak operations in the sample of CoffeeScript code from #17), and should not be dropped because the syntax looks ugly.

Also, if we sidestep the issue by not implementing that case now, and it becomes evident later that we really want it, we will have a more constraint choice in the future due to what is already in place. We’d better solve the syntax issue now.

@jridgewell
Copy link
Member Author

jridgewell commented Apr 2, 2018

What are the actual uses in coffee script code? Right now, I think iterator.return?.() is the best example (optionally implemented methods), but I wouldn’t be sad if we didn’t support it.

Why not think of this as an separate operator? It already has drastically different semantics than optional property access.

@rattrayalex
Copy link

rattrayalex commented Apr 2, 2018

One more modern usage pattern might be:

const MyComponent = ({onClick}: {onClick?: Function}) => (
  <div onClick={() => {
     someSideEffect();
     onClick && onClick(); // onClick?.();
  }}/>
)

(Personally I agree it is less appealing, semantically different, generally weird/unnecessary, and best found in another proposal, but thought I should share that example).

@ljharb
Copy link
Member

ljharb commented Apr 3, 2018

@jridgewell even if this was a separate operator, the consistency argument still applies to dot and bracket access, so dropping or separating optional call has no impact on the operator choice of optional member lookup imo.

@claudepache
Copy link
Collaborator

Why not think of this as an separate operator? It already has drastically different semantics than optional property access.

Note that, although we speak somewhat abusively of optional property access and optional call, it is in fact an entire chain (of property accesses and function/method calls) that is optional. This is the case in order to have short-circuiting semantics that is both most useful and syntactically determined (#20). From that perspective, it has by no way ”drastically different semantics”: the semantics of an ”optional chain” is the same whether it contains a function or method call at its beginning, or in its middle, or nowhere, and the semantics of a function call is the same whether it appears at the beginning of an optional chain, in the middle of it, or outside of it.

The reason that the three cases (a.b, a[b], and a(b)) are in the same proposal, is simply that they are technically related, as they are precisely the three operators in a so-called LeftHandSideExpression for which there are non-anecdotal real-world uses (per aformentioned CoffeeScript stats) both at the beginning and in the middle of an optional chain.


Now to the point: Splitting the proposal in two won’t help to resolve the syntax issue... at worst, it could worsen it (a?.b, a?.[b] and a?>(b), so much for consistency...) Unless maybe you have strong arguments to definitively drop optional call... But even in that case, the ?.[ syntax is still somewhat odd (more or less according to you sensibility), and the argument ”let’s use a?.b as in <languages X and Y>” is still considerably weakened by the fact that a?.[b] resembles to nothing found in other languages (so that JS is inconsistent with itself and with other languages, in addition to be incomplete because it lacks optional call, yesh!)

@saschanaz
Copy link

Out of curiousity, would you still hate optional call expressions if #48 changes it to obj.func??(arg.property)?

@michaeljota
Copy link

Like I said in #61, I personally don't see any value in optional calling if the operator won't check if the property is callable. This could end up with something like foo?.bar?.() and coders thinking this is a safe way to call a function, when bar could perfectly be a number.

This is discussed in #2 as well, and I think is a valid concern to drop optional function calling from this proposal, because this only check if an object is undefined or null.

@claudepache
Copy link
Collaborator

Like I said in #61, I personally don't see any value in optional calling if the operator won't check if the property is callable. This could end up with something like foo?.bar?.() and coders thinking this is a safe way to call a function, when bar could perfectly be a number.

I consider that having a non-nullish non-function where an optional function is expected is most probably a bug, and it is not worth to deviate from the general ”non-nullish” semantics (common to optional-chaining and null-coalescing) in order to support that specific dubious case.

In general, by design, for the sake of not making bugs more difficult to detect, optional chaining is not about a broad ”suppressing exception” but a precise ”checking for nullish”.

But if you disagree, i.e. if you think that f?.() should really check typeof f === "function" instead of f != null, please open an issue against that specific design choice.

@michaeljota
Copy link

michaeljota commented May 3, 2018

What I said, is either the proposal checks that is a function, that I personally don't think it should, or the proposal don't support for a callable operator, meaning that you can't optionally call a function, because you have no actual way to know if what you are trying to call is a function. The latter I think would be better.

So I think we both are saying the same thing at the end. 👍

@andrewmiller1
Copy link

Why not the following?

Error.captureStackTrace(this, CustomError)?

@claudepache
Copy link
Collaborator

@andrewmiller1

Why not the following?

Error.captureStackTrace(this, CustomError)?

What is the semantics of that line?

@andrewmiller1
Copy link

If class Error has object key / method 'captureStackTrace', Error.captureStackTrace(this, CustomError) is called. The main use case I see this is for calling functions that can exist, but the rest of the code isn't depended upon nor is it an error if the routine doesn't exist. In this case, logging as much as a browser supports.

This example comes from the Custom Error Type https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#Custom_Error_Types

Personally I'd like to use such functionality with yield for generating standard checklists without being restrictive. I can get right into testing a the full framework even if I only implemented 1 out of the 25 things in it.

function* buildSteps(metaBuildList) {
  yield metaBuildList.startMessage()?
  yield metaBuildList.printList()?
  /*
  Some builders implement to make package.json from package.json5, and also
  may have a personal project structure that needs to put some config files in
  the root while tools need them there
  */
  yield metaBuildList.provisionRequiredFiles()?
  // Some builders are used in environments without symlinks
  yield metaBuildList.mimicSymlinks()?
  // Some builders may not implement as they're static
  yield metaBuildList.updateDependencies()?
  yield metaBuildList.lint()?
  /*
  Some builders may need to run through their checklist before all build
  features are implemented
  */
  yield metaBuildList.deploy()?
}

Of course my own ulterior motive is to organize my design by contracts code in manageable, succinct lists.

I think the above is easy to understand and brings functionality that would traditionally be verbose.

Hopefully this makes sense.

@andrewmiller1
Copy link

andrewmiller1 commented Sep 3, 2018

Also I didn't say this, but I mean to include optional chaining as well: metaBuildList.printList()?.noColor(). However, I'll point out I was a bit confused as to why the period wasn't at the end like it usually is when chaining calls.

Edit: I figured that part out. I propose to use the above as I expected. Then do metaBuildList.printList()??.noColor() if you want to always do metaBuildList.printList(), but only chain if you get a value. Or something of the like. But I do agree with @jridgewell that putting the ?. in between the property and calling it is awkward each time it's typed.

Edit 2: After running through how to not leave out any functionality, I'll have to let go of a simple question mark at the end as the operator for "check for property and call":

property? checks for property
property?() checks for property and calls
property()? calls then checks for value
property?()? checks for property, calls, then checks for value

Same pattern for arrays. Chains should simply go at the end of the question marks.

I honestly can't think of another variation without it getting complicated

@claudepache
Copy link
Collaborator

If class Error has object key / method 'captureStackTrace', Error.captureStackTrace(this, CustomError) is called...

Ok; this is exactly what ”optional call” is intended for:

Error.captureStackTrace?.(this, CustomError)

Note that the ”optional-chaining operator” ?. is placed just after the expression which is expected to be sometimes nullish.

@andrewmiller1
Copy link

andrewmiller1 commented Sep 3, 2018

I see now. The period in between the property and calling the property gives a notion they're detached. Is it audacious to ask developers get syntactic sugar to imply that

Error.captureStackTrace?.(this, CustomError)

and

Error.captureStackTrace?(this, CustomError)

are the same?

Edit: Also, I'm curious if the period is ever needed if a question mark is used. Could it be implied in all cases?

@ljharb
Copy link
Member

ljharb commented Sep 3, 2018

No, because of ternary operators.

@andrewmiller1
Copy link

Do ternary operators include at least 1 space on either side of the question mark? I'm thinking since optional chaining operators require to hug their subject, could it be possible optional chaining without a period is still incompatible?

@ljharb
Copy link
Member

ljharb commented Sep 3, 2018

No spaces are required around the ? or the : in ternary operators. ? isn’t an option.

@andrewmiller1
Copy link

Ah interesting. Thank you for answering my questions :)

@lehni
Copy link

lehni commented Sep 3, 2018

@andrewmiller1 the complexities of supporting ?(), similar for example to what C# does, have been discussed in length before. Look for lookahead or parse in the issues to see a few cases, e.g.:
#52 (comment)

@andrewmiller1
Copy link

That provides a lot of insight into the issue. Thank you @lehni

@zenparsing
Copy link
Member

I completely agree with the idea of dropping optional call.

@caub
Copy link

caub commented Oct 3, 2018

I agree than the dot in obj.fn?.(arg1) is really confusing with object accessors (when there's a single argument above all).

obj.fn??(arg1) is more clear (I think it'd be ok, even if it lacks consistency with the rest of the proposal), but it conflicts with https://github.com/tc39/proposal-nullish-coalescing (which I find less useful than this proposal though)
or dropping completely those optional call expressions would still be better than introducing confusing syntax

@Jokero
Copy link

Jokero commented Oct 3, 2018

@caub why ??. For me just ? is enough:

obj.fn?(arg1)

@noppa
Copy link

noppa commented Oct 3, 2018

@Jokero That's not an option because of parsing issues.
See the first item in FAQ and a few comments above in this thread.

@Zarel
Copy link

Zarel commented Oct 31, 2018

@jridgewell I can't seem to find your rant against ?.( in your link. Am I missing something, or was it deleted?

@jridgewell
Copy link
Member Author

It's still there, but there are so many responses that it may not load:


#34 (comment)

I feel like I need to register my distaste for ?.( (optional call expression) again. It's absolutely hideous.

// Ick.
obj.func?.(arg);

// Why the hell are there parenthesis in my member expressions?
obj.func?.(arg).property;

// @#$*
obj.func?.(arg.property);

The first example is acceptable, but the second (a member expression chained onto an optional call) is just the most confusing syntax. I literally parse it as obj.func?.arg.property with some random parenthesis in the chain.

.......

@caub
Copy link

caub commented Oct 31, 2018

technically won't you rather need to do:

obj.func?.(arg)?.property

since you don't know if func exists. Or maybe I'm wrong and this would still work

const obj = {}, arg;
obj.func?.(arg).property

@jridgewell
Copy link
Member Author

Not necessarily, the return value of the function may be known to be an object.

@lehni
Copy link

lehni commented Nov 1, 2018

@jridgewell hypothetically speaking, to find out if you're against the operator altogether, or its current syntactic form, how would you feel about this, assuming a language where ?., ?[ and ?( could coexist happily:

obj.func?(arg);
obj.func?(arg).property;
obj.func?(arg.property);

I feel that here the potential confusion you talk about is not an issue, as ?( is more easily perceived as one operator, where ?.( is not, once it's chained together with further property access after.

This then raises the questions:

  • Is the notion of optional chaining of function calls wrong altogether, or:
  • Are we looking at it currently through the lens of a sub-ideal syntax, and judging it by that?

@claudepache
Copy link
Collaborator

@jridgewell
I feel like I need to register my distaste for ?.( (optional call expression) again. It's absolutely hideous.

Personally, I am more sensitive to the elegance of the algorithm than of the surface syntax. Without optional call, you may need to use temporary variables, to repeat yourself, to use boilerplate, ...

Naturally, it is different if the syntax is confusing (not just bad-looking); although I doubt that it is much more confusing than a?.[b].

@dwelle
Copy link

dwelle commented Nov 1, 2018

What's the argument for dropping call expressions? I hear mainly that there's supposedly no use-case or that it's "ugly".

So what's the fear? Is it that it's gonna sit in spec, unused, or that people will find it useful (e.g. in our codebase we have dozens of _.invoke() calls, so we would...) and start using it en masse, making code unreadable for others?

It can't be both. If it's the latter, then this issue should really be about "let's make the syntax prettier". But, as @claudepache, if the decision is between "ugly" syntax, and no syntax (i.e. dropping it), then I lean towards "ugly".

WTBS, current short-circuiting implem will make it less useful than e.g. the above mentioned _.invoke() so we might end up either mixing _.invoke() calls with optional-chaining, or keeping to _invoke altogether, so ¯\_(ツ)_/¯ (but this isn't an argument against optional calls).

@jridgewell
Copy link
Member Author

I feel that here the potential confusion you talk about is not an issue, as ?( is more easily perceived as one operator, where ?.( is not, once it's chained together with further property access after.

I agree. If we didn’t have the . I wouldn’t feel so strongly. I do have minor reservations about the semantics, because optional method invocation is different than optional access.

it is different if the syntax is confusing (not just bad-looking); although I doubt that it is much more confusing than a?.[b].

I disagree, I think it’s much more confusing than the optional bracket symbol.

@Mouvedia
Copy link

Mouvedia commented Nov 2, 2018

cross-comment


I think that we only have 2 viable path left:

  1. inefficient parsing (e.g. A pragmatic approach #52)
  2. concede that only ?. will be unanimously accepted

TC39 recently voiced that ?. cannot be introduced without its pendant—let's call it ?[] for now—which means 2 is not an option. It's totally understandable but Id rather have ?. than nothing.
If the ruling is definitive, we are left with 1; are there any precedents of dangling branches (parallel parsing) in ECMAScript i.e. momentarily splitting the parse tree?

@j-f1
Copy link

j-f1 commented Jan 6, 2019

There are async arrow functions, where the parser has to differentiate between async(foo); and async (foo) => bar;.

@littledan
Copy link
Member

I believe there was some discussion about this issue at a breakout session at the November 2018 TC39 meeting. Does anyone have notes or a summary from that discussion?

@dusave
Copy link
Member

dusave commented Jan 6, 2019

We had a very productive breakout session where we discussed all parts of this proposal. After looking at it from many angles, we reached a consensus that the proposal as is is the best path forward. With one pending opinion, I’ve been assured support for Stage 2 in March.

@rjgotten
Copy link

rjgotten commented Aug 6, 2019

Just tossing this out there.

If the ?.( syntax for optional calls is considered ugly, and there are concerns about muddying the operator's meaning, wrt checking for nullish-ness vs checking for callability, it makes a lot of sense to scrap the call syntax and instruct people to solve obj.fn?.() instead as obj.fn?.call(null).

That's nice and explicit and doesn't muddy the waters. And there's precedent, as this exactly follows what C# does to handle potentially null delegates: someDelegate?.Invoke()

And if using call(null, <...>) is considered ugly, then perhaps another proposal is in order to extend Function with a method that has "call with standard context" semantics. (Maybe even call it invoke...)

@ljharb
Copy link
Member

ljharb commented Aug 6, 2019

That would mean that delete Function.prototype.call would break the use case.

@claudepache
Copy link
Collaborator

claudepache commented Aug 6, 2019

and instruct people to solve obj.fn?.() instead as obj.fn?.call(null)

No, because obj.fn?.() is supposed to be equivalent to obj.fn?.call(obj), which does something else than obj.fn?.call(null).

@caub
Copy link

caub commented Aug 6, 2019

he surely meant obj.fn?.(...args) written as obj.fn?.call(null, ...args)

I think it's a good idea, because most of the added value is in optional property chaining anyway, not really optional calls, because a consumer of an API is supposed to know whether a property is a function or not, if it exists

and also because I believe in https://github.com/Agoric/proposal-infix-bang#infix-bang coupled with this proposal, and by reducing the syntax overhead here, it can help tc39/proposal-wavy-dot#8

@claudepache
Copy link
Collaborator

claudepache commented Aug 6, 2019

he surely meant obj.fn?.(...args) written as obj.fn?.call(null, ...args)

Again, obj.fn?.(...args) is equivalent to obj.fn?.call(obj, ...args), which does something markedly different from obj.fn?.call(null, ...args). A big issue is that you have to repeat obj, and one of the purpose of optional chaining is precisely to avoid to repeat oneself.

@michaeljota
Copy link

I don't think this is going to change, as this is now stage 3. But, the

obj.fn?.call?.(obj, ...args)

seems fine to me.

@caub
Copy link

caub commented Aug 6, 2019

Right, ok, I still don't see many concrete usecases for ?.(), apart from something like render props in react, maybe, and it's not really clearer

<div>{children?.(data) || React.cloneElement(children, data)}</div>
// vs
<div>{typeof children === 'children' ? children(data) : React.cloneElement(children, data)}</div>

that's just my reserve, but this proposal for the true optional chaining is excellent indeed

@obedm503
Copy link

obedm503 commented Aug 6, 2019

@michaeljota from the TC39 process

Indicate that further refinement will require feedback from implementations and users

seems like there's still time

@rjgotten
Copy link

rjgotten commented Aug 6, 2019

he surely meant obj.fn?.(...args) written as obj.fn?.call(null, ...args)

That's exactly what I meant, yes.
Reading back now, that also seems not entirely 100% clear. Apologies.

And indeed, what I forgot was that:

A big issue is that you have to repeat obj, and one of the purpose of optional chaining is precisely to avoid to repeat oneself.

Hence the idea for a different proposal that allows invoking a function using the original context, e.g.
obj.fn?.invoke( ...args ) I'm somewhat surprised that this idea hasn't surfaced before.

Also, with a proposal like the infix bang to chain promises you'd already end up with something quite hairy if you want to execute a function returned from a chained promise, if you don't go down the road of an explicit invoker method:

let result = await maybeCreatesFnAsync()!?.(...args);

I'd take

let result = await maybeCreatesFnAsync()!?.invoke(...args);

over that, for instance. Even if it's longer, it's far more readable.

@claudepache
Copy link
Collaborator

Hence the idea for a different proposal that allows invoking a function using the original context, e.g.
obj.fn?.invoke( ...args ) I'm somewhat surprised that this idea hasn't surfaced before.

Currently, obj.fn.invoke(...args) has a well defined meaning, which cannot be equivalent to obj.fn(...args), since the putative invoke method doesn’t get a reference to obj. And, of course, obj.fn?.invoke(...args) suffers from the same limitation. Making .invoke() do what you mean, is a nontrivial change of the language; and since the only real use case of invoke is precisely variations on obj.fn?.invoke(...args), it is more expeditious to just spec
obj.fn?.(...args).

There exists a stalled proposal that includes saving the original context of a method, see: https://github.com/tc39/proposal-bind-operator; but (using the current syntax of that proposal) ::obj.fn?.call(null, ...args) is quite convoluted, and even more when obj is a more complicated expression than just an identifier.

@claudepache
Copy link
Collaborator

Now that we are at Stage 3, we can consider that the semantics are fixed (except for the relatively minor issue of private field access, see #28). In particular, optional call is not dropped.

@jridgewell
Copy link
Member Author

Now that I'm getting used to it, I'm actually finding more places where optional call is nice to have.

const opts = Component.opts && Component.opts() || {};

We have this currently for a Component class that may optional provided a static opts() {}. If it's not defined, we'd like to use a default options object (so there are less if-statements in the consuming code). And the opts may return null. With optional call (and nullish coalescing), it can be written as:

const opts = Component.opts?.() ?? {}

Not the prettiest, but it's nice enough.

@michaeljota
Copy link

@jridgewell You snipped should work just like the first one. This, now outdated, discussion, was about whether or not this operator should check if a property is a function before trying to make a call, but you are not checking if opts is a function or not, just checking if it exists, just like the final operator works.

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