14:56
<Chris de Almeida>
meeting start is imminent!
15:02
<Michael Ficarra>
oooh lower latency on the transcription will be great!
15:03
<bakkot>
I have no idea why that would be
15:04
<bakkot>
unless it's like, yesterday google docs was slow, which I could totally believe
15:08
<Chris de Almeida>
it is noticeably better than last meeting
15:08
<Chris de Almeida>
just my observation 🤷
15:10
<Justin Ridgewell>
I don't think I need set.getAndDelete(), but have wanted set.getAndInsert(): boolean a few times
15:11
<Justin Ridgewell>
To both check if it's currently in, and insert it if it's not.
15:11
<Justin Ridgewell>
Mainly to guard if (!set.getAndInsert()) { expensive() }
15:12
<Justin Ridgewell>
Really add() should have returned the current boolean status all along…
15:17
<nicolo-ribaudo>
What if we had a special flag on objects to make them falsish, so that the Map returned by .add() turns into falsish for the first subsequent boolean check if the value was already there 🧠
15:18
<rbuckton>
Really add() should have returned the current boolean status all along…
I end up calling it tryAdd
15:19
<Justin Ridgewell>
That works too
15:20
<bakkot>
just do your side effects in getOrInsertComputed
15:21
<bakkot>
set.getOrInsert(v, () => { expensive(); return newValue })
15:21
<Justin Ridgewell>
That's only Map, right?
15:21
<ljharb>
for take, what's the proposal name at stage 1 (that describes the problem statement)?
15:21
<bakkot>
oh, sure
15:21
<Olivier Flückiger>
nicolo-ribaudo: to support this also for the declaration in conditionals it would have to be passed through assignments though
15:24
<Michael Ficarra>
parseFrozen sounds like I need to pass Object.freeze(Object("{ ... }"))
15:24
<Michael Ficarra>
like somehow JSON.parse will mutate its input
15:24
<Michael Ficarra>
that said, I still think it's better than using an option for this
15:25
<ljharb>
cc Devin Rousso
15:28
<Devin Rousso>
im not sure what you mean? 😅
15:28
<Devin Rousso>
i think getAndDelete was what folks liked
15:28
<ljharb>
that's the method name. but what's the problem it's solving? stage 1 is about a problem statement, not a solution/API
15:29
<Michael Ficarra>
that's also the problem it's solving, which is why we suggested it as the method name
15:29
<Devin Rousso>
oh! gotcha i understand what you're asking
15:29
<Devin Rousso>
it's being able to in a single operation "take" things out of Map
15:30
<Devin Rousso>
i.e. get the value for a given key AND delete the key at the same time
15:30
<ljharb>
i'm looking for something concise to title the proposal in the proposals repo :-)
15:30
<Michael Ficarra>
agh, Arrays still having Array.prototype is unfortunate
15:30
<Devin Rousso>
i described it as "Read a value and remove it in the same step" in the proposal repo
15:31
<Michael Ficarra>
Map read and remove?
15:31
<bakkot>
primitives still have their prototypes too
15:32
<bakkot>
arrays we could conceivably make a FrozenArrayPrototype which is just a copy with all the same methods but frozen + null protot, but we can't do that for primitives
15:33
<ljharb>
SGTM
15:33
<ljharb>
please bounce the proposal repo into tc39-transfer and i'll take care of it :-)
15:33
<Michael Ficarra>
that said, the relevant ES terms (from Map.prototype) are already "get" and "delete", so I would personally go with those
15:37
<Justin Ridgewell>
Didn't we discuss packed arrays recently? As long as the array has no holes, it shouldn't matter that it has a prototype.
15:40
<Olivier Flückiger>
not if you read a named property on the array
15:44
<nicolo-ribaudo>
We could have options presets as static properties of JSON
15:44
<nicolo-ribaudo>
Like, JSON.parse(..., JSON.FROZEN_AND_NULL_PROTO)
15:44
<nicolo-ribaudo>
Well, with a shorter name than JSON.FROZEN_AND_NULL_PROTO
15:44
<nicolo-ribaudo>
Something that's much simpler to type than spelling out the options bag
15:46
<hax (HE Shi-Jun)>
It's look like PHP :P
15:47
<nicolo-ribaudo>
bakkot Right now attributes only support strings, but the idea is that they are easily extendable to support primitives that have literal syntax
15:47
<nicolo-ribaudo>
(the only reason they don't is due to lack of use cases so far)
15:48
<bakkot>
great, yeah
15:50
<hax (HE Shi-Jun)>
I heard dog barking...?
15:51
<nicolo-ribaudo>
It's unfortunately James's dog
15:51
<bakkot>
very loud dogs yes
15:51
<Michael Ficarra>
oh god
15:51
<nicolo-ribaudo>
oh dog
15:56
<James M Snell>
extremely bad timing, sorry about that
15:57
<bakkot>
https://www.jasnell.me/posts/fetch-needs-error-codes
15:58
<eemeli>

Historically, YAML libraries' approach to parse-immutable-ish concerns has been to define something like JSON.parseSafe and ask everyone to use that instead of JSON.parse.

I don't think this is necessarily a good idea, but it's the closest historical precedent to the proposed change.

16:10
<eemeli>
I don't think anyone has supported frozen without a null proto, so it should be fine for the shorter name to be e.g. JSON.Frozen.
16:13
<Ashley Claymore>
yeah, and JSON.parse always creates packed arrays
16:14
<Michael Ficarra>
the reason it matters is that someone will want to call .slice on an array that was parsed from JSON data
16:16
<James M Snell>
https://webidl.spec.whatwg.org/#js-DOMException-specialness
16:16
<Lea Verou>

https://webidl.spec.whatwg.org/#js-DOMException-specialness

In the JavaScript binding, the interface prototype object for DOMException has its [[Prototype]] internal slot set to the intrinsic object %Error.prototype%, as defined in the create an interface prototype object abstract operation. It also has [[ErrorData]] and [[Stack]] slots, like all built-in exceptions.

Additionally, if an implementation gives native Error objects other special powers or nonstandard properties (such as line and column properties), it should also expose those on DOMException objects.

16:19
<James M Snell>
Sorry again about the dogs... but at least they're cute
16:19
<eemeli>
You should take them on a walk.
16:20
<James M Snell>
That one is currently waiting on me to ^
16:24
<hax (HE Shi-Jun)>
I don't like Decl in Cond if (let x = expr) have subtle different semantic with other language like Swift 😪
16:27
<bakkot>
hax (HE Shi-Jun): what difference do you mean?
16:27
<bakkot>
fwiw there's already some subtle differences between languages
16:27
<bakkot>
with basically the same form
16:32
<hax (HE Shi-Jun)>
Swift semantic is like if (let x = expr; x != null) as Decl in Cond.
16:36
<rbuckton>
Rust can differentiate between a type and a variable in its pattern matching. JS can't.
16:36
<Nikolaos Papaspyrou>
A declaration cannot fail; pattern matching may either succeed (and declare) or fail.
16:38
<bakkot>

Olivier Flückiger:

if (using resource = getResource(); resource.needsUpdate) {
  resource.update();
} else {
  await postMessage('no updates required');
}
16:44
<nicolo-ribaudo>

For comparison, a way to write this if the variable was visible in else would be:

let goToElse = false;
{
  using resource = getResource();
  if (resource.needsUpdate) {
    // ...
  } else {
    goToElse = true;
  }
}
if (goToElse) {
  await postMessage('no updates required');
}
16:44
<bakkot>
yeah
16:44
<bakkot>
or with a DisposableStack but that's even worse
16:44
<Olivier Flückiger>
yeah, or early return (depending)
16:47
<hax (HE Shi-Jun)>
They have overlap, for exmple, if (let x = ...) actually are pattern match in other languages like swift/rust/etc.
16:53
<ljharb>
keith_miller: to be fair, it had one presentation and then the champion left tc39, and it's only been reactivated for ~3 years, and 2 years ago it was seeking stage 2 before we got last-minute objections
16:53
<bakkot>

fwiw I strongly want to use this syntax for extractors as well:

if (let Some(x) = val) {
  // only runs if the extractor succeeded
} else {
  // only runs if the extractor failed 
}

I think that's a natural extension

Ron really doesn't like this because he thinks it is not general enough for pattern matching, which I think inclines me against pattern matching rather than against this proposal

16:55
<nicolo-ribaudo>
Thoughts about cancelling the fourth day?
16:55
<Ashley Claymore>
Of every week?
16:55
<nicolo-ribaudo>
Yeah 3 consecutive days of work is already enough
16:56
<Clément Pit-Claudel>
Does Temporal support this?
17:01
<Lea Verou>

Just so I understand, we're debating whether this will be TDZ or a reference error? Because I think it is perfectly meaningful from the author side and I can see use cases for it. Could it be a mistake? Sure. But so could many other things. That's why we have linters.

``js
let data = whatever();
if (let data = foo()) { ... }
else { console.log(data); }


IMO making this an error or TDZ is completely baffling and inconsistent with how scoping works anywhere else. TDZ implies uninitialized but same scope. This is _not_ the same scope.
17:01
<nicolo-ribaudo>
I think we had a ReferenceError for foo() = 1 and then converted it to SyntaxError
17:02
<Michael Ficarra>
why isn't the queue being advanced at all?
17:03
<Michael Ficarra>
oooh, that may be right
17:03
<bakkot>
https://github.com/tc39/ecma262/pull/1527
17:04
<Michael Ficarra>
ooh thanks for the link @bakkot
17:04
<Michael Ficarra>
I have no memory of that PR at all
17:05
<bakkot>
fwiw I kind of agree but I also kind of think that the code you wrote is baffling too. if people don't try to write that very confusing code then they never run into this proposed error and don't have to learn about it
17:05
<bakkot>
but I would also, personally, leave this up to linters
17:06
<Justin Ridgewell>
Devin Rousso: Did you want a continuation to ask for Stage 2?
17:06
<bakkot>
Ron had an objection to stage 2
17:06
<bakkot>
so I don't think there's any point
17:06
<Michael Ficarra>
at some point, I would like to rant about this committee's relationship with linters
17:06
<Jack Works>

fwiw I strongly want to use this syntax for extractors as well:

if (let Some(x) = val) {
  // only runs if the extractor succeeded
} else {
  // only runs if the extractor failed 
}

I think that's a natural extension

Ron really doesn't like this because he thinks it is not general enough for pattern matching, which I think inclines me against pattern matching rather than against this proposal

pattern matching gives you precised control over if a property should be matched or should be a declaration, so it may look like

if (val is Some({ match: { x: Some(1) }, let binding })) {}
17:07
<Devin Rousso>
i get the sense i won't get stage 2 due to the pattern matching objection so as much as i'd like to probably not ☹️
17:07
<Ashley Claymore>
If something is ambiguous in the language I'd rather it wasn't allowed than I hope I never see it and have to think about it because I'll always get to work on teams that have a lint rule that bans it
17:07
<bakkot>
right, I get why you want that, but that syntax is so much worse
17:08
<Lea Verou>

I agree, but it's no more or less confusing than

let foo = 1;
{
  let foo = 2;
}
{
  console.log(foo); // logs 1
}

which works just fine today.

17:08
<Michael Ficarra>
let them cook
17:08
<bakkot>
Lea Verou I generally think we should move away from "this existing thing is already confusing so it's ok to have a new confusing thing" as a justification
17:09
<bakkot>
see also "stop coercing things"
17:09
<Lea Verou>
Yes and no. Breaking internal consistency needs a stronger justification, and IMO it's less clear that this exists here. (re: https://www.w3.org/TR/design-principles/#consistency )
17:09
<bakkot>
fwiw I don't agree: I think that is very clear, in a way your earlier snippet is not
17:09
<Justin Ridgewell>
Agreed.
17:10
<Devin Rousso>
and see my argument is that that syntax is SO different that there's no overlap (i.e. even if the semantics of declarations in conditionals imply one thing then the different syntax of that can imply another)
17:10
<Richard Gibson>
but it is more confusing, because the if binding isn't obviously out of scope inside else
17:10
<Justin Ridgewell>
I think if we want the binding to only be visible inside the If body, then the If is creating its own lexical scope in its body.
17:10
<Justin Ridgewell>
Either it's outside the If body and visible in the Else, or it's inside and not.
17:12
<bakkot>
it's inside and not visible, but that doesn't mean it's not confusing to see code which is written as if it is visible
17:12
<Justin Ridgewell>
Having it poison the else's reference to parent scopes seems wrong.
17:12
<Lea Verou>
Unless I'm misunderstanding your point, it seems that would argue for making the binding available in else, which I think is also defensible.
17:13
<Ashley Claymore>
The very fact that this committee think both being accessible and not accessible are both acceptable semantics proves that it is ambiguious. A way to avoid someone making the wrong assumption is to make the ambiguous case an early error
17:14
<Richard Gibson>
I think there are reasonable intuitions in both directions regarding visibility in else, which are clearly resolved by making references there invalid
17:14
<Justin Ridgewell>
It's only ambiguous because we want a special disposal time for using. The least confusing answer is it's visible with else.
17:15
<Olivier Flückiger>
it could also be an argument against the proposal if we cannot agree
17:15
<Lea Verou>
That makes sense, as a temporary thing: make them an error now, which buys us time to figure out what the best behavior is without compat issues getting in the way. We do that a lot in CSS, wasn't sure if it's a thing here.
17:15
<Ashley Claymore>
that is also true
17:15
<Olivier Flückiger>
I think this kinda goes back to an earlier mistake that was made, hoisting let up to the last block
17:16
<Ashley Claymore>
No, it is ambiguous. Because we have in these calls looked at the exact same code and assumed the value will be different things
17:16
<bakkot>
this is just not my intuition at all fwiw, even without using I think having it visible in the else would be more confusing/surprising (to me)
17:16
<Richard Gibson>
using motivates scoping to just the if, but conflicting intuitions would be there even without such motivation
17:17
<bakkot>
and yes I know it's visible in C++ (and Go, I'd forgotten) but those are... not languages I associate with non-confusing design decisions
17:17
<bakkot>
I actually hadn't known it was visible in C++ before looking it up, despite having used it > 100 times
17:17
<Lea Verou>
I wonder if we could reach a decision that nobody actively objects to, even if we don't agree with it
17:17
<Olivier Flückiger>
sorry but if let would start a scope in JS, then it would be less confusting. all the cases where it's not available in else are basically languages where let starts a scope and/or the if is a pattern match
17:18
<Olivier Flückiger>
in JS a scope is started by { exclusively.
17:18
<Olivier Flückiger>
in some cases like loops, we have sort of an imaginary { around the statement
17:18
<bakkot>
I just do not see this super clear distinction between "a pattern match" and "an if"
17:18
<Olivier Flückiger>
the only reasonable extension for the if/else statement is to have an invisible { around it
17:18
<bakkot>
it's still just an if
17:19
<Ashley Claymore>
for (let i = 0; ...
17:19
<bakkot>
loops are considerably more magic than that
17:19
<Olivier Flückiger>
a failing pattern match creates nothing. a declaration which assigns something which is not true, still creates a binding.
17:19
<bakkot>
but that's OK
17:20
<Olivier Flückiger>
I think the fact that we agree that we need to prevent people from using it wrongly hints at the fact that its maybe not the version we want. the design space is bigger...
17:21
<Lea Verou>
Yes, if the binding is available in else, the mental model is "there is an invisible scope here that encompasses the if() {} and the else {}". It's a little weird, but defensible. But I cannot see any reasonable mental model where it's TDZ. It breaks the user's mental model of what TDZ means.
17:21
<bakkot>
ok, so an if (let x = foo) is just a special kind of pattern matching where it's considered to fail if it's falsy, whatever?
17:22
<Richard Gibson>
I don't think anyone is still arguing for TDZ—early error has been deemed preferable
17:22
<Lea Verou>
Ironically, that makes the error option more defensible. :/
17:22
<Jack Works>
and see my argument is that that syntax is SO different that there's no overlap (i.e. even if the semantics of declarations in conditionals imply one thing then the different syntax of that can imply another)
I worry if decl in if lands, PM may be required to not introduce is expression to switch to PM semantics, and forced to align with declaration in if condition, which requires the pattern be written in if (let _here_ = expr).
Since it prefixed with let, it must completely compatible with destruction semantics, and push PM back to 2022 semantics (create binding by default).
2022 version was presented and proved to a non-starter
17:22
<bakkot>
the purpose of an error is to avoid users having to form a mental model of it, because they will never read code where the question could arise in the first place
17:23
<bakkot>
in the same way you simply do not need a mental model of precedence for -2**2
17:25
<Olivier Flückiger>
it's not a pattern match. that's why we can't have if (let [a,b] = foo()) { in this proposal. Because we don't ahve a notion of this not matching.
17:25
<Richard Gibson>
interestingly, it's somewhat analogous to how use strict in a function body is not compatible with non-simple parameters (which precede the body)
17:26
<Lea Verou>
Has anyone asked actual developers what their mental model is and how they'd expect this to work? That would be useful data to get. None of us is completely representative of the average developer, so we shouldn't be designing this based on our own mental model. E.g. it may turn out that 99% of them expect it to be available. Or to just access the one from the parent scope. Or they may genuinely expect different things, in which case, error it is.
17:27
<bakkot>
I don't know what "it's not a pattern match" means. You said "a pattern match" is a thing where a failing match doesn't create a binding. ok, great, if (let x = y) doesn't create a binding if y is falsy. There are additional syntactic forms that you can't write but I don't see why that changes how if (let x = y) is interpreted
17:28
<Richard Gibson>
haven't we already definitively established "they may genuinely expect different things" without even looking outside this group?
17:28
<bakkot>
incidentally I would be totally fine with making if (let [a, b] = foo()) legal, and making it take the else in the case that foo() is null (rather than throwing as it currently does).
17:28
<bakkot>
(but not in the MVP)
17:29
<Lea Verou>
It doesn't follow that because folks in this group have diverging mental models, regular developers at large will also have diverging mental models. It's likely, but not so likely that getting actual user data is pointless.
17:31
<peetk>
does anyone a) strongly prefer binding in else and also b) really like and want this proposal? if so why?
17:32
<Richard Gibson>
I don't think collecting user data on this question is pointless, but I do think it is unnecessary. This group is a subset of the larger one, so we already know that the latter contains differing intuitions, even if they are (currently) wholly confined to our subset.
17:33
<Devin Rousso>
that's a lot of ifs. and i don't get why declarations in conditionals would require pattern matching to drop is. why can't both exist?
17:35
<Justin Ridgewell>

I don't think any user is really expecting this tranpsilation:

// In
if (using x = foo) {
  console.log(x);
} else {
  console.log('else')
}

// Out
outer: {
  {
    let x = foo;
    if (x) {
      console.log(x);
      break outer;
    }
  }
  console.log('else')
}
17:35
<bakkot>
I am expecting that transpilation
17:36
<Jack Works>
that's a lot of ifs. and i don't get why declarations in conditionals would require pattern matching to drop is. why can't both exist?
I guess the committee may say we should not have two ways to doing the same thing (for this basic case: test and create new binding in if)
17:36
<Justin Ridgewell>
That is the semantics you expect, through?
17:36
<bakkot>
or rather, I am using this feature, and am expecting the using to be disposed before the else, and am not thinking about the transpilation because that's a weird thing to think about
17:36
<Justin Ridgewell>
Unless you want to force transpiling using, too.
17:36
<Devin Rousso>
but those arent the same thing! you said it yourself: test vs create a new binding
17:36
<Lea Verou>
That …is not how user research works. TC39 is not a representative subset of developers who write JS, just like the CSS WG is not a representative subset of developers who write CSS. There are huge selection biases that make a certain type of developer get into standards. :)
17:37
<Justin Ridgewell>
But this is how I form mental models of new syntax. "How would I write this in the old syntax"
17:37
<Justin Ridgewell>
I think that's common.
17:37
<bakkot>
uhhh I think most people are not babel maintainers
17:37
<Justin Ridgewell>
That old syntax transpilation is not obvious.
17:37
<bakkot>
also it's only "new" syntax for a few years, after which it's just syntax
17:38
<bakkot>
the actual transpilation of for (let x is also not obvious
17:38
<bakkot>
but no one cares because it just works the way they expect
17:38
<bakkot>
if you actually write it out, it's pretty weird!
17:38
<Richard Gibson>
I'm not claiming that TC39 are representative. But that property is not necessary for an existence proof.
17:38
<Justin Ridgewell>
That's because of block scoping, and I think everyone wanted the let behavior from the beginning.
17:39
<bakkot>
it is not just because of block scoping
17:39
<bakkot>
even with block scoping it has additional magic
17:40
<Justin Ridgewell>
That the binding is available within everything inside the for's syntax? That makes it seem like if { } else { } should get it too.
17:40
<Justin Ridgewell>
The downlevel to for (var …) is obvious, it's only the block scoping that makes it magic.
17:41
<bakkot>
sorry, to spell it out, the additional magic is that the binding is per-iteration but ++i still works, somehow
17:42
<Justin Ridgewell>
That is, explicitly, block scoping's magic.
17:42
<Justin Ridgewell>
for (var i; i; i++) downlevel is easy to explain.
17:42
<bakkot>
I don't think of "block scoping" as meaning "we do weird things with loop updates", but sure, if that's what you mean then it is block scoping's magic
17:42
<Justin Ridgewell>
It's only block scoping's var capture for closures that has anything magical.
17:43
<bakkot>
no, it's the loop update which is magical here?
17:43
<bakkot>
anyway whatever
17:43
<bakkot>
the point is, this works because of, for (let x is a special syntactic form with a complex downleveling, and that's fine
17:43
<bakkot>
because no one actually thinks about the complex downleveling
17:43
<Justin Ridgewell>
Please explain why for (var i; i; i++) is wrong?
17:43
<bakkot>
it is fine, good even, to add new syntactic forms with good behavior but complex downlevelings. if they didn't have a complex downleveling they'd be less worth adding!
17:43
<Lea Verou>
It's not about existence though. Every language concept has been misunderstood by someone, somewhere. It's about distribution of expectations. If 99% of developers expect A and 1% expect B, it would be a poor design choice to throw because "we can't possibly know". But if 70% expect A and 30% expect B it's defensible.
17:44
<Jack Works>
but those arent the same thing! you said it yourself: test vs create a new binding

I mean test and create new binding

if (let decl = expr())
if (expr() is let decl)

for this simple case I think it maybe ok because decl in if is better in this case
but if decl in cond supports destruction it will overlap more

if (let { x, y } = point(); x === 0)
if (point() is { x: 0, let y })

then the committee may think it's a big overlap and require pm to match inside the let { ... }

17:44
<bakkot>
I have not said anything about for (var i being wrong and am confused why you are asking about it?
17:44
<Justin Ridgewell>
I think your Promise.try proposal points to this being wrong. We form a mental model of the code based on the way we'd write it now.
17:45
<Justin Ridgewell>
Because, literally my whole point, is that you form expectations based on how you'd write it today.
17:45
<bakkot>
we form a mental model of the code based on what we think it is. What I think Promise.try is is, "it's for calling wrapping sync errors", which suggests it should not do any additional stuff in the non-sync-error case
17:46
<Justin Ridgewell>
for (let …)'s transpilation is just var, unless you have a very specific case with block scoping and a closure.
17:46
<Justin Ridgewell>
It's extremely easy to explain.
17:46
<bakkot>
yes, block scoping and closures is the whole thing I am referring to wrt magic here
17:47
<Richard Gibson>
I understand this reasoning, and agree with it in many cases, but not in this one. Just like making -2**2 invalid syntax would still be reasonable even if 99% of then-current developers expected it to be negative.
17:47
<Justin Ridgewell>
The obvious transpilation of if (let x…) is let x; if (…)
17:47
<bakkot>
that is the obvious to you transpilation
17:47
<bakkot>
but to me if I see if (using x = ...) { } else { } I am really expecting the using to be disposed before the else
17:47
<Justin Ridgewell>
I think that's going to be the way 99% of people think of it.
17:48
<bakkot>
it would be (to me) completely insane for it not to be disposed before the else
17:48
<bakkot>
again, I strongly disagree with the claim that 99% of people think of transpiling code when reading code
17:48
<Justin Ridgewell>
I do not think anyone is going to expect this tranpsilation, because I struggled to come up with it.
17:49
<bakkot>
I don't think almost anyone would come up with the correct transpilation of for (let x = ...) setTimeout(() => x) either
17:49
<bakkot>
but that's ok!
17:49
<bakkot>
because people don't think in terms of transpilation!
17:51
<Justin Ridgewell>
for (var x;…) ((x) => { setTimeout(() => x) })(x);

We teach closure capturing as an interview gotcha question

17:51
<Devin Rousso>
does pattern matching create bindings in that destructuring example? or is it just testing whether it could destructure as described?
17:53
<bakkot>

right. it's a gotcha precisely because the transpilation is unexpected.

also, that suggests that you wouldn't be able to do an early return, so if you do think in terms of transpilation and that's your mental model you will still be confused. but in practice it is fine because, again, people do not think in terms of transpilation

17:55
<Justin Ridgewell>

How would you break inside any closure?

people do not think in terms of transpilation

I think everyone does.

17:55
<Justin Ridgewell>
It's just how new syntax features are taught.
17:55
<Lea Verou>

I think everyone does.

This is exactly why we're not representative users 😂

17:58
<Aki>
Your employer's interviewing style doesn't exactly have the best reputation for like… actually filtering for people who will be good at their actual jobs.
17:58
<Justin Ridgewell>
It's been banned for a while
17:58
<bakkot>
no one teaches this https://babeljs.io/repl#?config_lz=N4IgZglgNgpgdgQwLYxALhAJxgBygOgCsBnEAGhB22JgBdS0BtRkeAN3NFoUwHM6GIACYwwCAK5R6ZAARwA9rRkQYMgIxrZCpSoD6SeQCN1a8iANDJMBmKg0KYeZgDGMAIJQoAFUwI4xR0wkBlpMcRgAXwBdMhZibn4AWgBmTmEYZycEWidiADUYTGIIeTh0EAAmAAYK5MSNMxwIHBgoCDgYAAVMeRx5YgQocqR2iCRBkGiYkGJ5cRcYLwBPFvLiZ0xm2kmgA&code_lz=GYewTgBAFANgpgFwgDwgXggBgNwogHggGZcBqU5ASggG8AoCRiAZ0QBUBLAWzhAFcEUKNTQA-FOUrYGTDsGgBjEADsAJhwQcVw6gCMwcAIYBraUxbtuvAUJHjkk6QF8gA&lineWrap=true&version=8.0.4
17:58
<bakkot>
that is just not true
17:59
<bakkot>
they give an approximation which is good enough to get a sense of what it does
18:00
<Justin Ridgewell>
for (let x = 0; x < 3; ++x) {
    x = ((x) => (setTimeout(() => x++), x))(x);
    if (condition()) break;
    x = ((x) => (setTimeout(() => x++), x))(x);
}
18:01
<bakkot>
that has different semantics
18:01
<Aki>
👋🏻 hi could someone please volunteer to edit notes for an hour
18:01
<bakkot>
so I guess you don't think in terms of transpilation either? (sorry, that was rude, I apologize)
18:02
<Aki>
❌❌❌❌❌❌❌❌❌
18:02
<Aki>
hehe whoa that is not how i thought that would look
18:05
<Justin Ridgewell>

Or if we really want:

for (var x = 0; x < 3; ++x) {
    let y = x;
    setTimeout(() => y++));
    if (condition()) break;
    setTimeout(() => y++);
    x = y;
}
18:06
<Lea Verou>

Re: developer expectations, for anyone interested I just posted:

Still too early to draw conclusions (and polling on social media has its own, separate, biases obvs), but could be interesting as another data point.

18:06
<Michael Ficarra>
I am so excited to get async iterator helpers rolling
18:06
<Justin Ridgewell>
And then it's just block scoping's transform.
18:06
<Jack Works>
does pattern matching create bindings in that destructuring example? or is it just testing whether it could destructure as described?

current pattern matching does not create bindings by default, some examples:

x is { let a, b: 1 }, test x.b === 1 && "a" in x and create binding a
x is { a }, test "a" in x and no binding created.
x is { a: Some(let val) and let a }, test "a" in x && Some[@@matcher](x.a) and create binding a (equal to x.a) and val (equal to Some[@@matcher](x.a).next().value)

18:07
<Justin Ridgewell>
Need to run to a meeting
18:08
<Michael Ficarra>
buffered should totally be in the MVP
18:08
<Olivier Flückiger>
at least the bluesky post looks a bit biased as basically it focuses on the case where the else value is unexpected...
18:09
<Lea Verou>
I was trying for a snippet that is (a) short and (b) tests the else case, which is where the bone of contention lies. What would you use?
18:11
<Olivier Flückiger>
let a = "other";
if (let a = false) {
  log(a);
} else {
  log(a);
}
18:11
<peetk>
what's the goal? i think throw advocates acknowledge that throw is pretty novel semantics and thus wouldn't be anticipated by anyone. the point of throwing is people don't have to have a clear intuition. and throw mostly protects against a case where the developer doesn't realize the bound variable is shadowed
18:12
<Lea Verou>
Fair, that would be a little clearer. But do you think the difference might be significant enough to bias the results?
18:13
<Lea Verou>
Yes, it's not about "do people expect an error", it's about "what's the split between A and B". D only exists to keep the A vs B cleaner (instead of those who actually expect an error being forced to pick one of the two)
18:18
<Devin Rousso>
ok so yeah again there's a lot of syntax and behavior differences between those two. and for the few (debatably simple) scenarios that overlap it's a side-effect of the expressiveness of pattern matching, not an explicit goal
18:19
<Michael Ficarra>
@Lea Verou you should ask them what they think this should do:

if (let name = ...; name.length > 0) {
  log(name);
} else {
  log(name);
}
18:20
<Michael Ficarra>
I don't expect anybody will want it to log globalThis.name
18:20
<Devin Rousso>
@jackworks:matrix.org but more importantly, i don't really see how this conflict concern can be resolved without syntax/behavior/etc. changes to declarations in conditionals which likely would result in bringing in parts of pattern matching which is probably not something we would want
18:22
<Steve Hicks>
I find this case very compelling. My mental model is that if (x) { log(y); } else { log(y); } should be reducible to log(y);.
18:23
<Michael Ficarra>
which is exactly why we should ban it
18:23
<Michael Ficarra>
there's no good option where it runs and does something
18:23
<Steve Hicks>
Even if "x" is "let y = foo()" then I'd still expect log(foo()) to be equivalent.
18:25
<nicolo-ribaudo>
This demo is very cool
18:26
<Olivier Flückiger>
I don't think anybody would expect it to throw either....
18:27
<Michael Ficarra>
nobody is asking for it to throw
18:27
<Michael Ficarra>
we're asking for it to be an invalid program
18:27
<Michael Ficarra>
just like we did with exponentiation because nobody could agree what it should do
18:28
<Olivier Flückiger>
yeah, we only have runtime errors here. some are a bit earlier some later
18:28
<Olivier Flückiger>
this is actually gonna break lazy parsing btw.
18:28
<Ashley Claymore>
Developers will see SyntaxErrors very early...
18:29
<Ashley Claymore>
in their editor
18:29
<Olivier Flückiger>
one of the most important optimizations for the web where 80% of code is dead...
18:29
<Michael Ficarra>
that's not exactly an accurate model since it prevents earlier code from running
18:29
<Ashley Claymore>
maybe that was the motivation for TDZ over SyntaxError
18:30
<Olivier Flückiger>
might break, have to think about it some more
18:30
<nicolo-ribaudo>
Isn't it similar to how let a = 1; ... something ... let a = 2 is a syntax error?
18:30
<nicolo-ribaudo>
The parser needs to do some scope analysis
18:31
<Olivier Flückiger>
yes it does. I am not sure if it also looks at all uses
18:31
<Olivier Flückiger>
as i said, I have to think about it
18:31
<Lea Verou>
Yup, I think I agree with Michael Ficarra . It doesn't seem that developer expectations are sufficiently concentrated around either option, and an error can always become a not-error later, whereas if we pick either behavior, we're stuck with it forever.
18:39
<eemeli>
Urrr, would if (var x = y()) ... work? And if it does, would it too error for x access in the else?
18:40
<ljharb>
i assume it'd only allow let/const and not var
18:40
<nicolo-ribaudo>
I would be very in favour of just bannig var there
18:40
<Michael Ficarra>
it only allows let/const/using
18:40
<nicolo-ribaudo>
Regardless of which visibility we choose
18:42
<Steve Hicks>
var could let us have our cake and eat it too, since I'd absolutely expect the var binding to persist to the else... not that it's a good idea.
18:43
<Ashley Claymore>
Well var wouldn't be scoped at all, so there's no (real) benefit to putting it in there
18:43
<eemeli>
We do allow for (var x = ....
18:44
<ljharb>
that predates let/const tho
18:44
<eemeli>
Benefit: not introducing a need to think of where which declarations are allowed or not.
18:44
<Michael Ficarra>
for-var-of doesn't 😜
18:45
<ljharb>
yeah true
18:45
<nicolo-ribaudo>
What if we allowed only var
18:45
<ljharb>
you already get that benefit by never using var ever (when targeting engines that support let/const, ofc)
18:46
<Lea Verou>
I think it makes sense to allow it. Per eemeli 's argument, it's allowed everywhere else (edit: almost) and this would introduce another inconsistency that developers have to memorize (though as already mentioned, there's precedent). OTOH, it's mostly moot as developers have been so warned not to use var that they won't use it even where it would actually make their code cleaner because it's easier to just use let/const than to have to evaluate on a case by case basis.
18:47
<eemeli>
Seriously, I think we should allow var, even if it does exactly the same thing as if you had the declaration before the if.
18:47
<ljharb>
imo there's no excuse for hand-writing var once one is targeting let/const-supporting engines (there's one case for transpiler output of it, though, but that doesn't affect this)
18:51
<Lea Verou>
A while back I briefly experimented with adopting it for try ... catch where the scoping is typically undesirable, but that didn't last long, everything and everyone treats it as a mistake rather than a conscious choice.
18:51
<nicolo-ribaudo>

I like using var for things like

try {
  var foo = something():
} catch (e) {
  return;
}

use(foo)
18:51
<nicolo-ribaudo>
Oh well that :)
18:52
<ljharb>
we need to ship do expressions to solve that one :-p
18:52
<nicolo-ribaudo>
Real world example: https://github.com/babel/babel/blob/61bfda575b03e6a32af7877b65651a37eb339426/packages/babel-core/src/config/full.ts#L116
18:55
<Michael Ficarra>
try scoping is the next thing we should tackle after we get through this if/else scoping proposal
18:55
<Michael Ficarra>
those are the two places that I am constantly bothered by them being too broad
18:55
<Ashley Claymore>
ES2028 - the one where we fixed try
18:55
<eemeli>
Tackle how?
18:56
<Michael Ficarra>
I imagine it will just be try expressions
18:56
<Michael Ficarra>
give the developer a way to write a try that is scoped to exactly the thing they think might throw
18:57
<Michael Ficarra>
I hate seeing big blocks with like 10 statements inside a try/catch where the developer really only expects 1 little tiny expression in 1 of those statements to possibly throw
18:57
<Michael Ficarra>
but because of scoping, that's the easiest thing for them to do
18:57
<Michael Ficarra>
it's the same issue that we're solving with @Devin Rousso's proposal IMO
18:57
<ljharb>
to ship try expressions requires the same "completion value" question that blocks do expressions, and if we can resolve it for one we can do it for the other, so let's just advance do expressions :-)
18:58
<peetk>
python style try-else??
18:58
<Michael Ficarra>
I think one of them was slightly harder syntactically
18:58
<eemeli>
Pattern matching on catch would be really nice.
18:59
<Lea Verou>
Another idea might be a token to turn scoping off for certain blocks (a bit like regex's (?:) non-capturing groups). I just thought of this, so I may think it's a terrible idea later, but figured I'd put it out there :P
18:59
<nicolo-ribaudo>
A try expression could take an expression rather than a statement
18:59
<nicolo-ribaudo>
Like the current try expression proposal from somebody from the community does
19:00
<nicolo-ribaudo>
The one that returns a "result"
19:00
<Ashley Claymore>
Need extractors first...
19:01
<ljharb>
Pattern matching on catch would be really nice.
need pattern matching first :-)
19:01
<Michael Ficarra>
we're going to need a continuation of this async iterator helpers topic
19:02
<Michael Ficarra>
we really need to give Kevin feedback so he can make progress
19:03
<Olivier Flückiger>
I for my part am very happy with @erights Mark Miller (Agoric) MM setting the pace. If we run out of timebox then it's not because we are slow, but because it is complicated.
19:03
<peetk>
pattern matching but ONLY in catch
19:04
<Michael Ficarra>
I'm not complaining about us being slow, just saying it would be a shame to not finish giving Kevin feedback during this plenary (but we have plenty of time left at the end to continue, so I'm not worried)
19:05
<Michael Ficarra>
do failed pattern matches suppress or do they get rethrown?
19:06
<Justin Ridgewell>
Sorry, had another meeting. Why doesn't filter have a fast path for out-of-order?
19:06
<Justin Ridgewell>
The way we're discussing for map?
19:07
<eemeli>
Rethrow, probably?
19:07
<Michael Ficarra>
because you don't know which promise you can resolve with it while retaining the correct output ordering
19:08
<Justin Ridgewell>
But we're discussing that very thing for map?
19:08
<Michael Ficarra>
because for map you do know
19:08
<Justin Ridgewell>
Why is ordering important for filter but not map?
19:08
<Michael Ficarra>
input-output are 1:1 for map, not so with filter
19:08
<Olivier Flückiger>
agreed, but going to the next point can also evoke the expectation that the previous one is resolved.
19:08
<Michael Ficarra>
did someone ask to move along to the next point?
19:09
<Justin Ridgewell>
I don't understand.
19:09
<nicolo-ribaudo>
Do y'all see Ashley's slides as fullscreon or in a big black rectangle?
19:09
<Chris de Almeida>
not full
19:09
<Justin Ridgewell>
Tiny little rectangle
19:09
<nicolo-ribaudo>
It is weird but I guess fine, not worth trying to fix it
19:10
<Michael Ficarra>
I know that the third input value corresponds to the third output value for map; for filter, it could correspond to any of the first, second, third, or none of them (be dropped)
19:11
<Justin Ridgewell>
I get the words you're saying, I don't get the impl. How would map know this and not filter?
19:11
<Justin Ridgewell>
Why would it matter? If map can go out of order, why couldn't filter?
19:11
<Justin Ridgewell>
If filter can't, why would map?
19:11
<Michael Ficarra>
map resolves out of temporal order, but the values are yielded in sequence
19:12
<Michael Ficarra>
this entire proposal is order-preserving
19:12
<Michael Ficarra>
the out-of-order helpers are in a separate proposal (https://github.com/tc39/proposal-unordered-async-iterator-helpers)
19:13
<bakkot>
Justin Ridgewell: it is my hope that you can stare at https://bakkot.github.io/async-iterator-helpers-implementation/#map-concurrent and https://bakkot.github.io/async-iterator-helpers-implementation/#filter-concurrent-2 and be enlightened
19:13
<bakkot>
just play those scenarios out (hit right arrow or next repeatedly)
19:14
<hax (HE Shi-Jun)>
If want this, I think we should reserve if (let x = val) for consistency. (current proposal make it same as if (let x = val; val).
19:14
<Justin Ridgewell>
OHHHHH
19:14
<bakkot>
but, to put it in words: in either scenario, you do two pulls from the result. this does two pulls from the underlying. suppose the second settles and then the mapper settles / the predicate returns true for that value. for .map, you know that this value is going to go into the second result promise for .filter, it might not!
19:15
<Justin Ridgewell>
That was very helpful
19:15
<bakkot>
yaaaaay
19:16
<Michael Ficarra>
that totally justifies the hours and hours Kevin spent on the demo
19:18
<Michael Ficarra>
a 100-value composite seems a little bit unrepresentative of real code
19:18
<Michael Ficarra>
I would think they'd almost always be 2 or 3 values with a sharp drop off
19:20
<Michael Ficarra>
oh, I think both graphs are labelled the same but the first graph actually is only 3 values
19:20
<Justin Ridgewell>
Ok, the demo looks like different scenarios for the function, and not different semantics
19:20
<bakkot>
yes, demo just has one semantics implemented
19:20
<bakkot>
well, one for each of the three
19:20
<bakkot>
first link is a scenario for .map, second link is a scenario for .filter
19:20
<Justin Ridgewell>
Yah
19:21
<bakkot>
incidentally something like 80% of the complexity of this proposal is flatMap alone and we barely got to it, so, we have at least another hour of material to talk about I expect
19:24
<Michael Ficarra>
wait until you hear my opinions about flatMap, it's gonna be even more
19:24
<bakkot>
I had fable implement your thing, +647 -224
19:24
<bakkot>
I can do better but it's still going to be that order of magnitude
19:25
<bakkot>
I guess only +242 -131 for the actual implementation though, that was mostly tests
19:32
<Michael Ficarra>
I actually like the name Composite
19:37
<rbuckton>

I wish I were in a better position to discuss pattern matching, but all I have with me is my phone at the moment (and this takes awhile to type out). I looked into various ways to adopt pattern about 4 years ago now after researching the approaches taken in Rust, Swift, Python, C#, and a few other languahes, and was initially optimistic about the possibility of if..let. I found, though, that our current semantics for binding and assignment patterns are incompatible with well established pattern matching tenants. Our destructuring patterns aren't nearly strict enough, which would mean either inconsistent semantics between a normal let and if..let, or the need to introduce new syntax to opt-in to stricter matching behavior. The other big concern was how to differentiate between a binding and a reference. Typed languages can leverage static analysis and type systems, but JS does not have that capability. This, in turn, meant even more syntactic complexity to work around that results in footguns because it would by necessity be opt-in and not opt-out. The end result would either be confusing semantics or messy syntax, or a very limited, rough and inconsistent pattern matching experience.

I found, though, that the C# syntax and semantics were a good fit. C# separates destructuring (called "Deconstruction") from pattern matching and has both a branching match expression form and an infix is expression form. It's syntax for named bindings is both simple and explcit: Pattern Identifier?, where Pattern includes type names as well as var. Adapted to JS, this would mean you could write something like:

if (val is [Number x, Number y])...

or

if (val is { a: { b: String } a })...

or just

if (val is { a: let a })...

to both match and extract deeply nested values without bind/reference syntax confusion, while avoiding extra syntax for the most common cases (testing against types). I find patterns like x is Map or Set, x is not String, x is Some(Number), and x is [Number, Number] extremely convenient and clear, while x is @String or @Number (or similar) far less so.

19:37
<Justin Ridgewell>
Is there a scenario where ordering matters? If we held done results, and pushed pending calls first, could that simplify everything?
19:37
<Justin Ridgewell>
Then the consumer still sees a coherent stream.
19:38
<bakkot>
is there a scenario where the order of values from an iterator matters? to ask the question is to answer, surely?
19:39
<Michael Ficarra>
@bakkot I think this is just more support for my hypothesis that everyone is basically always going to want to use the unordered variants of these things
19:39
<rbuckton>
(a few typos, sorry. Not going to try fixing them because edits in Element on mobile always mess up formatting)
19:39
<Justin Ridgewell>
How would a consumer associate the pulled promise with the internal map/filter's call?
19:39
<Justin Ridgewell>
I don't know if there's a way for them to hold an identity where they could prove it
19:40
<bakkot>
possibly I am not understanding your suggestion. my position is: if the we do [1, 2, 3].toAsync().map(f).buffered(3).toArray(), we should get [f(1), f(2), f(3)] out, no matter what order the calls to f settle in
19:40
<bakkot>
if your suggestion does not violate that property then I do not understand your suggestion
19:41
<Justin Ridgewell>
Sorry, I'm not sure what buffered does
19:41
<Justin Ridgewell>
How does it interact with the fast delivery of map?
19:42
<bakkot>
pulls N times immediately, stores the resulting Promises in an internal buffer, hands them out in order to callers
19:42
<bakkot>
it doesn't inspect results of Promises at all
19:42
<Justin Ridgewell>
If f(2) returns done, does it matter that you receive [1, done, 3] or [1, 3, done]?
19:43
<Justin Ridgewell>
How does the consumer of the buffer associate the internal calls?
19:43
<bakkot>
I really really do not want to get [1, 3, done] in that case, map should not be changing order of results
19:45
<Justin Ridgewell>
I guess that does force an internal buffer of all things like filter.
19:45
<bakkot>

I don't understand the question.

.buffer is basically:

function buffer(iterator, N) {
  let buffer = [];

  for (let i = 0; i < N; i++) {
    buffer.push(iterator.next());
  }

  return {
    next() {
      buffer.push(iterator.next());
      return buffer.shift();
    },
  };
}
19:45
<bakkot>
modulo some handling of .return etc
19:46
<Justin Ridgewell>
My point was that whoever consumed the buffer couldn't tell which order the internal calls were made
19:46
<bakkot>
oh, yes, I agree with that, and it's OK / good to do the internal calls to the mapper/predicate function out of order
19:46
<Justin Ridgewell>
But this just becomes like a filter
19:47
<bakkot>
as long as the observed results are the same order you'd get if there was no concurrency
19:47
<Justin Ridgewell>
If we wanted to hold done values to reorder
19:51
<Michael Ficarra>
undefined values are dropped in JSON serialisation because JSON doesn't have undefined lol
19:52
<Michael Ficarra>
I don't think that says anything about how the language should behave
19:53
<bakkot>
I do occasionally kick around the idea of a { x?: v } syntax that doesn't create x if v is undefined
19:54
<bakkot>
if we had that syntax, I would feel more strongly that undefined should not be treated as absent, because if it's present it's there on purpose
19:56
<bakkot>
did everyone else just get kicked out
19:56
<nicolo-ribaudo>
Lol
19:56
<Lea Verou>
just came to ask the same question
19:56
<nicolo-ribaudo>
Yes
19:56
<Olivier Flückiger>
yes
19:57
<nicolo-ribaudo>
Are we on a free teams plan?
19:57
<Aki>
hmm.
19:57
<Ashley Claymore>
"this meeting is going on too long"
19:57
<Michael Ficarra>
well that was unfortunate timing
19:57
<peetk>
that's one way to timebox
19:57
<James M Snell>
That's.... fun
19:57
<Richard Gibson>
wow, judgmental much?
19:57
<Olivier Flückiger>
I think we all agree, thanks teams
19:57
<Michael Ficarra>
can we agree never to use Teams ever again?
19:57
<Aki>
Drag me, teams.
19:57
<Justin Ridgewell>
Lol
19:58
<James M Snell>
That's not a timebox, that's a time hammer
19:59
<Olivier Flückiger>
so I guess restart tomorrow? I hope somebody can restart it
19:59
<James M Snell>
For those who were dropped and have not yet rejoined... you can use the same link to just rejoin
20:00
<James M Snell>
but we're almost done anyway
20:00
<Chengzhong Wu>
btw refreshing the page won't work. Join with the link again
20:04
<Michael Ficarra>
yes this is what I was suggesting: we should solve that problem for object initialisers, it's not a problem for composites to solve
20:04
<bakkot>

Ashley Claymore: my topic was, IIUC you were concerned that people would write { a: 0, b: possiblyUndefined } and { a: 0 } and expect them to be equal. currently it is annoying to omit the property in the first case, and it's clunky to explicitly add a b: undefined in the second case, so I get that.

would you feel differently if we had syntax where they could write { a: 0, b?: possiblyUndefined } where the property would not be created if the value was undefined? that way you could easily omit the property in the first case.

20:04
<Aki>
Woof the google doc's summaries and conclusions are sadddddd so far. I apologise for not being more annoying earlier.
20:04
<Aki>
DON'T FORGET TO WRITE YOUR SUMMARIES AND CONCLUSIONS
20:04
<Aki>
I WILL FIND YOU
20:05
<James M Snell>
I did mine already so I wouldn't incur Aki's wrath
20:05
<Michael Ficarra>
there's still like 16 days left, I'm not worried
20:05
<Aki>
MF out here conspiring to give me a stroke
20:05
<Michael Ficarra>
I'll get to it in 15 days
20:06
<bakkot>
though, maybe we shouldn't do this because it encourages creating objects with different shapes, idk
20:07
<James M Snell>
Personally I'd prefer to avoid syntax and use something like new Composite({a: 0, b: possiblyUndefined}, { omitUndefined: true }) ... but I suspect I might be in the minority. I don't like special casing undefined by default
20:08
<James M Snell>
To be fair, 15 days, 23 hours and 50 minutes or so
20:08
<James M Snell>
give or take
20:08
<bakkot>
James M Snell: partly I want this for completely unrelated reasons: I want to be able to write x = { ...y, v?: undefined } to get all the properties of y except for v (and not have a stray v: undefined on x)
20:08
<Michael Ficarra>
if they want to make objects different shapes, they're gonna make objects different shapes
20:09
<Michael Ficarra>
whether it's more readable/maintainable or not
20:09
<bakkot>
I mean, no, mostly they create objects with extraneous undefined properties currently
20:10
<Michael Ficarra>
oh sure, you're saying some portion of them will give up and just fill with undefined instead if it's too wordy
20:10
<Michael Ficarra>
yeah I buy that
20:11
<James M Snell>
I just think it's weird that if I have Composite({ b: undefined, a: 0 }) that would be treated the same as Composite({ a: 0, b: undefined }) while Composite({ b: null, a: 0}) would be distinct from Composite({ a: 0, b: null})
20:11
<Richard Gibson>
is that being proposed? I thought property order was irrelevant.
20:12
<James M Snell>
Proposal shows, for instance, Composite({x : 1, y: 2}) -> Object.keys(c); // ["x", "y"] ... I assume order ends up mattering
20:12
<James M Snell>
unless there's a normalization I missed
20:13
<Richard Gibson>
I believe there is. That's the key sorting Ashley Claymore was talking about
20:13
<James M Snell>
ah! ok, then I missed that part of the discussion entirely thanks to dogs ;-)
20:14
<James M Snell>
still, even with that, I'm not seeing a strong reason to special case undefined
20:16
<Richard Gibson>
I agree. And we didn't even discuss -0 this time, which has similar possibilities
20:17
<bakkot>
-0 :(
20:18
<Ashley Claymore>
If we intern we have to normalize -0 IMO
20:19
<Ashley Claymore>
normalize to 0
20:19
<Richard Gibson>
interning vs. normalizing primitives into equivalences classes seem like completely independent decisions to me
20:19
<Ashley Claymore>
-0 === 0
20:20
<Ashley Claymore>
So a composite should be ===
20:20
<Richard Gibson>
and NaN !== NaN
20:20
<Richard Gibson>
=== is not an identity comparator
20:20
<Ashley Claymore>
I'm talking about intuition
20:21
<Ashley Claymore>
we also can't keep talking about -0 and NaN.
it's been over 5 years 
20:21
<bakkot>
the point of interning is that it lets you do === cheaply i.e. without a recursive walk; if it doesn't let you do === cheaply there is no point
20:22
<Ashley Claymore>
Exactly. Interning has to mean actual interning here 
20:22
<Ashley Claymore>
Not: maybe intern but not for -0 and NaN
20:23
<Richard Gibson>
that's true but immaterial... actual interning can be achieved with or without primitive normalization
20:23
<Richard Gibson>
they are independent
20:24
<Ashley Claymore>
true but not if we also want 0 and -0 to remain equal in a key, which I want 
20:24
<Ashley Claymore>
We already normalize them for Map and Set 
20:24
<Ashley Claymore>
That this what we should do here 
20:24
<Ashley Claymore>
Anything else is very unintuitive for 99.9% of JS developers 
20:25
<Ashley Claymore>
I frequently ask seniors engineers what they think about the equality of negative zero in floating point numbers and it's the first time they have ever heard of negative zero 
20:26
<James M Snell>

A case like

let obj = { a: 0, b: undefined };
let comp = new Composite(obj);
'b' in obj; // true! 
'b' in comp; // false! (because undefined was omitted)

Is likely to be a bit confusing for users... likely an edge case but still ... Is the proposal still planning to expose the members of composite as properties on the composite?

20:27
<Ashley Claymore>
I'm happy to change my stance on (not) including undefined values 
20:27
<Ashley Claymore>
I get  the feeling that my original stance is in the minority 
20:27
<James M Snell>
I'm just trying to poke around edge cases to see if I can convince myself that my opposition to ignoring undefined is incorrect
20:30
<Ashley Claymore>
I was mostly coming from the perspective of how prevalent JSON.stringify is used for keys today, and I hadn't come across dropping undefined as an issue.
whereas the key order and unsupported types were 
20:32
<James M Snell>
yeah, and I can absolutely appreciate that view. I can see it both ways but special casing it just feels more incorrect. Going to have to stew on it for a bit
20:33
<Ashley Claymore>
and NaN !== NaN
Yea but they are equal in a Set
20:34
<Ashley Claymore>
It would be catastrophic to defining interning a key that has NaN within it to always mint a new key 
20:34
<Ashley Claymore>
Just so they are not equal 
20:34
<Ashley Claymore>
so NaN in a key must be equal 
20:35
<Richard Gibson>
yes, Set and Map both have opinionated non-configurable key normalization that doesn't correspond with ===
20:35
<Ashley Claymore>
And throwing would be a real shame. If that's the only case we throw on 
20:35
<bakkot>
honestly that specific example is fine for me, I don't care that much about equality for keys containing NaN
20:35
<Ashley Claymore>
yes, Set and Map both have opinionated non-configurable key normalization that doesn't correspond with ===
This is why it is related to interning.
the interning will define the equality  in Map and Set 
20:36
<bakkot>
I do think it's important that Composite({ x: -0 }) === Composite({ x: 0 }), which implies one of: not interning, a full walk for === despite interning, normalization, or throwing
20:36
<bakkot>
I think we should rule out the "full walk despite interning" option, and throwing feels bad
20:39
<Ashley Claymore>
https://acutmore.github.io/record-tuple-laboratory/ I made this 4 years ago.
we just keep having this same exact conversation 
20:42
<Richard Gibson>
this is probably the crux: I don't think it's important that Composite({ x: -0 }) === Composite({ x: 0 }), and haven't determined whether or not I'm opposed to it. Composite is in some sense key normalization that sits outside of Map and Set, and normalizing primitives into equivalence classes adds a lot of friction to recovery where the distinction matters (which tends to matter rarely, but not never)
20:42
<Richard Gibson>
we keep having the conversation because no proposal has ever made it far enough for a satisfactory resolution
20:43
<ljharb>
fwiw i fail to understand any unordered use cases; in all cases i'd want to preserve the order (talking about async helpers, not composite)
20:44
<Ashley Claymore>
there is no order here, it's key value pairs
20:44
<Ashley Claymore>
And the keys are sorted (no symbol keys allowed)
20:45
<Ashley Claymore>
ah sorry, though you were talking Keys 
20:46
<Ashley Claymore>
Fair.
this is why I wanted to resolve the interning question first.
so that we can have a discussion on -0 with that decision already made 
20:46
<Ashley Claymore>
as they are related 
20:46
<Richard Gibson>
there are tons of cases where order doesn't matter. For a simple one: perform some operation on every file in a directory
20:46
<bakkot>
I definitely agree that you usually you want to preserve order. but there are cases where you might not: any time you use .some() on the result, or .reduce((a, b) => a+b), or similar. in those cases it literally does not matter what order things resolve in, and so you just want to get the answer asap
20:47
<bakkot>
(well, quibbles around commutativity of floating point addition aside)
20:47
<James M Snell>

sorry, still stuck on the undefined bit here... one example case where ignoring undefined causes.. weirdness

const err1 = new Error('A', { cause: undefined });
const err2 = new Error('A', { });
const comp1 = new Composite(err1);
const comp2 = new Composite(err2);
comp1 === comp2;

These are definitively two separate sources, but they end up resulting in the same Composite? That feels... off. When we're talking about options bags / object literals that are passed in, it seems ok, but for stuff like this... treating them as equivalent feels ... wrong

20:47
<James M Snell>
Btw, I'm intentionally ignoring stack in that as it's just a quick example
20:50
<bakkot>

or, say you want to fetch a bunch of files, but the files have associated filenames, like [{ name, url }, { name, url }]; you ultimately want a Map from name to url and you know by construction that the names are unique. so you do

let result = new Map;
let iter = items.toAsync()
  .map(async ({ name, url }) => [name, await fetch(url)])
  .buffered(3); // 3 fetches at once
for await (let [name, file] of iter) {
  result[name] = file
}
20:52
<bakkot>
you can get results strictly faster with unordered map/buffered in that example, because then you are always fetching three results at once; when order is enforced, you can't drain later results from the buffer until earlier ones have settled, so you're often blocked waiting for earlier ones to settle. there's a nice animation of this in actually on https://github.com/tc39/proposal-unordered-async-iterator-helpers
20:56
<hax (HE Shi-Jun)>
so I expect [1, x?, 2] could give u [1, 2] if x is undefined 😉
20:58
<Richard Gibson>
they don't seem very related to me, but I nonetheless support resolving interning vs. not first and would have done it the same way
21:05
<ljharb>
ok but that's a single operation on a directory. if i had a list of the files, absolutely the order should be preserved
21:25
<Richard Gibson>
counting occurrences of a particular search term is not a use case in which order matters
21:26
<ljharb>
ok - in any of the cases where you can discard order, does it matter if order is preserved? (like, could you use the order-preserving ones and it'd be the same)
21:27
<bakkot>
depends on whether you care about how long things take
21:28
<bakkot>
you will ~always get the same result, it'll just take longer
21:28
<ljharb>
why would that be relevant? preserving order doesn't mean the operations can't all be done in parallel/concurrently
21:28
<ljharb>
it just means the final results need to be in order
21:28
<ljharb>
eg the way Promise.all works
21:29
<bakkot>
generally speaking you want to have a bounded degree of concurrency and also a bounded buffer size
21:29
<bakkot>
if you drop one of those requirements then yes you can just do everything at once and keep it all in memory
21:32
<ljharb>
the final result has to be all in memory anyways tho, right? so why can't that be built up out of order (in the proper order) as results settle?
21:33
<bakkot>
no, there are lots of cases where the final result does not have to all be in memory at once?
21:37
<ljharb>
ah k because it's an async iterator
21:38
<ljharb>
ok so with the unordered one, it's just a totally nondeterministic order in the result?
21:38
<bakkot>
yup
23:31
<Richard Gibson>

here's an example of exactly that pattern, which I think of as a work pool: https://gist.github.com/gibson042/d8ab425d133400959bc5bb875898a97b#file-github-fixer-user-js-L1023-L1029

concurrency ramps up to the specified capacity as quickly as possible by take()ing input iteration results from source, output iteration results are yielded as quickly as possible (as [index, result] pairs in case the consumer wants to restore ordering), and new inputs are take()n immediately thereafter until source is exhausted