02:03
<jschoi>
My assumption is currently that pipeline will be attractive to X% and call-this will be attractive to X+Y%. I would like to get a better feel for the magnitudes of X and Y. X is small + Y is small = do neither. Neither will make a meaningful difference. X is large + Y is small = Just do pipeline. It's more generally applicable and call-this provides only marginal additional benefit to end users. X is small + Y is large = do call-this. Pipeline won't move the needle but call-this will. X is large + Y is large = do call-this, then maybe pipeline if it interests a different set of method-users.

It‘s probably more like a Venn diagram with some overlap: X%+Y%+Z%, where Y% is the overlap between pipe operator and call-this. A lot of people have expressed interest in the pipe operator but not call-this over the years, even disregarding code-shaking as a motivation. I don’t quite think pipe-attracted people are a subset of call-this-attracted people. With that said, it’s true that we have not, to my knowledge, pitted them head-to-head in front of the community.

Well, actually, I think prior State of JS surveys have maybe asked developers about both proposals when assessing their interest in various TC39 proposals.
I’d need to double-check the prior years’ results…but I’m not really looking forward to doing so. It’s a good resource, but I find the State of JS websites difficult to navigate…

02:16
<jschoi>
I'd really like to pursue Elixir style (to support known arg position, auto imports, tacitness, and fluency), with PFA layered on (to support any arg position)

