01:08
<Justin Ridgewell>

A single underscore character is the lightest reasonable syntax for signifying the absence of a name. It is commonly used in other languages, such as Scala and Python, for this purpose. A single underscore was, originally, a valid identifier in Java 1.0, but we later reclaimed it for unnamed variables and patterns

01:09
<Justin Ridgewell>
I very much prefer we use _, I think refactoring hazards are pretty simple to fix since it's a lexically scoped name.
04:03
<rbuckton>
java is using _ for void bindings: https://openjdk.org/jeps/456
Everybody uses _ for void bindings. I'd prefer we use it too, but unless ljharb relaxes his position, it's not an option.
04:04
<rbuckton>
I very much prefer we use _, I think refactoring hazards are pretty simple to fix since it's a lexically scoped name.
Agreed, I also wish we could use it for pipeline. We could have even used it for both, since they wouldn't conflict
04:06
<rkirsling>
while I'm typically opposed to chaos, this would be the chaotic move that I'd support
06:32
<ljharb>
i'm pretty sure i'm not the only one who objects to violating TCP
06:34
<ljharb>
https://fanf.livejournal.com/118421.html and https://esdiscuss.org/topic/regarding-tennent-s-language-design-based-on-semantic-principles are good reading on the topic
06:37
<bakkot>
I think you might be the only one who objects in this case
06:40
<ljharb>
sometimes one person objecting means other objectors don't speak up. not violating TCP came up a ton around ES6 and i'd be surprised if in fact nobody else cares about preserving that property.
06:41
<bakkot>
I do not immediately see how using _ for void bindings would violate TCP
06:42
<ljharb>
tbf for TCP i was thinking about pipeline. it's possible that doesn't apply for void bindings, i'll have to take another look
06:43
<ljharb>
so would const { _, ...kept } = whatever make _ be a discarded binding but const _ = whatever._ wouldn't?
06:44
<ljharb>
for example, https://www.npmjs.com/package/minimist (57M weekly downloads) produces an object with a _ property, would you still be able to destructure it as you can now?
06:47
<bakkot>
so there are a variety of ways this could work. the simplest backwards-compatible way would be, no changes to any current programs, but it becomes legal to re-declare _ in the same scope; if you do, any references to _ within that scope are an error (possibly at parse time).
06:48
<bakkot>
this would not distinguish between const { _, ...kept } = whatever and const _ = whatever._ in any way
06:49
<ljharb>
ok so like, const { _ } = whatever; console.log(_) would continue to work, and if i added const { x: _ } = foo after it, it would discard the new binding but also poison use of _ after that?
06:49
<ljharb>
so if i wanted to discard a binding i'd have to rename my existing binding?
06:50
<bakkot>

I would want it to poison all uses anywhere in the scope. so you could write

const { _ } = whatever;
console.log(_)

and

const { _ } = whatever;
const { x: _ } = foo;

but not

