00:00
<rbuckton>
TabAtkins: I think "add 1 to this value" can also be done with this:
00:00
<rbuckton>
```
00:00
<rbuckton>
((x |> say(?)) + 1) |> print(?)
00:00
<rbuckton>
```
00:01
<TabAtkins>
Those parens ar emaking me physically recoil
00:01
<TabAtkins>
That's *precisely* the sort of code contortions I want to make sure are never necessary.
00:02
<TabAtkins>
Going from `x |> say(?) |> print(?)` to... that, just because you realized you needed to incrememnt the value in the middle
00:03
<TabAtkins>
(realistically you'd write `x |> say(?) |> x=>x+1 |> print(?)` tho)
00:03
<rbuckton>
I've considered proposing functional operators for those kinds of cases too, which are similar to F# symbolic operators.
00:04
<TabAtkins>
And I dont' think that's an unreasonable thing to propose on its own, but as a way of avoiding Hack-style pipelines, it's just piling more and more new features ^_^
00:04
<rbuckton>
Basically `{+}` which means `(a, b) => a + b`, plus fixed arguments: `2 {+}` meaning `(b) => 2 + b`
00:05
<rbuckton>
So, you'd end up with:
00:05
<rbuckton>
```
00:05
<rbuckton>
x |> say(?)
00:05
<rbuckton>
|> {+} 1
00:05
<rbuckton>
|> print(?)
00:05
<rbuckton>
```
00:05
<rbuckton>
Or
00:05
<rbuckton>
```
00:05
<rbuckton>
[1, 2, 3, 4].reduce({+}, 0)
00:05
<rbuckton>
```
00:06
<rbuckton>
Or even:
00:06
<rbuckton>
```
00:06
<rbuckton>
x |> filter({>} 0)
00:06
<rbuckton>
|> map({+} 1)
00:06
<rbuckton>
|> reduce({*}, 1)
00:06
<rbuckton>
```
00:07
<rkirsling>
I'd just as well have Operator.add or something at that point, I think
00:07
<TabAtkins>
I mean, it's not bad. Now do the same thing for all the other cases in the comparison slide. ^_^
00:07
<rbuckton>
rkirsling: Yeah, I have a package with operators like that.
00:08
<rbuckton>
TabAtkins: `|> _ => (/*whatever was in the slide*/)` ?
00:08
<TabAtkins>
Yes, you're proposing ways to avoid having to write an arrow function
00:09
<TabAtkins>
partial-application, operator-funcs, etc
00:10
<msaboff>
ljharb Do you know where the 2021 Ecma/TC39/2021/012 document was posted? All I can find is https://tc39.es/ecma262/ which looks like the evolving draft.
00:11
<rkirsling>
it's on the releases page
00:11
<rkirsling>
https://github.com/tc39/ecma262/releases/tag/es2021-candidate-2021-03
00:12
<rkirsling>
namely https://github.com/tc39/ecma262/releases/download/es2021-candidate-2021-03/ECMA-262.12th.edition.June.2021.pdf
00:12
<rkirsling>
er
00:12
<rkirsling>
that's the PDF version, I mean
00:12
<rkirsling>
HTML version is https://tc39.es/ecma262/2021
00:12
<msaboff>
rkirsling Thanks
00:13
<rkirsling>
sure!
00:28
<rbuckton>
```
00:28
<rbuckton>
const Op = {
00:28
<rbuckton>
set(obj, key, value) { obj[key] = value; return obj; },
00:28
<rbuckton>
new(f, ...args) { return new f(...args); },
00:28
<rbuckton>
};
00:28
<rbuckton>
x |> o.m(?)
00:28
<rbuckton>
x |> o.m(0, ?)
00:28
<rbuckton>
x |> Op.new(o.m, ?)
00:28
<rbuckton>
x |> ? {+} 1
00:28
<rbuckton>
x |> Array.of(0, ?)
00:28
<rbuckton>
x |> Op.set({}, "key", ?)
00:28
<rbuckton>
x |> o.m(?) |> await
00:28
<rbuckton>
x |> o.m(?) |> /* tough one because `yield` is valid on its own... */
00:28
<rbuckton>
```
00:34
<rbuckton>
Meta operators (mostly tongue-in-cheek idea, but maybe...?):
00:34
<rbuckton>
```
00:34
<rbuckton>
x |> o.m(?) |> await.?
00:34
<rbuckton>
x |> o.m(?) |> yield.?
00:34
<rbuckton>
```
00:34
<TabAtkins>
That works! Note tho that every single line you wrote is either identical to Hack-style, or longer/more complex/new features.
00:35
<rbuckton>
the `Op.new` thing is because I currently disallow `new f(?)`, but I could remove that restriction.
00:36
<TabAtkins>
Sure, the exact set of ops isn't that imporant; you need more than the two you listed anyway, like `get`
00:36
<rbuckton>
Also, I was thinking that F# has multiple pipeline operators: `|>`, `||>`, `|||>`. Though for those its about piping multiple inputs (i.e., `a b ||> f` is equivalent to `f a b` in F#)
00:37
<TabAtkins>
JS has that reasonably covered already - `[a, b] |> f(...#)`
00:37
<rbuckton>
We could have both (as long as the topic and the placeholder don't share the same token).
00:38
<rbuckton>
```
00:38
<rbuckton>
x ||> [0, #]
00:38
<rbuckton>
```
00:38
<rbuckton>
I just don't like overloading the syntax that much though.
00:39
<TabAtkins>
I agree that I'm not a fan of mixing partial-app and Hack-style. I just think that the result of that unease is to regard it as one more strike against partial-app.
00:39
<rbuckton>
the only thing infeasible with F#+papp compared to Hack is yield. Honestly though, if you're yielding in the middle of a pipeline that seems a bit like code-smell...
00:40
<TabAtkins>
Likely, yeah; `yield` is important to me only in the sense that we carved out a special exception to make `await` work, but left this other very similar keyword out in the cold.
00:40
<TabAtkins>
It feels dirty, is all. But I don't think it's important in practical terms.
00:41
<TabAtkins>
Very much a "theoretical purity"-level concern in the priority of constituencies.
00:41
<rbuckton>
We could do the same thing we do in async generators with `yield promise` and just have `|>` await the result for you...
00:41
<rbuckton>
Not that I'm a fan of that
00:42
<TabAtkins>
Yeah, not a fan of that magic outside of the explicit async boundary.
00:42
<rbuckton>
but something explicit like `await.?` informing the pipeline would work for `yield.?` too...
00:43
<TabAtkins>
Like I said during the talk, ultimately the two are *so close to identical*. Ignoring await, *every single* Hack-style pipeline like `val |> XXXXX` becomes an F#-style with `val |> x=>XXXXX`. And *every single* F#-style pipeline like `val |> XXXXX` becomes a Hack-style with `val |> XXXXX(#)`. Doesn't matter how complex the XXXXX expression is, the transform works in 100% of cases (ignoring await).
00:44
<rbuckton>
`await` and `yield` violate TCP, so they're always going to be tough to work around.
00:45
<TabAtkins>
With await, the only diff is that F#-style *requires* you to unfold the XXXXX into two or three steps, with the promise isolated in the middle step. `val |> foo(await fetch(#))` must be unfolded into `val |> fetch |> await |> foo`; you can't write `val |> x=>foo(await fetch(x))`.
00:46
<TabAtkins>
(Technically you could write `val |> async x=>foo(await fetch(x)) |> await`, but ugh.)
00:47
<rbuckton>
I was also considering that `|> yield` could be a special form like `|> await`, and if you *really* wanted to yield `undefined` you could use parens or `do` or something:
00:47
<rbuckton>
```
00:47
<rbuckton>
x |> m.o(?) |> yield |> print(?); // basically `print(yield(m.o(x)))`
00:47
<rbuckton>
const memo = _ => () => _;
00:47
<rbuckton>
x |> m.o(?) |> memo(yield) |> print(?); // basically `print((m.o(x), yield))`
00:47
<rbuckton>
```
00:50
<TabAtkins>
I don't understand how the `memo(yield)` part works. That doesn't produce a function.
00:50
<rbuckton>
F#+papp only requires you to unfold the steps if you are awaiting the "topic":
00:50
<rbuckton>
```
00:50
<rbuckton>
x |> o.m(?, await p); // perfectly reasonable.
00:50
<rbuckton>
```
00:50
<rbuckton>
Since you only have a single "topic", awaiting it in the middle isn't that burdensome (and if anything is clearer)
00:50
<rbuckton>
`memo` produces a function
00:50
<TabAtkins>
rbuckton: The moment your expression is complex enough to need an arrow-func, not partial-app or operator-funcs or what-have-you, it fails regardless of what you're awaiting.
00:51
<rbuckton>
It produces a function that always returns the argument passed to `memo`:
00:51
<rbuckton>
```
00:51
<rbuckton>
const memo = _ => () => _;
00:51
<rbuckton>
const f = memo(3);
00:51
<rbuckton>
console.log(f()); // 3
00:51
<rbuckton>
console.log(f()); // 3
00:51
<rbuckton>
console.log(f()); // 3
00:51
<rbuckton>
```
00:51
<TabAtkins>
rbuckton: Right, but `yield` isn't a function.
00:52
<rbuckton>
Yeah, you evaluate an expression that returns a function. `memo(yield)` is an expression. I was illustrating the exceedingly rare case of "I want to `yield` the value `undefined` and return its result"
00:52
<rbuckton>
That's the reason `|> yield` doesn't work but `|> await` does.
00:52
<rbuckton>
`await` requires an operand, `yield` does not.
00:52
<TabAtkins>
Ohhhh, okay, so it's not the same as the preceding line (which interprets `yield` as a special form that yields the topic)
00:52
<jridgewell>
Yield should behave the same as await.
00:53
<rbuckton>
no, I was explicitly illustrating the "if you *really* wanted to yield `undefined` you could use parens or `do` or something:" case
00:53
<TabAtkins>
Okay, using the parens from a wrapping function call was confusing me. ^_^
00:53
<rbuckton>
jridgewell: As I said, the reason it doesn't is that `yield` (no operand) is a valid expression in JS
00:53
<rbuckton>
So `x |> yield` might mean something different to one person vs. another.
00:53
<jridgewell>
Yes, in pipe, it should be treated like `await`
00:54
<rbuckton>
It could mean, "I want to yield x", or it could mean "I want to call the function resulting from yielding `undefined` with the value of `x`
00:55
<rbuckton>
jridgewell: I was proposing we do something like `|> await.?` and `|> yield.?` to be more specific about the behavior, but I generally agree. `|> yield` should be treated like `|> await` if it comes down to it.
00:55
<jridgewell>
That's resolve by `|> (yield)` vs `|> yield`
00:55
<rbuckton>
If you want the "I want to call the function resulting from yielding `undefined` with the value of `x case just do `|> (yield)`.
00:55
<rbuckton>
yeah
00:56
<jridgewell>
The first invokes the return of yield, and the second yields the pipeline arg
00:56
<jridgewell>
But this all comes down to `yield` being inherently incorrect, it should have been spec'd like `await`
00:57
<jridgewell>
It always takes an arg, and if you wanted to yield nothing, do `yield undefined`
00:57
<rbuckton>
Requiring that `yield` have an operand is a very "iterator-centric" point of view and ignores other generator/coroutine-like scenarios.
00:58
<rbuckton>
You might always just want to write `const x = yield` since it only matters what you're sent, not what you receive.
01:00
<jridgewell>
That's making very similar operators behave very differently for, what I imagine, is .0000000000000000000001% of the uses.
01:00
<rbuckton>
Regardless, its too late to change that.
01:00
<rbuckton>
I agree that we could make the distinction that `|> await` and `|> yield` behave the same.
01:01
<rbuckton>
In F# pipes, if you wanted pipe `x` into a function stored in a Promise, you'd have to do `x |> (await pfn)` anyways
01:01
<rbuckton>
No different from `x |> (yield y)` really
01:24
<TabAtkins>
Why would you have to do that? Aside from the "bare await" syntax carve-out, the RHS is just an arbitrary expression that must resolve to a function, so in F#, `x |> await pFn` should be completely valid.
01:25
<jridgewell>
https://babeljs.io/repl#?browsers=defaults%2C%20not%20ie%2011&build=&builtIns=false&spec=false&loose=true&code_lz=B4AgPgfCCGDu0EsAuIAOAxAdkA&debug=false&forceAllTransforms=false&shippedProposals=false&circleciRepo=&evaluate=true&fileSize=false&timeTravel=false&sourceType=module&lineWrap=true&presets=stage-0&prettier=true&targets=&version=7.13.10&externalPlugins=
01:26
<jridgewell>
We forbid await without parens to avoid the ambiguity
01:26
<jridgewell>
But it could be made lega
01:26
<jridgewell>
legal**
01:26
<TabAtkins>
That's not an unreasonable position to take, imo.
01:27
<jridgewell>
Oh, Babel didn't even implement `x |> await`, though
01:27
<jridgewell>
So I have no idea.
01:29
<rbuckton>
A) we need a way to `await` the "topic"
01:29
<rbuckton>
B) we want to reduce confusion and be consistent.
01:29
<rbuckton>
If we want consistency between `await` and `yield`, then we need to explicit.
01:29
<rbuckton>
`|> await` has no operand and awaits the topic
01:29
<rbuckton>
`|> (await p)` awaits `p` and invokes the result with the topic
01:30
<rbuckton>
`|> yield` has no operand and yields the topic
01:30
<rbuckton>
`|> (yield)` yields `undefined` and invokes the result with the topic
01:30
<rbuckton>
`|> (yield p)` yields `p` and invokes the result with the topic
01:30
<rbuckton>
Both `|> await` and `|> yield` would need NLTs and restrictions to forbid a leading `AwaitExpression` or `YieldExpression`.
01:30
<rbuckton>
Also, due to the precedence of `yield`, you probably need to parenthesize it since `yield`'s precedence will probably conflict with `|>`
01:31
<rbuckton>
```
01:31
<rbuckton>
// if no parens, for this:
01:31
<rbuckton>
x |> yield |> F
01:31
<rbuckton>
// you probably wanted:
01:31
<rbuckton>
(x |> yield) |> F
01:31
<rbuckton>
// but you got this instead:
01:31
<rbuckton>
x |> (yield |> F)
01:31
<rbuckton>
```
01:31
<rbuckton>
or rather
01:32
<rbuckton>
That example was incorrect.
01:32
<rbuckton>
```
01:32
<rbuckton>
x |> yield a |> F
01:32
<rbuckton>
// would be
01:32
<rbuckton>
x |> yield (a |> F)
01:32
<rbuckton>
// but you wanted
01:32
<rbuckton>
(x |> yield a) |> F
01:32
<rbuckton>
``
01:32
<rbuckton>
```
01:33
<TabAtkins>
Agreed.
01:33
<rbuckton>
So, forcing the parens is better for user expectations:
01:33
<rbuckton>
```
01:33
<rbuckton>
// no precedence concerns
01:33
<rbuckton>
x |> (yield a) |> F
01:33
<rbuckton>
```
01:34
<rbuckton>
So, if we want to avoid precedence issues and remain consistent, `|> await` and `|> yield` would be special forms, and if you don't want the special forms, you use `|> (await ...)` and `|> (yield ...)`
01:34
<TabAtkins>
rbuckton: For your point A), I think a more general statement is that we need a way to `await` *things* inside a pipeline. The topic is one thing and often what you'll want, but not the sole thing, depending on what your code is doing.
01:35
<TabAtkins>
Not unreasonable to start from `val |> x=>foo("arg", x)` and later realize you actually need to grab `"arg"` from the network.
01:35
<rbuckton>
Yeah. Outside of the position immediately to the right of `|>`, you could use `await` anywhere else in the pipeline expression, i.e. `x |> foo(?, await y)`.
01:35
<TabAtkins>
Sucks if `val |> x=>foo(await fetch("/arg"), x)` doesn't work and there's no way to make it work without contortions
01:36
<rbuckton>
Well, that wouldn't work unless you made the arrow async anyways...
01:36
<TabAtkins>
Again that point is *only* valid so long as you can avoid an arrow-function wrapper.
01:36
<TabAtkins>
("that point" being that you could just drop an `await` into partial-app)
01:37
<rbuckton>
```
01:37
<rbuckton>
x |> foo(await fetch("/arg"), ?)
01:37
<rbuckton>
```
01:37
<rbuckton>
Would be valid in F#+papp
01:37
<TabAtkins>
Yes, because the RHS happens to be simple enough to be expressible in partial-app. If that's not true, you need a wrapper function.
01:37
<rbuckton>
The `await fetch("/arg")` happens first, before the papp function is returned.
01:37
<TabAtkins>
And suddenly you need to contort yourself.
01:38
<TabAtkins>
Assume that you're gonna manipulate the topic as well so you can't papp it, like `foo(await fetch("/arg"), ? + 1)`
01:39
<rbuckton>
`x |> ? + 1 |> foo(await fetch("/arg"), ?)` works for that, but I see your point.
01:39
<rbuckton>
well
01:39
<rbuckton>
```
01:39
<rbuckton>
x |> ? {+} 1 |> foo(await fetch("/arg"), ?)
01:39
<rbuckton>
```
01:39
<rbuckton>
rather, if we're talking functional operators...
01:40
<rbuckton>
I guess one of my issues with Hack-style is that you don't need new syntax for it.
01:40
<TabAtkins>
Yeah, my point is that suddenly you're having to do *larger* rewrites from your starting point of `val |> x=>foo("arg", x+1)`
01:41
<TabAtkins>
rbuckton: explain?
01:41
<rbuckton>
```
01:41
<rbuckton>
var _;
01:41
<rbuckton>
(
01:41
<rbuckton>
_ = x,
01:41
<rbuckton>
_ = _ + 1,
01:41
<rbuckton>
_ = foo(await fetch("/arg"), _),
01:41
<rbuckton>
_
01:41
<rbuckton>
)
01:41
<rbuckton>
```
01:41
<rbuckton>
That's a hack pipe with no new syntax.
01:41
<rbuckton>
plus, you can control your topic variable.
01:42
<TabAtkins>
No new syntax, sure. But quite a lot of tax.
01:42
<TabAtkins>
F# doesnt' require any new syntax either:
01:42
<rbuckton>
Not much, you're replacing `|>` with `_=` and `,`
01:43
<TabAtkins>
`function pipe(val, ...fns) { for(const fn of fns) val = fn(val); return val; }` is F# pipe.
01:43
<TabAtkins>
`pipe(val, {+} 1, foo(await fetch("/arg"), ?))`
01:43
<rbuckton>
Except static analysis for type systems sucks for that case.
01:44
<TabAtkins>
Valid, tho not a concern for 90%+ of JS devs. ^_^
01:44
<rbuckton>
In TypeScript, you end up with an overload ladder that eventually bottoms out.
01:46
<TabAtkins>
And given that, as I said earlier, Hack and F# pipelines are *trivially* translatable between each other, if Hack-style is bad because you can already write it in existing syntax, then F# is too.
01:47
<TabAtkins>
Tbf, it does invoke a slightly larger tax - every line would have a `(_)` at the end of it.
01:48
<TabAtkins>
Regardless, tho, "you can use comma and assignment to get the same effect at a similar cost in characters" doesn't fly in practice, because people still love method chaining but don't love it enough to do comma-and-assignment. It feels much, much nastier and heavier-weight even if the raw character weight is similar.
01:49
<rbuckton>
When I started down the road of looking into `|>` and papp several years ago, my design choices were driven by projects like lodash and Ramda. Specifically, designing a syntax that doesn't heavily prefer one over the other as I don't want it to feel like TC39 is "choosing a winner"
01:49
<rbuckton>
this was following on the heels of the earlier `::` bind proposal.
01:49
<TabAtkins>
Yeah, "better `::`" is one of the reasons I came to support `|>` too.
01:50
<rbuckton>
One of the upsides of the F#+papp proposal, is that it worked equally well with both (and other) fp-style libraries.
01:50
<rbuckton>
Hack-style works too, but favors lodash, since Ramda uses currying.
01:54
<rbuckton>
```
01:54
<rbuckton>
// F#+papp
01:54
<rbuckton>
// lodash-style
01:54
<rbuckton>
x |> map(?, _ => _ + 1)
01:54
<rbuckton>
// Ramda-style
01:54
<rbuckton>
x |> map(_ => _ + 1)
01:54
<rbuckton>
// Hack
01:54
<rbuckton>
// lodash-style
01:54
<rbuckton>
x |> map(?, _ => _ + 1)
01:54
<rbuckton>
// Ramda-style
01:54
<rbuckton>
x |> map(_ => _ + 1, ?) // can't leverage Ramda currying
01:54
<rbuckton>
```
02:01
<rbuckton>
Plus, lodash and Ramda both support partial application:
02:01
<rbuckton>
```
02:01
<rbuckton>
// Ramda
02:01
<rbuckton>
R.curry(g)(R.__, 2, 3)(4)
02:01
<rbuckton>
// lodash
02:01
<rbuckton>
_.partial(g, _, 2, 3)(4)
02:01
<rbuckton>
```
02:01
<rbuckton>
But that requires a fair amount of code beneath the surface to support, vs:
02:01
<rbuckton>
```
02:01
<rbuckton>
g(?, 2, 3)(4)
02:01
<rbuckton>
```
02:30
<TabAtkins>
Sorry, wife came home and I had to rush off to make dinner. I'll pick up tomorrow.