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. |