const { _ } = whatever;
console.log(_)
const { x: _ } = foo;
06:50
<bakkot>
and yes, it would mean that if you want to use discard bindings, you would need to not name a thing _ in that scope
06:50
<ljharb>
ok so in that last example, adding line 3 would create an error where it previously worked
06:50
<bakkot>
correct
06:50
<ljharb>
and why is that better than const { x: void } = foo which doesn't have that downside?
06:51
<bakkot>
because void is so many more characters, and means a different thing
06:51
<bakkot>
whereas _ is the convention across many programming languages
06:51
<ljharb>
ok, but most JS devs won't have ever used another language.
06:51
<ljharb>
and 4 chars isn't that many more than 1, but sure, technically 4 > 1
06:52
<bakkot>
I dispute that claim and also I think we should still care about consistency with other languages even if it were true
06:52
<ljharb>
and void already means "ignore this thing". it's just that existing uses in JS also mean "… and produce undefined". in TS it literally means "you can't use this thing".
06:54
<ljharb>
iow i totally accept the argument of "_ is what most other langs use", that's empirical; as is "4 chars is more than 1". but i find the latter a weightless argument, and the former an argument only when looking at similar alternatives.
06:55
<rkirsling>
I don't think the average JS app developer is using void 0 so I would say any JS devs who have an association with void as a keyword would be thinking of it as a return type
06:55
<ljharb>
_ is a perfectly reasonable thing to use in a language where it's not a valid identifier, to be clear - precisely because it's short and connotes "nothing"
06:55
<ljharb>
I don't think the average JS app developer is using void 0 so I would say any JS devs who have an association with void as a keyword would be thinking of it as a return type
i agree! in which case (for TS) it means "ignore this thing", and fits even better than the JS usage.
07:10
<rbuckton>
_ is a perfectly reasonable thing to use in a language where it's not a valid identifier, to be clear - precisely because it's short and connotes "nothing"
In almost every language that added discards, _ was already a legal identifier.
07:11
<ljharb>
my experience with scala tells me that conflating what "_" means, specifically, is a bad idea - i think there's 18 meanings of it depending on context? (update: https://stackoverflow.com/a/8001065/632724 has 16)
07:12
<rbuckton>
If we held the "don't repurpose identifiers" position back in es2015, we would not have yield or await. Those are valid because you had to opt in to the new syntax. Every case where we've proposed coopting _ has also been new syntax.
07:13
<ljharb>
i think for those it was because the entire containing function had to use new syntax. this is just adding a line.
07:18
<rbuckton>
For pipeline, it's fairly obvious that the boundary is the |> expression. For discards, it's a little less clear, but we could even go so far as to issue early errors if you reference a _ declared in a scope with more than one declaration of _. It would be immediately obvious and indicate a need to refactor instead of silently picking a _ declared in an outer scope.
08:35
<nicolo-ribaudo>

I would want it to poison all uses anywhere in the scope. so you could write

const { _ } = whatever;
console.log(_)

and

const { _ } = whatever;
const { x: _ } = foo;

but not

const { _ } = whatever;
console.log(_)
const { x: _ } = foo;
Also note that already today adding the third line here makes the code an error, it's not something new
10:41
<rbuckton>
Also note that already today adding the third line here makes the code an error, it's not something new
Not when you use var
11:46
<nicolo-ribaudo>
I would assume we don't change the behaviour of var
11:46
<nicolo-ribaudo>
var already allows assigning to _ as much as I want
11:47
<nicolo-ribaudo>
The problems are let/const and strict function parameters
12:43
<rbuckton>
Using _ as a discard in var would not be a discard. It would not have the safety guarantees we're discussing. If you have existing code that uses a var _ and a reference to _, adding new code with _ as a discard would not result in an early error and would cause that code to reference the wrong value.
14:40
<shu>
https://arxiv.org/pdf/2403.11919.pdf
16:11
<TabAtkins>

From the Java proposal:

We assume that little if any actively-maintained code uses underscore as a variable name.

lol that doesn't apply to JS