The point that Elixir pipes (but not F# pipes or Hack pipes) would support autocompletion on the first argument is intriguing. I wonder if autocompletion would have changed Daniel Ehrenberg’s decision to use F# pipes instead of Elixir pipes, back in 2017 (nearly a decade ago…).

Though I don’t know if I really want to wade through that whole pipe-style bikeshedding ordeal again. Weren’t quite a few people against promoting the zeroth argument to be special, even with PFA syntax?
And then there were the engine concerns with PFA syntax and optimizability of observable stack traces, etc.

02:17
<jschoi>
Well, I suppose call-this could also serve that role of providing tree-shakeability, fluency, and autocompletion. And that Reddit comment about Firebase’s change makes it clear that at least some developers feel strongly about autocompletion too. This makes me more torn about that statement I made earlier about rather having Hack pipes than call-this.
I suppose that, even in a world where call-this makes this-based standalone functions more common, the pipe operation could still be useful…because non-this-based standalone functions like Array.from, Object.keys, etc. are not ever going away. But the engine implementers are likely not to agree.
02:22
<Justin Ridgewell>

I think we dismissed Elixir without really discussing its merits originally. There are definitely delegates that dislike the special first arg behavior, but there are also those that dislike this binding behavior too.

Re PFA and optimizing, I don't think this will be a concern the way it is with F#. Using PFA directly within a pipeline is optimizable, but we've only talked about PFA with F# and F# promotes any type of closure-returning function call.

02:23
<Justin Ridgewell>
You could abuse Elixir to generate F# closure pipelines, but it actively resists it. (You would have to put trailing foo(arg)() to generate an F# single param style call)
02:26
<jschoi>

Using the pipe readme’s example from node/deps/npm/lib/unpublish.js:

02:26
<Justin Ridgewell>
But, I'd settle for any of Hack/Elixir/Call pipelines. I would like multiple, but I don't know if we'll get that.
02:29
<jschoi>

…I’m trying this again; Element’s message editing is so unreliable. From inside NPM’s code:

// Status quo:
const json = await npmFetch.json(
  npa(pkgs[0]).escapedName,
  opts,
);
// With Hack pipes
const json = pkgs[0]
|> npa(##).escapedName
|> await npmFetch.json(##, opts);
// With Elixir pipes,
// assuming that it supports a `|> await memberExpression(arg0)` form:
const json = (pkgs[0] |> npa()).escapedName
|> await npmFetch.json(opts);
02:37
<Justin Ridgewell>

Nit: we could support tacit pkgs[0] |> npa.

But yes, running arbitrary expressions is a little more difficult, and likely pushes some to do getEsapedName helpers or similar.

And await/yield could be handled as either special behavior inline, or a postfix |> await |> … so we don't redefine the expression inline.

02:39
<jschoi>
Double checking HISTORY.md, it looks like Dan ran into automatic semicolon insertion problems with |> await, which is why he tried to defer it to a future proposal.
02:40
<jschoi>
…Times like this is why I’m glad I wrote this document back then…
02:40
<Justin Ridgewell>
Very good job! This is sooo long ago I can barely remember.
02:40
<Justin Ridgewell>
Of course ASI is biting us 🙄
02:42
<rbuckton>
F# also doesn't solve this, because you have to complete the closure call to know what function is being invoked by the pipeline
What do you mean by "complete the closure call" in this case? I'd venture to guess that with F#-style and PFA, the majority of use cases would have been |> f(?, a, b) anyways since most functional libraries for JS are first-arg (aside from Ramda).
02:45
<Justin Ridgewell>

That was from me trying Rx pipelines. You'd do x |> foo(a, b), which means you wrote a ton of code before we could figure out what you're doing.

Even with x |> foo(?), you still need to write foo( before we can figure out where you're placing the arg and how filter the possible set of functions.

02:46
<Justin Ridgewell>
With Elixir style, x |> f is enough to start filtering for functions starting with f that receive x type
02:46
<rbuckton>
From an autocomplete perspective, a language like TS could prioritize completions for F#-style based the topic type first by tacit functions, then by functions that could accept the topic in any position (and possibly complete with f(_, ?) for non-first-arg cases with the cursor at the _)
02:53
<rbuckton>
Brian Terlson and I were discussing pipelines the day before a plenary back in 2017/2018, I think? One of our concerns at the time was that Elixir style pipes primarily favored first-arg based libraries like lodash/underscore at the expense of last-arg based libraries like Ramda, which would essentially "choose a winner" in that space, which would likely result in significant community backlash. My solution to the issue was F#-style pipes with PFA as it applied equally to both camps.
02:54
<rbuckton>

There's also the potential for confusion by not supplying a first-arg when pipeline is involved, i.e.:

map(ar, x => x + 1)
// vs
ar |> map(x => x +1)

as these two calls to the same function could potentially live side-by-side in the same file.

02:56
<rbuckton>

The upside of PFA is that it kept that intuition as to argument placement. The argument didn't just disappear because I'm using |>:

const mapped = map(ar, x => x + 1);
// vs
const mapper = map(?, x => x + 1);
const mapped = mapper(ar);
// vs
const mapped = ar |> map(?, x => x + 1);

All three styles could be used interchangeably in the same file without confusing users as to argument order.

02:58
<rbuckton>

Ramda essentially avoids this because Ramda functions curry, so they set the expectation that partial application results in a bound function:

add(1, 2); // 3
add(1)(2); // 3
add()(1)(2); // 3

Each of these produces the same result, so it builds an intuition for the developer as to how arguments are applied.

02:59
<rbuckton>
Elixir style pipes don't provide any such intuition. There's nothing special about the function your calling (i.e., no currying), only the |> syntax, so it doesn't carry over to any other aspect of the language.
02:59
<rbuckton>
It's just one corner-case of the language that does this unique thing, so its far less intuitive.
02:59
<Justin Ridgewell>
In my experience, first arg is the most common, and the way I personally write my libraries. But with PFA, Elixir handles every case as well as Hack or F#.
03:00
<jschoi>
This is good context for HISTORY.md. I should add it to the file.
03:00
<Justin Ridgewell>
Half of the battle with Hack is the topic token. Why didn't we just ban PFA inside pipeline and use the same ? token?
03:00
<rbuckton>
Yes, all the pipe styles can (mostly) accomplish the same things. My point is that Elixir pipes are unintuitive.
03:01
<Justin Ridgewell>
I mean, I would call F#'s auto call style unintuitive, too. It's just which one you get used to using.
03:01
<rbuckton>
I've never really been a fan of Hack style. IMO, while I think PFA has some value on its own, its case is severely weakened by lack of support in pipelines.
03:02
<rbuckton>
Auto-call?
03:02
<rbuckton>
you mean x |> F is F(x)?
03:02
<Justin Ridgewell>
x |> foo(a, b) does foo(a, b)(x)
03:02
<rbuckton>
That's what the |> means in F#
03:02
<rbuckton>
It's the same intuition behind @ in decorators
03:02
<Justin Ridgewell>
And this is what it means in Elixir. Both of them are foreign to JS, neither is intuitive.
03:03
<Justin Ridgewell>
You just learn the syntax and then it becomes intuitive after using it enough. I think we can get the community to adopt either style.
03:04
<rbuckton>
I still believe F#-style has a very low learning curve. F#-style could be explained without magical argument placement, even without PFA, and PFA could be explained without F#, but they work best together.
03:04
<Justin Ridgewell>
The benefit to Elixir is that it won't suffer from the performance regressions promoted by closure allocs.
03:04
<rbuckton>
In F#-style |> is just function application. Call the thing on the right with the argument on the left. F# even has multiple-argument styles for |>
03:05
<rbuckton>
F#-style doesn't need closure allocs when used with PFA. Those can be statically replaced with an immediate call, and could even be specified as such.
03:06
<rbuckton>
x |> f(?) is easily evaluated without a closure
03:06
<jschoi>
Yes, in the end, that was the part that was important. The engines just did not want to encourage more hot function allocation. They want people to name their functions or to use callbacks that are very simple to optimize. With Elixir or Hack pipes, you cannot do x |> f(y) to mean f(y)(x), and, for the engines, this is an advantage. They might have been fine with it if F# pipes restricted the RHS side to a member expression like |> f or |> NS.f?
03:06
<rbuckton>
And so is x |> f
03:07
<rbuckton>
You only really need to produce a closure for f(?) on its own
03:08
<jschoi>

It’s true that autocompletion engines could use a zeroth-argument-is-probably-the-principle-type heuristic on F# pipes, switching to the next argument’s type. With F# pipes and PFA syntax, as soon as I type x |>, my editor’s autocompletion engine could assume that I’m going to type x |> f(? or something similar, and it could show completions for standalone functions that expect x’s type at their zeroth parameters (followed by functions that expected x’s type at their first parameters, then second parameters, etc.). Just as a heuristic.

…And this would be true even for Hack pipes, too, wouldn’t it? If I type x |>, and we were using Hack pipes, the engine could also use the same heuristic. It could show functions that expect x’s type at their zeroth parameters (and then first, second, etc.). So, actually, Justin, I might have changed my mind again thanks to Ron, haha. I think Hack pipes probably are fine for autocompletion.

03:09
<Justin Ridgewell>
I don't think this is the style that F#'s syntax promotes, though. You can do this, but there's the subtle push towards f without explicitly placing the arg.
03:10
<rbuckton>
I think the engines concerns about f(?) may have been a bit over cautious. You're less likely to see x |> f(?) in a tight loop, as it's usually the f in this scenario that has the loop (i.e., map, filter, reduce, etc.). You'd be producing no more closures than you normally would with the arrow in map(x, a => ...)
03:12
<Justin Ridgewell>
It's definitely RX.js that scared them. I agree that PFA inline a pipeline wouldn't be an issue.
03:12
<rbuckton>
F#+PFA definitely promotes x |> f(?) as the majority case. x |> f is far less frequent, and introduces no closure on its own. The other cases for PFA on its own are no worse than => today.
03:13
<rbuckton>
If Rx.js had PFA, it would arguably be no worse than => since Rx.js consumers were going to use => anyways. All PFA does is reduce the second-guessing about closed over variables. In a way, it reduces closures since it evaluates the arguments immediately and only holds the values (like .bind).
03:14
<Justin Ridgewell>
I don't agree. If I'm writing x |> foo(?, a, b, c), do I really want to place the arg? If I just wrote x |> foo(a, b, c) and returned a closure, it shaves the chars… I think that's the subtle push that doesn't happen with other styles.
03:27
<rbuckton>

Sure, if you are the author of foo and don't mind the overhead of returning a closure, then maybe x |> foo(a, b, c) seems like a win. Performance wise, it isn't.
PFA gives you the option to produce a function and allows you to use existing functions that were not designed for currying. F#-style support for tacit functions promotes reuse rather than one-off closures:

// F#+PFA
const add = (x, y) => x + y;

add(1, 2); // no closure

ar |> add(?, 1); // no need for closure

const addOne = add(?, 1);

ar1 |> addOne;
ar2 |> addOne; // reused

// vs F#-style with only function-returning functions
const add = x => y => x + y;

add(1)(2); // produces short-lived closure

ar |> add(1); // produces short-lived closure

const addOne = add(1);
ar1 |> addOne;
ar2 |> addOne; // reused

PFA is good for all three cases. Function returning is only good for one case.

03:33
<Justin Ridgewell>

don't mind the overhead of returning a closure, then maybe x |> foo(a, b, c) seems like a win. Performance wise, it isn't.

We know this, but will library authors? I have done horribly inefficient things in the name of clean looking code when I was a novice.

03:35
<Justin Ridgewell>
But I agree with you about PFA. I want it in the language, and I hope it's enough to overcome the subtle push.
03:37
<Justin Ridgewell>
Like, look at Chai.js style syntax chaining 🙈
03:38
<rbuckton>
I'm personally not sold on Hack-style's premise that people want to use |> to operate purely on the topic, i.e. x |> # + #. Function pipeline allows you to perform higher-order operations. We can already do x + x, and you can emulate Hack-style pipes with ,, so it doesn't really add anything to the language, IMO.
03:40
<rbuckton>
// Hack-style
x = ar
    |> f(#)
    |> # + #;

// Plain-old JS
var _;
x = _ = ar,
    _ = f(_),
    _ = _ + _;
03:41
<rbuckton>

or if you want it to look all sigil-y:

x = _= ar,
    _= f(_),
    _= _ + _;
03:41
<Ashley Claymore>
TypeScript doesn't love this 
03:41
<rbuckton>
Hack-style pipes is the ,_= operator.
03:41
<Ashley Claymore>
and it would be rejected in a PR
03:42
<rbuckton>
Perhaps, but that's the downleveling.
03:42
<Ashley Claymore>
Right, it's purely syntatic 
03:43
<Ashley Claymore>
an engine would likely need zero new bytecode for it
03:44
<rbuckton>
TypeScript handles this fine
03:44
<Ashley Claymore>
It's always the same type there 
03:44
<Ashley Claymore>
what if it changes to different object types through the pipe 
03:46
<rbuckton>
works fine if you use parens:
03:47
<rbuckton>
(You need the parens to keep the precedence correct).
03:47
<Justin Ridgewell>
Mildly agree. I think it's convenient that Hack allows this, but I think our first priority should be solving method DCE. These kinds of expressions would be mostly simple to extract into helpers, or break apart into multiple pipes (we don't always need super long pipe flows)
03:47
<rbuckton>
hmm. strikethrough doesn't work in Element.
03:51
<rbuckton>

IMO the only thing Hack-style pipes do better than F# is await and yield, though you can still address those with parens:

// Hack-style
a |> await f(#) |> yield g(#);

// F#-style
yield (await a |> f(?)) |> g(?);

Though there was a proposal to address await and yield as a special operation:

a |> f(?) |> await |> g(?) |> yield;
03:53
<rbuckton>
I don't know how often that would be an issue in practice, though.
03:55
<Justin Ridgewell>

Though there was a proposal to address await and yield as a special operation:

I just mentioned that above, apparently there are ASI issues:

03:55
<rbuckton>
it's bad practice to yield deep within an expression, it makes it hard to reason over where you're function might suspend. await is the same. It's usually better to break up awaits into multiple statements, otherwise you're back to parenthesizing like (await f()).foo()
03:59
<rbuckton>
I wouldn't call it an "ASI Issue", per se. Anyone who chooses to rely on ASI to avoid semicolons already needs to pepper them throughout their code. ASI is only really an issue if you're trying to introduce new syntax that could be interpreted as existing syntax due to ASI. That doesn't apply to |> because its mere existence in the language doesn't cause unrelated code to break.
04:05
<rbuckton>

And as far as Hack-style's benefit for x |> # + #, that can be achieved in F# using an arrow, and we could theoretically inline arrows statically within the spec as well:

x |> (_ => _ + _)

The only thing that would care that it's not actually a function would be a stack trace, but I'm ok with that.

04:05
<rbuckton>
Still no yield or await, but I contend that's just not as critical here.
04:10
<rbuckton>
F#+PFA only really introduces two distinct concepts (|> and ?/...) but gives us so many things. For Hack-style we've suggested multiple additional sigils like |+> to provide something like PFA, which is starting to feel like a sigil soup. Too many tokens to remember what does what. It's like trying to remember keybinds in Blender. There's 1000s of them.
04:12
<Justin Ridgewell>
I think the same could be said for Elixir. But the additional sigils are still likely going to be suggested regardless of which we choose, eg to support nullish short-circuiting.
04:13
<rbuckton>
short-circuiting?
04:14
<rbuckton>
Ah. I think that's less of an issue for |> than for function calls in general. Aside from ? and if there's no good way to make a call conditional an an input argument being nullish
04:14
<Justin Ridgewell>
x |> foo ?|> bar could stop the pipeline if the return value from foo is nullish.
04:15
<rbuckton>

In F# style, you could just fall back on an arrow:

x |> (_ => _ == null ? null : f(_) |> ...)
04:15
<Justin Ridgewell>
One of Evan's arguments is that pipeline needs to be as ergonomic as method chaining. x.foo()?.bar() is very ergonomic to write.
04:16
<Justin Ridgewell>
If we want to encourage people to stop writing class methods, the alternative needs to feel at least as good.
04:16
<rbuckton>
I'm not sure ?|> is the sigil though. It looks awkward.
04:17
<rbuckton>
Maybe just ?>
04:17
<Justin Ridgewell>
Yah, it all starts to look like soup eventually. Evan was pushing .. as the pipeline operator, so it'd be x?..foo() (specifically with call-this semantics)
04:17
<Justin Ridgewell>
That doesn't feel too bad.
04:18
<rbuckton>
?.[ and ?.( are already bad, if necessary. For pipelines, I'd rather have something recognizably pipe-like.
04:19
<Justin Ridgewell>
I could get behind ?>
04:20
<rbuckton>
IMO, ?.. should just be synonymous with ?., given that ?.[ is the nullish [ and ?.( is the nullish (, then ?.. should be the nullish .
04:20
<rbuckton>
Not that I want that syntax in the language, just that it essentially holds that space logically.
04:22
<rbuckton>
We should stay away from ... We already have . and ... meaning two wholly different things, we don't need .. meaning something else, especially since some languages use .. in the same way we use ...
04:22
<rbuckton>

i.e., C# supports .. in collection initializers:

int[] x = [1, 2, ..y];
04:23
<rbuckton>
And other languages use .. for ranges, i.e. a..b.
04:25
<rbuckton>
If we hadn't needed to use ?.[ and ?.( for chaining, I would have suggested x.(f)() or x.[f]() for call-this. That said, I still think :: is perhaps a good choice for that.
04:29
<rbuckton>

Though I had thought about using :: for a syntactic event mechanism. It was used for events in a different way in early versions of IE/JScript:

function window::onload(e) {
}
04:35
<Justin Ridgewell>
That reminds me too much of class syntax in C++
04:40
<rbuckton>

My thought was to use event to describe named events in a separate namespace (similar to #) and :: to access them, e.g.:

class Button {
  event click;
  event dblclick;

  raiseClick(e) {
    this::click(e); // can only invoke 'click' using :: when lexically inside class
  }
}

const b = new Button();
b::click += e => { };
b::dblclick += e => { };
04:41
<rbuckton>
and the whole syntax would leverage a symbol-based protocol that could be adapted to both NodeJS EventEmitter and DOM EventTarget (and any other event system in userland)
04:43
<rbuckton>
If that were to ever make it to an actual proposal, I'm sure there are other symbols that could be used. :: here is mostly JScript nostalgia.
04:43
<rbuckton>
:: for bind-this/call-this is also perfectly reasonable.
06:35
<ljharb>
I'm blanking. What's the potential bug with the open paren?
the bug is “i meant to type . and typed ..” or the reverse. Two syntaxes with quite different resolution semantics shouldn’t be that similar
08:00
<Ashley Claymore>
I like '::` for call-this.
I don't think we need syntax for events in JS.
17:27
<jschoi>

I kind of feel like we’ve gone in circles over this over the years.
I already did a somewhat thorough review of what big open-source JS projects actually do for the current pipe proposal readme.
Almost nobody actually uses _ = …, _ = …. What people do use is deeply nested parenthesized expressions, mostly for function calls and await expressions, but also sometimes for object literals, array literals, and template literals. We can say things, “They should just break up all of those up into statements with different variables,” or, “They can already reassign to a dummy variable,” but the fact remains that almost everyone doesn’t do this. They deeply nest expressions in consolidated statements as if they were method chains, except with lots of recursive parentheses.
Many of the real-world examples I found are still in the readme.

I do agree with Justin that the most important feature of Hack pipes is that, like other pipe styles and like call-this, it encourages DCE-friendly API use. I agree that it’s a mere secondary convenience that Hack pipes do accommodate mixing different styles like function calls, member access, method calls, object/array/template literals, and the like in a unified postfix style.

Nevertheless, I do not agree that “people can already just use _ = …, _ = …; therefore Hack pipes are not beneficial”. The fact is that, even if developers already can use ,, and even if they can already break every await into a separate statement, they simply they do not. They continue to create poorly readable, deeply nested expressions everywhere. They want a fluent syntax that accommodates these somehow, whether it be Hack pipes or something else.

17:33
<rbuckton>

Almost nobody actually uses _ = ...

I don't expect they do. I do expect there are many instances of

const a = f1(x, ...)
const b = f2(a, ...)
const c = f3(b, ...)

in the wild, which is essentially the same thing. My point with _ = ..., is that it readily reproduces Hack-style behavior with no new syntax. It greatly sours the value of Hack-style for me, as I'd initially hoped for a simple syntax to give me more power over function application (tacit or otherwise), which is a far more novel capability.

18:16
<Justin Ridgewell>
I like that you wrote Elixir-favoring functions there 😉
18:16
<rbuckton>
First-arg favoring, maybe, but examples aren't intended to speak to a preference.
18:18
<Justin Ridgewell>

They want a fluent syntax that accommodates these somehow, whether it be Hack pipes or something else

Agree with this. I like that Hack feels like regular code sequences, and can break them up to be much more readable. I think people would use this more than I would personally like, but the end result will still be readable code.

18:18
<Justin Ridgewell>
Same same.
18:21
<rbuckton>
One place where F#-style aligns with JS is Stage 3 Decorators. In F# style, x |> F invokes F with x, and x |> F(a) invokes the result of F(a) with x. This is the same with decorators/decorator factories: @F invokes F with the decorator context, and @F(a) invokes the result of F(a) with the decorator context.
18:23
<rbuckton>
And while we didn't propose the syntax, F#'s reverse pipe <| follows the same order as decorators: @F @G is essentially F <| G <| x
18:34
<Justin Ridgewell>
Given my hatred of decorators, I don't think trying pipeline to the (reversed) execution model of decorators is a win.
18:35
<Justin Ridgewell>
Especially, why did decorators need to be tied to closure buildup?
19:19
<rbuckton>
It's less the reversed execution model and more the existing mental model related to decorator vs factory.
19:19
<rbuckton>
Especially, why did decorators need to be tied to closure buildup?
Prior art from python and also that these only run during class declaration init