Skip to content

Latest commit

 

History

History
205 lines (128 loc) · 12.1 KB

06-17.md

File metadata and controls

205 lines (128 loc) · 12.1 KB

June 17, 2021 Incubator Call Notes (Pipeline)

Attendees:

  • Shu-yu Guo (SYG)
  • Tab Atkins (TAB)
  • Daniel Ehrenberg (DE)
  • Ron Buckton (RBN)
  • Chiyi Pin Lim (LCP)
  • John Hax (JHX)
  • Jordan Harband (JHD)
  • Hemanth HM (HHM)

Pipeline

TAB: I haven't had enough energy to devote more time to the proposal. With any luck this meeting can be the start of more time devoted to pipeline.

That said most important question before we bring it up again at a plenary, core question is what syntax are we supporting? Once we decide that all other work is detail work that are uncontroversial.

My preference here is for Hack-style quite strongly, but I'm okay with either solution.

(Presents old slide)

When we presented that slide, committee members found it pretty convincing in favor of Hack pipes. Lays out clearly there's exactly one case where F# pipes express things clearer and shorter than Hack. All other cases F# pipes require you to wrap an arrow function while Hack lets you write expression directly. This has been core disagreement between champions so far so I'm not convinced we can just go with Hack pipes. I want to hear other folks' thoughts on how to move forward.

RBN: I saw a tweet the other day that was exactly like this comparing stuf in frameworks. This slide is missing F# + partial-application combination (?). When I first brought this up, I wanted to bring together partial application and a way to do leading/trailing args together.

I've been chatting with Yulia who may have interest in co-championining partial application. From her user studies partial application has interest from community much more so than pipeline.

To me F# + partial-application still makes sense to me as the solution. Wanted to point out this slide is missing a key puzzle piece.

TAB: With partial application the first 3 rows of the slide would then look exactly like Hack pipes. The rest of the rows remain same.

LCP: I agree with Ron about partial application. With Hack pipes you get the sense that partial application is supported everywhere, not just in pipes. Seems to break a lot of parsing and how you intuitively think about functions. Hack pipes is a bit mind boggling, you think you're looking at an expression but it isn't.

RBN: Partial application follows TCP but Hack doesn't.

TAB: It's interesting you all have that mental model. Hack pipes don't create functions, it's just application. If you think you're creating functions (e.g. via partial application) that are implicitly executed by the pipelines, yeah that'll be confused. If you start from that place you'll be confused. You're just executing expressions with Hack pipelines.

LCP: That's interesting. A typical approach is to pass in a function, because in most languages when you have a pipe operator the RHS operand is always a function. Now we're changing that mental model to expression-based instead.

TAB: Not true that most pipes do that. Wide variety of different pipelines. Here we have F# and Hack-style. Elixir has a different way where it's just function calls, no function objects. Wide variety of similar-looking but very different mental model-backing ways of expressing a pipeline across the ecosystem. Idea that the RHS is always a function that's implicitly executed it's not widespread and far from the only way to do it.

LCP: I feel the F#-style supports parameter destructuring a little cleaner. It's useful but it's just syntax.

TAB: Makes sense, but if we have do expressions we can get the same benefit in the Hack style by having a destructuring statement as the first one in the do.

(Livecoding)

var x = foo().bar().baz()

baz(bar(foo()))