16:15
<Jesse>
_ should be safe
16:16
<TabAtkins>
How is _ safe in a page that uses underscore.js?
16:27
<Chris de Almeida>
and lodash, for anyone unfamiliar with underscore
16:27
<TabAtkins>
Ah, and it looks like Java has done the Work to make it safe - Java 8 issued warnings for using _ as an identifier, and 9 made it an error. So the pain is just people who had previously compiled only on Java 7 trying to upgrade to a modern Java.
16:29
<nicolo-ribaudo>
Well Java introduced it through a breaking change — we wouldn't change the meaning of existing valid code, but just relax some errors
16:42
<rbuckton>
_ should be safe
IMO, __ is worse than void. _ makes sense as it's a single character and has precedence in many languages. feels way more arbitrary
16:43
<littledan>
I'm pretty sure __ was joking...
16:50
<Jesse>
it's a single non-ASCII character
16:51
<ptomato>
16:54
<Justin Ridgewell>
How is _ safe in a page that uses underscore.js?
Because existing code would still work.
16:55
<Justin Ridgewell>
We don’t need to make _ invalid, we just need to allow reassignments in new code.
16:55
<littledan>
sure, I guess the principle is that new features should work nicely, when mixed together with existing code
16:55
<littledan>
(when possible)
16:56
<nicolo-ribaudo>
We can allow reassigning to all identifiers, moving existing redeclaration errors to errors on reference access
16:56
<nicolo-ribaudo>
And underscore.js users can use $
16:56
<nicolo-ribaudo>
And jQuery+underscore can use $_
16:57
<Justin Ridgewell>
@bakkot’s idea would work for that. And in cases where it causes a conflict, renaming a local, lexically-scoped variable is trivial and automatable.
16:59
<Anthony Bullard>
Why not something novel like *? I’m sure there’s a good reason
16:59
<littledan>
so there are a variety of ways this could work. the simplest backwards-compatible way would be, no changes to any current programs, but it becomes legal to re-declare _ in the same scope; if you do, any references to _ within that scope are an error (possibly at parse time).
I believe this is what nicolo-ribaudo has been advocating for
16:59
<nicolo-ribaudo>
Yes
16:59
<Anthony Bullard>
I mean *
16:59
<nicolo-ribaudo>
let *= a is already valid
17:00
<Anthony Bullard>
nicolo-ribaudo: ?
17:00
<nicolo-ribaudo>
You are multiplying the let variable by a
17:00
<Anthony Bullard>
Let is a keyword
17:02
<Anthony Bullard>
Am I missing something obvious?
17:02
<iain>
Let is only reserved in strict mode
17:02
<littledan>
sloppy mode
17:02
<littledan>
let didn't used to be a keyword; you can still use it as a variable sometimes
17:03
<littledan>
you can switch into this mode with eval()
17:06
<Anthony Bullard>
I really don’t love void bindings in bare assignments, ie not destructuring
17:06
<littledan>
wouldn't it be weird to distinguish them, though?
17:07
<Anthony Bullard>
Yes but a throwaway binding is meant to throw something away that you have to reference but don’t want to bind in the scope.
17:07
<Anthony Bullard>
But I’ll concede it’s messy
17:08
<TabAtkins>
_ should be safe
Man you really gotta make it obvious when you're doing unicode shenanigans (U+ff3f FULLWIDTH LOW LINE, not ASCII underscore) ^_^
17:10
<Anthony Bullard>
Man you really gotta make it obvious when you're doing unicode shenanigans (U+ff3f FULLWIDTH LOW LINE, not ASCII underscore) ^_^
JavaScript now will begin its APL arc
17:12
<TabAtkins>
This is why CSS took a (technically non-binding) resolution on itself to limit its built-in syntax to the ASCII range, like a decade ago.
17:12
<TabAtkins>
(Non-binding because we can always reverse our own resolutions.)
17:17
<Anthony Bullard>
Honest question: would it be better to change the binding rules for an existing (and widely used) identifier, or using a new piece of syntax for this that only works in strict mode? Is there any precedent for the latter?
17:27
<ljharb>
My preference is still to use a new piece of syntax. void satisfies that and works in sloppy mode too. there's certainly precedent for strict-only syntax as well.
17:29
<bakkot>
Honest question: would it be better to change the binding rules for an existing (and widely used) identifier, or using a new piece of syntax for this that only works in strict mode? Is there any precedent for the latter?
that's a matter of opinion, but given that the former would allow us to be consistent what ~every other language does, it seems like the better option by far
17:32
<Anthony Bullard>
that's a matter of opinion, but given that the former would allow us to be consistent what ~every other language does, it seems like the better option by far
I agree with you, and with ljharb as well, somehow at the same time. I think it depends on what our values here are. Consistency with other languages or minimal impact to existing language semantics
17:54
<bakkot>
I suppose _ has some awkward interactions when used in assignment position
17:56
<Anthony Bullard>
I don’t love the keyword void, but that’s just aesthetic mostly. I’d love to have some piece of syntax that could be used with no ambiguity in strict mode
18:00
<nicolo-ribaudo>
Even in strict mode, * (or other operators) don't work in using declarations
18:00
<nicolo-ribaudo>
using *= x
18:05
<Anthony Bullard>
Even in strict mode, * (or other operators) don't work in using declarations
Damn you’re right. Is there any symbol that would work?
18:09
<rbuckton>
Damn you’re right. Is there any symbol that would work?
Not really. @ and # have alternative semantic meanings (decorators and private names) that make them a bad fit, plus @ and # have already been considered for infix operations in the past, and I'd rather not close off that syntax space just for discards.
18:10
<Anthony Bullard>
What about ?
18:11
<rbuckton>
? is proposed as a special token for partial application, and had already had feedback bout "too much ?" considering conditionals, optional chaining, null coalesce, and null coalesce assignment.
18:12
<Anthony Bullard>
It’s use in partial application is what made my think of it
18:12
<rbuckton>
I've also proposed using ~ as part of partial application since one of the other concerns was the need for a token to opt-in to the behavior, so foo~(1, ?, 2) as a partial application of foo that supplies the first and third argument with a placeholder for the 2nd argument.
18:14
<Anthony Bullard>
I've also proposed using ~ as part of partial application since one of the other concerns was the need for a token to opt-in to the behavior, so foo~(1, ?, 2) as a partial application of foo that supplies the first and third argument with a placeholder for the 2nd argument.
This example showed me maybe partial application is a bad idea in JS, as much as I love it in FP languages
18:14
<rbuckton>
I don't think any other ASCII symbols that are generally available on most keyboards match the semantic behavior of a discard.
18:15
<rbuckton>
This example showed me maybe partial application is a bad idea in JS, as much as I love it in FP languages
That's a fairly weak example. Considering how often it's used in Ramda and other FP libraries, I think its extremely valuable.
18:15
<Anthony Bullard>
We are just running out of concise syntax
18:15
<rbuckton>
We've already run out of concise syntax. One of the blockers for pipeline has been picking a topic token.
18:16
<Anthony Bullard>
We've already run out of concise syntax. One of the blockers for pipeline has been picking a topic token.
Pipeline works best when a language grows around the semantics of either data first or data last matching the operator semantics
18:16
<rbuckton>
I'll admit, I'm still partial to F#-style pipes and papp without the ~, regardless the direction the pipeline proposal took.
18:18
<Anthony Bullard>
? Being a operator that in an expression context meaning “something referred to but not bound” is somewhat elegant on its own
18:18
<rbuckton>
Two of biggest FP-ish libraries use opposite positions (lodash and underscore are data-first, Ramda is data-last). The benefit of papp was that you didn't have to "pick a winner"
18:18
<Anthony Bullard>
Not operator, but syntactic construct
18:19
<rbuckton>
You can't have ? stand on its own in a given expression without some kind of boundary. For papp, I wanted that boundary to be Arguments
18:20
<rbuckton>
So, add(1, ?) would have been fine, but add(1, { option: ? }) would not, because it goes from being an Argument placeholder to being an arbitrary expression.
18:20
<Anthony Bullard>

