Skip to content

To what extent is functional-style not already addressed in userland? #164

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
tabatkins opened this issue Apr 4, 2020 · 18 comments
Closed

Comments

@tabatkins
Copy link
Collaborator

tabatkins commented Apr 4, 2020

I've been thinking on this a bit again, and as far as I can tell, most of the functional-style's functionality can be done purely in userland, with similar syntax weight.

(Edited to add: bad wording here; by "userland" I really just mean "as an ordinary function". Even if it could be done by authors, there's still some optimization-related benefits to be had if it was provided as a built-in, not to mention the ecosystem benefits of having it in a standardized, always-supported form.)

That is, to unfold:

exclaim(capitalize(doubleSay("foo")))

The functional style would be written as:

"foo" |> doubleSay |> capitalize |> exclaim

But this could be done in userland already as:

function pipe(val, ...funcs) {
  for(const f of funcs) val = f(val);
  return val;
}

pipe("foo", doubleSay, capitalize, exclaim)

This is nearly identical in syntax weight, and carries the same readability benefits afaict.

The only additional functionality unlocked by the functional-style operator seem to be:

  1. Piping to an object's method, a la "foo" |> bar.baz. Note that the userland impl can handle this just fine when the object is just a namespace, like pipe("foo", console.log) or pipe(5, CSS.px), or a module record; it's only actual object methods that userland needs to wrap in a function (pipe("foo", x=>bar.baz(x))).

  2. Awaiting a value in the pipeline. The operator has a special form that lets it do this; userland is more complicated:

    async function apipe(val, ...funcs) {
    	val = await val;
    	for(const f of funcs) val = await f(val);
    	return val;
    }
    
    await apipe("foo", doubleSay, fetchCapitalFromNetwork, exclaim)

(Note that the user has to await the result. No need to worry about asyncness within the pipe, tho; they can freely mix sync and async there.)


Is there anything else that the functional-style brings to favor it over a userland impl?

@noppa
Copy link
Contributor

noppa commented Apr 4, 2020

There's already several discussions about adding a pipe function instead of an operator in the issues. #154 (closed), #131 and #146

I guess what you are proposing is a bit different because those issues are about adding a new function to the spec while you are suggesting that it's left for users to implement (i.e. keeping the status quo). Still, there's some general discussion about function vs operator in there that applies here too.

@mAAdhaTTah
Copy link
Collaborator

I'd suggest there's a subjective readability benefit to the operator that you don't get. A long function call is subjectively more onerous to read because pipe(line operator) encourages longer chains of function calls, and on those longer chains, the explicitness of the operator itself next to each line/call makes it easier to track, that saying so at the top with a long line of commas.

My understanding also is that engines would be able to optimize away some cases of inline arrow functions, eliminating the overhead of the function call and perhaps even the creation of the function object itself. This wouldn't be feasible in a userland pipe function because the engine couldn't know the behavior of pipe in advance.

Lastly, an operator would be much easier to manage for static type systems. I don't know how much influence that has on JS's language design, but |> would be easier to get right in a static type system. It's possible to type pipe so that it works in TypeScript, and Flow introduced special types to handle pipe & compose, but I think both of those solutions are inferior to the operator.


I don't know if these are functional-specific (I'd argue the third is, at least), so may not be exactly what you're looking for, but those are some advantages the operator has over the pipe function at least.

@tabatkins
Copy link
Collaborator Author