var x = foo() |> bar |> baz;
var x = foo() |> bar(#) |> baz(#);

var x = foo().await.bar().await.baz().await;

var x = await (await (await foo()).bar()).baz();

var x = foo() |> await # |> #.bar() |> await # |> #.baz() |> await #;

var x = await foo() |> await #.bar() |> await #.baz();

var x = foo() |> await |> x=>x.bar() |> await |> x=>x.baz() |> await;

What is pipelines achieving? Some folks are approaching it as function composition. But I think a more productive way is: The point of pipelines is to linearize code. Method chaining is a ridiculously popular way of writing code because it lets them write methods linearly. Method chaining is so popular it's a big reason why JQuery remains the most popular JS lib in the world for over a decade now. Benefit of pipeline I want to emphasize is that pipeline lets you get the same benefit for all your code.

Await is a wonderful feature but terrible syntax. In Rust await looks like a property access. In today's JS it's awful, as it removes linearity of code flow. In pipeline you get the linearity back as you do in Rust. Await's not a function, so this isn't function composition. Main value is linearization.

HHM: How does error handling work? How do you handle an error in the chain?

TAB: Whatever's in the pipeline body is either a function or an expression, depending on which syntax we go with, so try-catch blocks just don't work. If you need to catch errors at a specific point in the pipeline, then you're probably getting too complex and you should break down the expression.

JHD: You could also put a .catch().

TAB: Yeah you could, but unless it's already named it's probably getting too complex to put inside a single chained expression. Might be okay depending on coding standards.

LCP: I have a question. About function composition. Suppose we want to refactor a very long chain into a pipeline. How do both proposals handle that?

TAB: (Writes in code editor to compare Hack vs F#).

function async firstBit(val) {
	return val |> foo(#) |> await # |> #.bar();
}

var x = val |>
	|> firstBit(#) // hack style
	|> firstBit    // f# style
	|> await # 
	|> #.baz() 
	|> await #;

LCP: Looks good, I want to make sure we support extracting the middle of a long chain reasonably.

TAB: Very important. This use case is why I feel like F# or Hack are the only two choices. Elixir style isn't good for this. IMO either of these two choices are good and are easy to handle.

JHX: What's the problem of the Elixir style?

TAB: (Writes in code editor.)

function elixir(a,b,c) {...}

var x = val |> elixir(a,b);
// =>
var x = elixir(a,b,val);

var x = val |> # + 1; // in hack...
var x = val |> (x=>x+1)() // in elixir??

TAB:. In Elixir style you call a function with fewer arguments than it takes and the pipeline auto fills it in. I don't like this as much. First, it's a unique way to call functions. It gets confusing when you want to do anything more than function calls. Not sure how to do the equivalent of this: x |> # + 1. How do you express that? Convert it into an IIFE? If so, that's nasty. If not and there's some magic, then it's an inconsistent evaluation and I don't like that either. F# and Hack both have the advantage of perfectly consistent evaluation strategies.

RBN: Yeah, not a fan of Elixir. Elixir style has the same problem as 'this' in JS: implicit arguments like an implicit 'this' argument. Only way Elixir would've worked for me is some kind of implicit currying based on argument count, which I don't think would work for JS.

(Presents)

const add = (x, y) => x + y;
const gt = (x, y) => x > y;

// hack
var x = val
    |> map(#, _ => add(1, _))
    |> filter(#, _ => gt(0, _))

// F# + papp
var x = val
    |> map(?, add(1, ?))
    |> filter(?, gt(0, ?))
    |> bar(?0, ?0)
    |> await
    |> yield

// hack + papp?
var x = val
    |> map(#, add(1, ?))
    |> filter(#, gt(0, ?))
    |> bar(#, ?)
    |> #()

// papp
const lte = gt(?1, ?0)
[1, 2, 3].forEach(console.log(?))

We've talked about F# vs Hack-style wrt partial application. Piping a variable through to something like + 1 feels more like an abuse of the feature than using it where it shines. One of the reasons I'm pushing partial application is that partial application works uniformly so I don't have to wrap everything inside an arrow. There are downsides like some syntax aren't quite as nice, like for # + 1. IMO I think once you're doing that you should break them out in other statements.

There can be confusion around topic variables (#) in Hack-style. I have to know the context, could get confusing what topic is being referenced. People have proposed oh you can use ## or ###.

JHD: Why do you want to refer to a topic variable multiple times?

TAB: It's a variable, so you might want to use it multiple times. Example: (console.log(#), #)

RBN: Re: topic variable confusion, if mental model is that it's an argument placeholder instead of a topic variable, partial app makes that mental model easy. Topic variable you have to think about binding, it's not about the application itself.

I agree there are caveats for F#+partial app like bare expression, but I consider those corner cases. If you really need them you can use arrow functions. If you're at that level again I feel you're reaching a level of complexity where it's better for you to refactor into separate statements. This feature really shines for FP where I want to pipe a value through function application. We've been considering ways with partial application to handle this binding. I've been debating on and off if I want to support that.

Another thing: just like Hack style can support multiple arguments, but you'd need to use position arguments like ?0 and ?1, e.g. lte = gt(?1, ?0). One reason I've been pushing partial app is that it gives you a lot more outside of pipeline.

TAB: What bothers me about trying to go for this approach is that it does still make the ability to call a multiarg function simpler, but that's the only thing it makes it easier. The minute you do anything that's not expressed as a bare function call you still have to bust out arrow functions. It buys you a little more in terms of short expressibility it doesn't buy you very much more.

RBN: If you're using libs like Lodash you already got this. JS sits the fence between half OO and half FP so we don't really meet the needs of either. Partial apps adds a powerful tool towards the FP crowd.

SYG: I'm more in favor of Hack style in that linearization for partial application + F# seems much harder to optimize with a proliferation of intermediate arrow functions. I think linearization is going to be reached for often if it becomes a thing, so it's important to optimize this in the frontend without depending on the JITs. With F# + partial app depending so much on intermediate functions, it's probably not as optimizable in the frontend which would be a shame.

RBN: A direct call with partial app doesn't have an observable intermediate function. I'd expect engines to be able to optimize that away. Could be even feasible to put it in the spec.

SYG: Good to know, that makes me feel better but I want to do more investigation.

So I want us to get us directionally aligned. RBN's camp seems to be depending on the value add of partial app outside of pipelines, and don't want pipelines if Hack style is chosen to preclude partial app.

TAB: I'm happy to definitively drop ? here to not block forward progress with partial application.

RBN: Mixing both in the same place would be a weird wart.

JHD: Why would it be confusing? I'd naturally think of partial application as creating new functions, pipelines as pipelines.

TAB: I agree with JHD.

RBN: Sorry I disagree with that intuition.

TAB: I'm 100% on board with Hack and 90% on F# with or without partial app. The syntax that partial app affects is small.

JHD: Agree, but if other implementers agree with SYG on performance then my 90% drops a lot.

RBN: We need to pick one and just go with it, honestly, whether with Hack or F# I need this for my own code.

SYG: I'd like to surface optimizability concerns in plenary to get JSC and SpiderMonkey's thoughts.

Conclusion

3 paths forward:

  • Hack
  • F#, with or without partial app
  • Hack with partial app

AIs:

  • For champion group: Is partial application blocked by Hack-style? Is there a way to support both?
  • For champion group: Pick a style and go with it.
  • For implementers: How optimizable in the frontend is F# + partial app style (and maybe Hack + partial app)? Simple arrow functions thrown in a bunch.