The boundaries could be:

  • assignment
  • array element in destructuring
  • object value in destructuring
  • arguments in Papp
18:21
<rbuckton>
I'm not sure I understand what you mean by the first three.
18:21
<Anthony Bullard>
Lhs
18:21
<rbuckton>
I mean, what would ? = x even mean?
18:22
<Anthony Bullard>
I’m not sure, but above it was communicated that there is a desire to have a discard token be the same in destructuring and bare assignment
18:22
<Anthony Bullard>
The use case for the latter is unclear to me
18:24
<rbuckton>
For discards, void = x is currently disallowed because it is meaningless.
18:24
<Anthony Bullard>
let ? = X is weird for sure
18:24
<Anthony Bullard>
So drop that
18:25
<Anthony Bullard>
And leave it to array and object destructuring and Papp arguments
18:25
<Anthony Bullard>
Maybe I should be having this discussion in public on the proposal?
18:26
<rbuckton>

Also true. In the proposal it is only present for two reasons:

  1. consistency with using
  2. avoiding comma expressions in cases like const a = x(), b = (y(), z()) (more of a benefit for transpilers than developers)

Both are very weak reasons.

18:26
<rbuckton>
This was discussed in the last plenary.
18:26
<rbuckton>
I just haven't had time to update the proposal explainer with those outcomes yet.
18:27
<rbuckton>
It's also already mentioned here: https://github.com/tc39/proposal-discard-binding/issues/1
18:27
<Anthony Bullard>
This was discussed in the last plenary.
Unfortunately, despite my company hosting it I didn’t have budget to make the last plenary so I’m a little behind
18:33
<Jack Works>
some language repurpose identifiers as special things. that's good, but I don't think we have that tradition in js. by "repurpose identifiers as special things" I mean new things are using slightly different visual feelings in the common use cases. e.g. "var yield = f()" and "yield x + y" don't feel the same. (I know there are cases that are valid in both interpretations like yield[expr] so it's up to syntax parameters) repurposing _ is not what we do to the yield and await. The new use case of _ looks very similar to the old one (old "_.add()" new "_ = x") so I don't think it's a good addition
18:36
<rbuckton>