(Note before I start responding: my point in starting this thread is to make sure that my arguments against the functional-style pipeline aren't missing anything major. I intend to argue for topic-style pipeline.)

I guess what you are proposing is a bit different because those issues are about adding a new function to the spec while you are suggesting that it's left for users to implement (i.e. keeping the status quo).

Oh, not necessarily. Whether this is actually done in userland, or JS defines a built-in for it, doesn't matter much to me.

There's already several discussions about adding a pipe function instead of an operator in the issues. #154 (closed), #131 and #146

Thank you for the links! They were very helpful; my exact sync pipe() was already talked about, and a cleverer async pipe was also given; neat.

A long function call is subjectively more onerous to read because pipe(line operator) encourages longer chains of function calls, and on those longer chains, the explicitness of the operator itself next to each line/call makes it easier to track, that saying so at the top with a long line of commas.

Yup, def a valid point, but a more minor one. The major benefit of pipeline is unfolding nested function stacks into linear, reading-order-is-execution-order code. We already have syntax for some instances of this, in the form of method chaining. I support making it easier to do this more widely (method chaining is popular for a reason), but if we can do it without minting new syntax, all the better.

That is still a valid point, tho, so thanks.

My understanding also is that engines would be able to optimize away some cases of inline arrow functions

Right, a JS built-in that still looks like a function could get around that, so not a concern for my purposes.

(I'm somewhat dubious about how much could really be optimized away, but JS engines have done plenty of miraculous things, so who knows.)

Lastly, an operator would be much easier to manage for static type systems.

Yeah, writing the type of an n-ary compose is... tricky. But it looks like (from reading the other threads linked) that Flow, at least, provides such a type directly precisely to allow things like this to be typed more easily, and it looks like people have figured out how to get TS to deduce the type.

But also, a built-in function for it could be given proper typing directly, so this isn't a concern for my purposes.

@mAAdhaTTah
Copy link
Collaborator

if we can do it without minting new syntax, all the better.

This is effectively the argument for F#: instead of creating a topic token and its associated complexities, lean on the existing syntax of the arrow function. I don't personally think that's a strength of the Smart Pipeline.


Right, a JS built-in that still looks like a function could get around that, so not a concern for my purposes.

I honestly have no idea how that would work. If it's a variable or a method (e.g. Function.pipe), those seem dynamic enough to make optimization much harder. How could the engine know at the call site that this is the built-in vs some modification from userland? Unless it was a built-in module or something like that. That said...

(I'm somewhat dubious about how much could really be optimized away, but JS engines have done plenty of miraculous things, so who knows.)

...maybe it could be, I am not an engine dev, what do I know?

I will also add that the Babel plugin itself already optimizes away a number of arrow function cases. My assumption is the much-more-clever engine developers would be able to expand on that.


it looks like people have figured out how to get TS to deduce the type.

The types exist, yes, but I've experienced enough headaches with typing them that I don't really use them. Maybe someone can chime in if they've seen this improve (I know generic functions in pipelines were painful).

@tabatkins
Copy link
Collaborator Author

This is effectively the argument for F#: instead of creating a topic token and its associated complexities, lean on the existing syntax of the arrow function. I don't personally think that's a strength of the Smart Pipeline.

(I'm not wanting to debate merits of the proposals in general in this topic, but I will note that functional-style still has to invent special-case syntax for some of its cases, notably await, while topic-style can just use standard syntax with no special rules. So, once you've accepted the use of the new operator, further syntax complexities cut both ways. This topic is about enumerating what benefits the pipeline operator itself bring to functional-style.)

I honestly have no idea how that would work. If it's a variable or a method (e.g. Function.pipe), those seem dynamic enough to make optimization much harder. How could the engine know at the call site that this is the built-in vs some modification from userland?

Oh, engines do this all the time actually, tracking whether you've overridden a built-in or not, and using a more optimized, less pessimistic implementation if it's untouched.

@mAAdhaTTah
Copy link
Collaborator

Ok, I'll hold off on that debate until later then.


Well then, "what do I know" indeed. That said, that wouldn't apply to a "userland" pipe (which I probably should have mentioned upthread), only a built-in of some kind, ya?

@tabatkins
Copy link
Collaborator Author

Right.

Also, I put in a clarifying paragraph in the OP (marked explicitly as an edit to avoid making later comments seem weird) clarifying that I was using words badly, and I really meant "pipeline as an ordinary, possibly built-in, function". Thanks for the push to clarify it. ^_^

@noppa
Copy link
Contributor

noppa commented Apr 4, 2020

The partial application proposal promises to use a shallow stack, so

function divide(numerator, denominator) {
  if (denominator === 0) throw new Error('Division by zero');
  return numerator / denominator;
}

return 0 |> divide(5, ?)

Would give you a nice, short stack trace like

Uncaught Error: Division by zero
    at divide

I didn't see mention of this in the Smart proposal, but I'd expect the same result there.

AFAIK the spec doesn't give any tools to observe stack from userland, but I'd imagine the implementers would want to keep the current behavior of Error.stack as it is now, so with a global Function.pipe at best it would look more like

Function.pipe(
  0,
  _ => divide(5, _)
)
Uncaught Error: Division by zero
    at divide
    at <anonymous>
    at Function.pipe

// Little better with .bind but not ideal for UX/readability

Function.pipe(
  0,
  divide.bind(null, 5)
)
Uncaught Error: Division by zero
    at divide
    at Function.pipe

Performance-wise, I have no idea how well the engine would still be able to treat
0 |> _ => divide(5, _) like it was divide(5, 0) if they have to keep record of the anonymous functions for possible stack traces.

@tabatkins
Copy link
Collaborator Author

I didn't see mention of this in the Smart proposal, but I'd expect the same result there.

Yes, 0 |> divide(5, #) doesn't imply any sort of temporary function creation at all, so you'd just get the expected stack showing only divide(); no need to worry about special-casing.

@pygy
Copy link
Contributor

pygy commented Apr 25, 2020

Yup, def a valid point, but a more minor one.

@tabatkins for you, perhaps, but that means you're not in the target audience for this proposal.

Infix math operators are not needed, semantically, but they make math-heavy code more approachable. The same goes for the pipeline. Point-free composition is a major pattern in FP, being able to tell it apart from other function calls helps make the code more clear, visually. In a typical app written by an FP mind, function composition is more prevalent than math.

FWIW, I wouldn't be in the target audience either (I (try to) think functionally, but serialize my thoughts to plain JS), but I hang around people who would.

@tabatkins
Copy link
Collaborator Author

Given that I asked this question in the context of putting together slides for a presentation on the pipeline operator, I think I'm 100% the target audience for this proposal. ^_^ I just wanted to make sure I wasn't missing anything obvious in the functionality of the functional-style syntax specifically that would make it more competitive amongst the possible syntaxes.

@pygy
Copy link
Contributor

pygy commented Apr 27, 2020

@tabatkins FP in JavaScript eschews statement composition for function composition. It's all function calls.

Without the pipeline, you end up in Lisp land... Oatmeal with fingernails clippings.

I didn't mean that you're not welcome to discuss this, and I'm glad you're setting your sight on this, but the fact that you see the visual aspect of syntax as a minor point is significant. The people behind the proposal do see syntax as a major aspect, because the subset of JS they use doesn't have any otherwise.

Edit: "not any" is an exageration, but there's, comparatively, a dearth of syntax when programming functionally in JS.

@aadamsx
Copy link

aadamsx commented Apr 27, 2020

@tabatkins is it true you're one of the "committee" members?

I've been thinking on this a bit again, and as far as I can tell, most of the functional-style's functionality can be done purely in userland, with similar syntax weight.

So for all the conversations we've had on this proposal via this repo and elsewhere, for the babel implementation, and for the proposal to get past stage-1 and the work on stage-2, you got from that that this can all be done in userland, with the implication there's no need for a |> operator at all?

Not singling you out, but in general we could have a Tower of Babel situation going on around here, where we're speak past each other, with different agendas, and in different "languages".

Is there anything else that the functional-style brings to favor it over a userland impl?

I think the operator allows functions to be composed together in a much more readable way that looks like a fluent style query syntax. We could achieve something similar by writing pipe("foo", doubleSay, capitalize, exclaim) but the |> operator and the partial application syntax save us some key strokes and looks a hell,of,a,lot,better.

.

Just take the class keyword addition as an analogy. Many in the JS community want the |> for most of the same reasons. JS is a multi-paradigm, inclusive language, we should foster this with an addition to the language that would excite and please many.

@pygy
Copy link
Contributor

pygy commented Apr 27, 2020

There was a ton of demand for class.

I mentioned elsewhere that classes were good at modeling artificial problems. The corporations is the quitesential human contraption, and OOP was unsurprisingly a good match when the time came for automating them.

Corporations were full of programs that had been written for people not only to read, but also to interpret.

These workflows that had often been designed by people without systems qualification, and, given is therefore logical that these processes relied on abstractions that are easy on the brain, even if conceptually sub-optimal.

Taxonomy is easy on the brain, and corporations are almost universally built as hierarchies. Per Conway's law, it isn't surprising that classes and inheritance ended up being a great tool in the industrty. Also a good cultural fit.

This created a large cohort of smart people who think in OO and who applied their skills to other domains.

... and those folks wanted to use their skills in the browser, and some of them were shaming JS for not having classes.

So having classes in JS was important from a mostly sociological standpoint.

However, now that we have these folk's attention, I'm not sure that dedicating that much energy to classes is a good use of the TC's time.

JS is about to lose the exclusivity in its niche (IE11 will soon be out, and WASM-derived solutions will thus become mainstream). You want to skate where the ball is going to be, not where it was five years ago. I don't know if FP as it is done today is where the future lies, but it is a step in the right direction. It is currently a fertile breeding ground of ideas (reactive streams, lenses, patch-oriented state management AFAIK all originate from there). Encouraging FP in JS today is a good bet for the language's future.

@tabatkins
Copy link
Collaborator Author

I'm gonna stop responding here, because I don't know how many times I can repeat that I want a pipeline operator, and was just making sure there was nothing I was missing functionality-wise in the functional-style syntax.

@aadamsx
Copy link

aadamsx commented Apr 28, 2020

Sorry @tabatkins from the title and the first post by you, it seemed you were making the case against it. After looking at all your comments and turning my head sideways, I can see what you're trying to do (but it wasn't obvious to start).

Also, you say you're going to argue for topic-style pipeline? Why that style? Why not F#? And why not just argue for both styles to be implemented (and have the "committee" pick which one they prefer)?

@pygy
Copy link
Contributor

pygy commented Apr 29, 2020

@aadamsx Tab is on the committee, and having more votes who count behind any proposal is a plus at this point.

The winds seems to be turning towards "topic"-based pipes, which I'm afraid will kill partial application (too close in syntax and purpose to have both). P-app is useful in its own right in non-pipeline scenarios, but a little worse in pipelines than topic/smart pipes, even when combined with F# style.

So while "topic/smart" is IMO [ missing the forest for the tree / aiming for a local maximum / too far on the Playmobil side of the Lego-Playmobil scale ], having pipelines land at all is still a major plus.

@mAAdhaTTah
Copy link
Collaborator

I know this thread is closed, but while working on a project this evening, and doing a somewhat-annoying composition without either pipe or |>, I notice that, given the choice between refactoring into pipe vs refactoring into|>, the ergonomics of that refactor would strongly favor |>. Putting the commas & parens in the right place in a chunk of code that already has a couple of parens & commas is not fun.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Sep 24, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants