| 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. |
| 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? |
| 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 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 |
| 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:
|
| 02:37 | <Justin Ridgewell> | Nit: we could support tacit But yes, running arbitrary expressions is a little more difficult, and likely pushes some to do And |
| 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 |> 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 Even with |
| 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.:
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
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:
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 …And this would be true even for Hack pipes, too, wouldn’t it? If I type |
| 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
PFA is good for all three cases. Function returning is only good for one case. |
| 03:33 | <Justin Ridgewell> |
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> |
|
| 03:41 | <rbuckton> | or if you want it to look all sigil-y:
|
| 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
Though there was a proposal to address
|
| 03:53 | <rbuckton> | I don't know how often that would be an issue in practice, though. |
| 03:55 | <Justin Ridgewell> |
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
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:
|
| 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
|
| 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
|
| 04:35 | <Justin Ridgewell> | That reminds me too much of class syntax in C++ |
| 04:40 | <rbuckton> | My thought was to use
|
| 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? |
| 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 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 |
| 17:33 | <rbuckton> |
I don't expect they do. I do expect there are many instances of
in the wild, which is essentially the same thing. My point with |
| 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> |
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? |