On the papp/pipeline side, I still think F# pipes and papp would have been far more likely to get to stage 2 as they were general purpose capabilities that dovetailed. F#'s style biggest drawbacks were how to handle yield and await, and a push to allow you to do general operations against the topic (i.e., x |> _ + _). For F#-style await was solvable, and I don't think yield was all that important (i.e., you could still write (yield x |> F) |> G). I also think the overwhelming majority case for pipelining was writing expressions in arrow functions, not immediately on the right side of the pipe:

// most likely use
ar |> filter(?, x => x > 1)
   |> map(?, x => x * 2)

I just don't see y = x |> _ + _ as all that valuable, especially since you can already use , for that today with no new syntax.

var _;
y = (_ = x,
    _ = _ + _);
18:36
<Anthony Bullard>
Definitely, but if we can find a symbol that’s not used in identifiers that in a context would be unambiguous and could be used for discard AND Papp topic token that would be a win in my eyes
18:37
<Anthony Bullard>

On the papp/pipeline side, I still think F# pipes and papp would have been far more likely to get to stage 2 as they were general purpose capabilities that dovetailed. F#'s style biggest drawbacks were how to handle yield and await, and a push to allow you to do general operations against the topic (i.e., x |> _ + _). For F#-style await was solvable, and I don't think yield was all that important (i.e., you could still write (yield x |> F) |> G). I also think the overwhelming majority case for pipelining was writing expressions in arrow functions, not immediately on the right side of the pipe:

// most likely use
ar |> filter(?, x => x > 1)
   |> map(?, x => x * 2)

I just don't see y = x |> _ + _ as all that valuable, especially since you can already use , for that today with no new syntax.

var _;
y = (_ = x,
    _ = _ + _);
You have not encountered enough point free F# people 😂
18:38
<Anthony Bullard>
Though they also are creating their own operators too
18:40
<rbuckton>

I don't think using the same token for discard and papp is a good idea. In the pattern matching space, I've been considering how partial application could allow you to supply arguments to a custom matcher:

match (x) {
  when NodeMatcher~(SyntaxKind.Identifier, ?)({ let pos, let end }): ...;
}

as a way to synthesize an extractor in-place as opposed to

const identifierMatcher = _ => NodeMatcher(SyntaxKind.Identifier, _);
match x {
  when identifierMatcher({ let pos, let end }): ... ;
}
18:41
<rbuckton>
You have not encountered enough point free F# people 😂
There was a tremendous amount of very vocal frustration in the RxJS community that pipeline did not use F# style.
18:42
<rbuckton>

I don't think using the same token for discard and papp is a good idea. In the pattern matching space, I've been considering how partial application could allow you to supply arguments to a custom matcher:

match (x) {
  when NodeMatcher~(SyntaxKind.Identifier, ?)({ let pos, let end }): ...;
}

as a way to synthesize an extractor in-place as opposed to

const identifierMatcher = _ => NodeMatcher(SyntaxKind.Identifier, _);
match x {
  when identifierMatcher({ let pos, let end }): ... ;
}
Yes, it's not pretty, but I'm still thinking about how they would work together.
18:42
<Anthony Bullard>
There was a tremendous amount of very vocal frustration in the RxJS community that pipeline did not use F# style.
Today I learned that RxJS community still exists. No shade, just didn't realize that
18:43
<rbuckton>
It's still very popular and heavily used in React projects. Rx's Observable has already been a proposal to TC39 (which stalled), and is now a proposal in WHATWG
18:44
<bakkot>
(though the WHATWG one is substantially pared down, which is good)
18:44
<Anthony Bullard>

I don't think using the same token for discard and papp is a good idea. In the pattern matching space, I've been considering how partial application could allow you to supply arguments to a custom matcher:

match (x) {
  when NodeMatcher~(SyntaxKind.Identifier, ?)({ let pos, let end }): ...;
}

as a way to synthesize an extractor in-place as opposed to

const identifierMatcher = _ => NodeMatcher(SyntaxKind.Identifier, _);
match x {
  when identifierMatcher({ let pos, let end }): ... ;
}

These are done different, so I don't know if this is a good comparison. I really don't see the difference :

const identifierMatcher = _ => NodeMatcher(SyntaxKind.Identifier, ?);
match x {
  when identifierMatcher({ let pos, let end }): ... ;
}
18:45
<rbuckton>
That would have been const identifierMatcher = NodeMatcher(SyntaxKind.Identifier, ?)
18:45
<Anthony Bullard>
It's still very popular and heavily used in React projects. Rx's Observable has already been a proposal to TC39 (which stalled), and is now a proposal in WHATWG
Give me a standard library before Observables. I'm actually going to start saying that in reference to every new API at this point
18:45
<rbuckton>
the issue is that if ? is both a placeholder and a discard, parsing when F(?) isn't obvious to either the spec or the reader of code.
18:45
<Anthony Bullard>
That would have been const identifierMatcher = NodeMatcher(SyntaxKind.Identifier, ?)
Oh yeah, sure
18:47
<rbuckton>
when F(?)(?): would be a partial application to a destructured void binding. But you don't need to destructure a match, so when F(?) is now ambiguous as to whether I'm matching the subject against a partial application of F or against F's custom matcher.
18:47
<Anthony Bullard>
Yeah, with the expanded destructuring proposals, it does have problems
18:47
<rbuckton>
So discards and papp cannot share a sigil
18:48
<Anthony Bullard>
And papp cannot use _ right, since that is a valid identifier
18:48
<rbuckton>
Yes. Papp definitely can't use _.
18:48
<Anthony Bullard>
In case I want to supply lodash to my partially applied function
18:48
<rbuckton>
a(_) is legal JS, we can't change it's meaning.
18:50
<rbuckton>
whereas const [_, _, x] = ar is not legal thus we are not changing its meaning.
18:53
<rbuckton>
The only place using _ as a discard would change the meaning of legal JS is if it is used with var.
18:53
<Anthony Bullard>
So just to recap this long convo, is ? as a replacement for void in your proposal (and that alone) a live option?
18:53
<rbuckton>
No, I don't believe it is.
18:54
<Anthony Bullard>
No, I don't believe it is.
In the sense of it hasn't been formally proposed, it won't work, or you don't like it ;-)
18:54
<rbuckton>
I also don't believe @, #, or ~ are viable options a well.
18:55
<rbuckton>
It would seriously conflict with an existing proposal at stage 1 and leave that proposal with no viable way forward, whereas we could choose to continue with void or find a way to make _ work and not conflict.
18:55
<Anthony Bullard>
Sorry, what proposal?
18:55
<rbuckton>
Partial application.
18:56
<rbuckton>
I am still planning to come back to partial application, it has just been far down on my list of priorities.
18:58
<rbuckton>
I also don't think ? has a semantic meaning of "discarding something". It generally has a semantic meaning of "asking something": "what do I do if x is truthy?" (conditional), "is this value null or undefined?" (optional chaining, null coalesce), or "what else do you need to tell me?" (partial application)
18:58
<bakkot>
Give me a standard library before Observables. I'm actually going to start saying that in reference to every new API at this point
I don't know what this means. Observables would be part of a standard library.
18:59
<rbuckton>
Discarding things in JS is either through void (as a syntactic feature of the language), or through _ (on its own or as a prefix, by convention).
19:02
<rbuckton>
Observables and cancellation really should have stayed in TC39. They're core language capabilities that are not specific to the web platform, and it's unfortunate the ecosystem had to adopt whole chunks of the DOM into non-DOM runtimes just to support code sharing and interop.
19:03
<rbuckton>
Observables/signals/events/etc. JS could really use a standardized event subscription mechanism, and the closest we got was Promise.
19:04
<Anthony Bullard>
Discarding things in JS is either through void (as a syntactic feature of the language), or through _ (on its own or as a prefix, by convention).
Cool, thanks for the conversation. I learned a lot
19:04
<rbuckton>
And we were quick enough to adopt it before WHATWG moved ahead with Future, otherwise we would not have anywhere near the flexibility of async/await in the language.