tc39 / proposal-pipeline-operator

A proposal for adding a useful pipe operator to JavaScript.
http://tc39.github.io/proposal-pipeline-operator/
BSD 3-Clause "New" or "Revised" License
7.51k stars 108 forks source link

Tacit programming / point-free style and pipes #206

Open treybrisbane opened 3 years ago

treybrisbane commented 3 years ago

As I mentioned in this comment, it's starting to seem like there are some more fundamental points of debate in this space than just the surface semantics of this proposal. One such point is the idea of point-free programming, and whether or not it should be enabled or encouraged within JS.

So, accordingly, I'd like to ask the question: Should enabling point-free programming/APIs be a goal of the Pipeline Operator?

treybrisbane commented 3 years ago

Incidentally, I personally think it should. General linearization of code is a reasonable goal IMO, but enabling point-free programming within the language gives developers a new way in which to express solutions and/or structure APIs. I do recognize that there are downsides to point-free programming, but I would personally rather have the option. 🙂

(Also, yes I upvoted my own issue, because I suspect people are going to "vote" with upvotes and downvotes, so I figured I might as well start! 😅)

kiprasmel commented 3 years ago

Yes.

This is exactly what Unix scripting does with Pipes:

find . -type f | grep -E '.jsx$' | xargs sed -ibp 's/var/let/g'

Without point-free/tacit/"curried" programming, the Pipeline Operators lose most of their value.

I argue and provide more examples on this in part "1. Function composition" at https://github.com/tc39/proposal-pipeline-operator/issues/205.

mAAdhaTTah commented 3 years ago

Point-free programming is not a goal in and of itself. It's a tool for achieving other goals in your code. The pipe operator is a tool for code linearization. If point-free programming is the tool to achieve that, fine, but that's not the point of the operator.

jderochervlk commented 3 years ago

Point-free programming is not a goal in and of itself. It's a tool for achieving other goals in your code. The pipe operator is a tool for code linearization. If point-free programming is the tool to achieve that, fine, but that's not the point of the operator.

But there is no reason for the operator to block point free?

Yes.

This is exactly what Unix scripting does with Pipes:

find . -type f | grep -E '.jsx$' | xargs sed -ibp 's/var/let/g'

Without point-free/tacit/"curried" programming, the Pipeline Operators lose most of their value.

I argue and provide more examples on this in part "1. Function composition" at #205.

100% this. Pipes are a well known and understood concept. Do thing a, pass the results to the next step, and so on.

mAAdhaTTah commented 3 years ago

But there is no reason for the operator to block point free?

Changing the behavior based on the presence of the token makes the operator harder to reason about because you have to find the token before you can determine whether it's function application or an expression. It makes the behavior more confusing. This was the original Smart-Mix proposal, which had a limited "bare style" for function application.

jderochervlk commented 3 years ago

But there is no reason for the operator to block point free?

Changing the behavior based on the presence of the token makes the operator harder to reason about because you have to find the token before you can determine whether it's function application or an expression. It makes the behavior more confusing. This was the original Smart-Mix proposal, which had a limited "bare style" for function application.

But this is already true for an anonymous function used in .map on an array.

You can do foo.map(add(1)) or foo.map(n => add(1, n)) and get the same results. Why would this existing syntax not work for the |>?

mAAdhaTTah commented 3 years ago

If by "the operator", you mean the Hack pipe, which "blocks" point-free by requiring a topic token, then those two examples aren't equivalent. The "token" that indicates this is an arrow function occurs much earlier, and in a consistent location. The token that indicates whether something is a function application or expression can appear anywhere, which could introduce a refactoring hazard:

x |> f(1)
// desugars to f(1)(x)
// but
x |> f(1) + ^
// desugars to f(1) + x

This is a footgun, as either adding or removing a topic token during refactoring could shift between modes unexpectedly.

Avaq commented 3 years ago

Correct me if I'm wrong here, but I think @mAAdhaTTah and @jderochervlk might be arguing for two different things, perhaps over a small misunderstanding.

When @jderochervlk said:

But there is no reason for the operator to block point free?

I think they meant "But there is no reason to choose Hack over F# specifically on the premise that F# enables point free programming?", or something along those lines. Am I interpreting that correctly @jderochervlk ?

But then @mAAdhaTTah interpreted it as meaning: "But why can't the operator allow tacit style as well as topic style?", and proceeded to make arguments against the "Smart Mix" proposal. Smart Mix, which attempts to combine tacit and topic styles into a single operator, loses benefits for both styles while introducing footguns and other problems.

EDIT: After reading the conclusion of https://github.com/tc39/proposal-pipeline-operator/issues/205#issuecomment-918366356, I'm starting to doubt whether I interpreted @jderochervlk's remark correctly :sweat_smile:

runarberg commented 3 years ago

@mAAdhaTTah

Point-free programming is not a goal in and of itself. It's a tool for achieving other goals in your code.

I would argue that this is a false statement. Library authors quite often have the API style of the library as the main selling point. When I personally pick a library I search for the one that provides the API which I prefer (which is the primary reason I like AVA > Jest). Having the option of point-free programming gives library authors a chance to write a library which provides this API over the other. That makes it a goal of the library author.

mAAdhaTTah commented 3 years ago

I would argue that this is a false statement.

Even in your examples, point-free is servicing the goals of the library consumer, which have larger stylistic goals that point-free enables. The goal is concise, readable code, ease-of-composition, etc. Point-free is a tool for enabling those things, but the goal is not to write point-free code; it's to accomplish those things with point-free code.

Writing point-free code for its own sake is rarely, if ever, the primary goal of an author of a given piece of code. Even the library author in your example chose point-free because the consumer needs the library in that form to accomplish the consumer's goals.

runarberg commented 3 years ago

We might be arguing semantics, but even if you are right and the end goal is better code (as opposed to point free code), I still think that enabling point free programming a worthy goal in and of it self for this reason. That is to allow library authors more diverse options in providing APIs for their consumers. By enabling easy point free programming, we are giving library authors that choice regardless of what their end goal is.

tabatkins commented 3 years ago

I have my own strong opinions on HOF/point-free programming, based on my extensive experience in and love of functional languages, but they're neither here nor there.

As I argued in my essay, the two syntaxes are exactly equivalent in power (assuming F#-style has a special syntax for await/yield), so which to choose is a numbers game: which syntax makes the most code the easiest to read/write/understand, weighting for overall usage. I think it's a pretty easy call that unary functions are a lot less common overall than "everything that's not an unary function" (even if we cut out the things that a partial-application proposal could cover).

More directly germane to this particular discussion, tho, is the fact that F# (and most languages which have chosen that particular style of pipeline syntax) is auto-curried; when you define a function, it knows its own arity, and if you call it with less arguments than it expects, it automatically returns a function that'll take the remaining args. JS doesn't have this (and is unlikely to ever gain it).

The upshot of this is that, in F#, there's no difference between "pipeline that invokes an unary function RHS with the LHS as its argument" and "pipeline that expects a function-call on the RHS, and inserts the topic as the final argument"; that is, you can consider the desugaring to be:

val |> foo(a,b)
// desugars to
foo(a,b)(val)
// or!
foo(a,b,val)

Both are correct! Importantly, this means that in F# you can just write your three-arg function as a three-arg function, and then the user of your library can either call it with three args in normal code, or call it with two args in pipeline code, whichever suits their purposes at the time.

But because JS is not auto-curried, you don't get this equivalency. Unless you pay the additional cost of manually implementing auto-currying or invoking a library that can do it for you, both of which come with runtime and maintainability costs, you have to make a decision up-front on how you intend for your function to be called: in a pipeline, or as a normal function. If the library user ends up wanting to call your function in a different way than what you intended, they have to adapt it; this isn't hard to do, in either direction, but it's still extra work.

That is, if you wrote your function intending it to be pipelined, like const add = a=>b=>a+b, then if the user wants to call it normally they have to write add(1)(2), which is foreign to JS and quite weird. If you wrote your function intending it to be called normally, like const add = (a,b)=>a+b, then if the user wants to use it in a pipeline they have to write 1 |> x=>add(x,2). Either way, this is an inconvenience to the user, and requires an extra up-front decision for the library author that they previously didn't need to think about. (Authors of libraries that want to interoperate with HOF libraries do have to make this exact decision today, but the vast majority of library authors don't.)

Compare with Hack-style, where the library author just writes a 2-arg function as taking two args (const add = (a,b)=>a+b;), and then the library user can either call it normally in the idiomatic way add(1,2) or put it in a pipeline in a straightforward way 1 |> add(^,2) that looks the exact same as the normal call. The author doesn't have to make any new decisions on how to write their function, and the user has the exact same function-calling experience in either style; the pipeline, if they choose to use it, is solely letting them pull a complex argument out in front, is all.


Whoops, this ended up longer than I wanted, but I hadn't put this reasoning into words in this repo yet, so that's probably valuable.

In short: encouraging point-free style is awkward in JS because JS isn't auto-curried.

jderochervlk commented 3 years ago

@Avaq

But there is no reason for the operator to block point free?

I meant to suggest that it is a negative for the hack style pipe to force a function to have a defined parameter. The F# style does not force you to have point free functions (you can use a => a if you want), the hack style does not allow point free functions, which to me will make it not really feasible to adopt since it will just make things more verbose or force me to write my code differently.

js-choi commented 3 years ago

I appreciate this issue because it gets to the heart of a lot of the disputation. Kudos here to @treybrisbane for doing so. We should have addressed this in the explainer in the first place, and it was our bad for not doing so.

One thing I particularly appreciate about this issue is that it does not conflate “JavaScript functional programming” with “JavaScript tacit programming / point-free style”. The two are different. Tacit programming might be a subset, but it is certainly not equivalent to functional programming. Plenty of functional programming happens in a point-free way (see @getify’s https://github.com/tc39/proposal-pipeline-operator/issues/205#issuecomment-918352755 for a personal example).

In fact, non-tacit higher-level-function programming is the purpose of many functional languages’ built-in syntaxes, like the do notation of Haskell or the computation expressions of F#. From what I recall, the question of when to do tacit vs. non-tacit programming is in fact controversial in many functional programming languages’ communities.

I think F#’s documentation itself brings a lot of wonderful wisdom about tacit programming. It’s important enough that I’ll reproduce it here:

F# supports partial application, and thus, various ways to program in a point-free style. This can be beneficial for code reuse within a module or the implementation of something, but it is not something to expose publicly. In general, point-free programming is not a virtue in and of itself, and can add a significant cognitive barrier for people who are not immersed in the style.

Do not use partial application and currying in public APIs

With little exception, the use of partial application in public APIs can be confusing for consumers. Usually, let-bound values in F# code are values, not function values. Mixing together values and function values can result in saving a few lines of code in exchange for quite a bit of cognitive overhead, especially if combined with operators such as >> to compose functions.

Consider the tooling implications for point-free programming

Curried functions do not label their arguments. This has tooling implications. [
] At the call site, tooltips in tooling such as Visual Studio will give you the type signature, but since there are no names defined, it won't display names. Names are critical to good API design because they help callers better understanding the meaning behind the API. Using point-free code in the public API can make it harder for callers to understand.

If you encounter point-free code like funcWithApplication that is publicly consumable, it is recommended to do a full η-expansion so that tooling can pick up on meaningful names for arguments.

Furthermore, debugging point-free code can be challenging, if not impossible. Debugging tools rely on values bound to names (for example, let bindings) so that you can inspect intermediate values midway through execution. When your code has no values to inspect, there is nothing to debug. In the future, debugging tools may evolve to synthesize these values based on previously executed paths, but it's not a good idea to hedge your bets on potential debugging functionality.

Consider partial application as a technique to reduce internal boilerplate

In contrast to the previous point, partial application is a wonderful tool for reducing boilerplate inside of an application or the deeper internals of an API. It can be helpful for unit testing the implementation of more complicated APIs, where boilerplate is often a pain to deal with. For example, the following code shows how you can accomplish what most mocking frameworks give you without taking an external dependency on such a framework and having to learn a related bespoke API. [
]

Don't apply this technique universally to your entire codebase, but it is a good way to reduce boilerplate for complicated internals and unit testing those internals.

F# is a wonderful language, and its design and conventions have a lot of wisdom. I think F#’s conventions about point-free style are excellent: Point-free style is wonderful but should be used judiciously.

My own belief is that the pipe operator should encourage functional programming, but it should not necessarily encourage tacit/point-free programming. The latter is a subset of the former.

I understand that some people have been strongly wishing for tacit programming to be built into the language syntax for a long time, and it feels like something has been taken away. I understand your frustration. Having said that, F# itself recommends that tacit programming be only judiciously used in internal code, for good reason (cognitive overhead, tooling, etc.). We’re trying to focus on things like Web Platform APIs (the most common JavaScript APIs in the world) over something that F# itself recommends should only be occasionally used in internal code. But I know that many people have different priorities, and I apologize that they feel that they are not being addressed.


Kudos again here to @treybrisbane for cutting to the meat of the matter. The pipe operator should encourage functional programming (we think that Hack pipes would do so), but that doesn’t mean the pipe operator should encourage tacit programming, when pointful functional programming is generally super common.

We should have addressed this in the explainer in the first place, and it was our mistake for not doing so. We should add a section addressing this to the explainer later.

samhh commented 3 years ago

It doesn't need to encourage tacit programming, merely enable it. The F# proposal achieves this by leaving open the opportunity to open up a lambda.

runarberg commented 3 years ago

@tabatkins So what you are saying is that it is the purpose of this committee to dictate which API library authors should choose based on what you believe is better for the general user of the language?

tabatkins commented 3 years ago

It is the purpose of TC39 to evolve and shepherd the JS language, which does involve making value judgements about how the language is to evolve, yes.

js-choi commented 3 years ago

It doesn't need to encourage tacit programming, merely enable it. The F# proposal achieves this by leaving open the opportunity to open up a lambda.

I agree; this is an important distinction. But it is also true that tacit programming is already enabled
in userland, with userland functions like rx.pipe.

We’re talking about whether to strongly encourage it by baking it into the language syntax itself: whether to enable it at the language-syntax level, not just at the userland level.

To give a parallel example: monoids, monads, applicative functors, etc. are already enabled in JavaScript
with userland functions, even if not with language syntax. (Although I love monoids/monads/applicatives, and in fact I am in the midst of writing a proposal for monadic comprehensions, using F# computation expressions. It’s very incomplete, though.)

lightmare commented 3 years ago

@mAAdhaTTah

Point-free programming is not a goal in and of itself.

The question was: Should enabling point-free programming/APIs be a goal of the Pipeline Operator? (I didn't even add emphasis, it's in the OP).

@tabatkins

Whoops, this ended up longer than I wanted, but I hadn't put this reasoning into words in this repo yet, so that's probably valuable.

In short: encouraging point-free style is awkward in JS because JS isn't auto-curried.

It sure is valuable, but your rationale and conclusion is specific to F# style (arg-last), while the question was generic. Elixir style (arg-first) doesn't have a problem with JS not being auto-curried.

runarberg commented 3 years ago

@tabatkins

which does involve making value judgements about how the language is to evolve

And I believe you have not done a good enough job at that, or at least not been convincing enough. I created #204 asking for data for how you’ve reached the conclusion that Hack is better suited over F#. From what I gathered in the issue threads is that your methodology of gathering the evidence needed to make a valued judgement is flawed. Therefore I have reasons to believe that the valued judgement you’ve reached in this instance is insufficient and is providing a sub-optimal stewardship.

samhh commented 3 years ago

I agree; this is an important distinction. But it is also true that tacit programming is already enabled
in userland, with userland functions like rx.pipe.

We’re talking about whether to strongly encourage it by baking it into the language syntax itself: whether to enable it at the language-syntax level, not just at the userland label.

We're already somewhat there with higher-order functions. It's very common to see xs.map(f) instead of xs.map(x => f(x)), and ironically the usual refrain for doing so is that variadic functions mightn't behave as you'd expect at first glance. It should likewise be second nature to anyone who's used pipes in a terminal.

I'm unfortunately (in terms of subjective preference) aware that lots of developers actively dislike tacit programming. In the case that this is the committee's primary reason for rejecting F# it might help to communicate that as clearly as possible; I know a lot of people myself included were excited about a functional pipeline operator making it into JS, and the knowledge that something very fundamental to FP - being really rather core to readable function composition - is disliked by committee would help to temper expectations in the future.

tabatkins commented 3 years ago

It sure is valuable, but your rationale and conclusion is specific to F# style (arg-last), while the question was generic. Elixir style (arg-first) doesn't have a problem with JS not being auto-curried.

Sure, and I rather like Elixir-style; it's definitely more compatible with JS-as-she-is-written, imo (where the most important arg is usually written first, and functions have rest args or optional args). I didn't mention it because it has nothing to do with point-free invocations. ^_^

The reason I didn't pursue Elixir-style is that it requires the same special-case syntax for await/yield that F#-style does, but afaict is even worse for non-function-calling expressions; without more syntax special-cases, I'm not sure how you'd, say, add one to the topic value, without just writing function versions of every JS operator.


And I believe you have not done a good enough job at that, or at least not been convincing enough.

You're free to believe that, and I doubt I'll convince you otherwise.

runarberg commented 3 years ago

@tabatkins (I’m sorry, I’m going off topic here, this debate belongs in #204 ).

You're free to believe that, and I doubt I'll convince you otherwise.

I doubt so too. But there are ways to make your arguments more convincing by doing more research rather then relying on speculation. And being a figure of authority I would expect you to do so. You’ve previously made claims which many people have reason to doubt. If you’d have done the research and backed your arguments up with data from various studies you would have been more convincing. Perhaps not enough to convince me, but maybe enough for me to stay silent.

Jopie64 commented 3 years ago

I think F#’s conventions about point-free style are excellent: Point-free style is wonderful but should be used judiciously.

My own belief is that the pipe operator should encourage functional programming, but it should not necessarily encourage tacit/point-free programming.

Yet the F# pipe proposal actually comes from... F#. Where it is extensively used in use code. So.... đŸ€”

js-choi commented 3 years ago

Yet the F# pipe proposal actually comes from... F#. Where it is extensively used in use code. So.... đŸ€”

Yes. F# itself gives several reasons to avoid point-free style (except judiciously in internal code). These reasons given by F# (cognitive overload and tooling) are general and would apply to other languages like JavaScript. F# does have point-free style baked into language syntax (e.g., its |>), but that is because the designers of F# (whom I find inspiring and respect very much) happened to decide that it remained a good fit for the F# language despite those reasons to avoid point-free style.

This does not mean that transplanting F# semantics (which fit with language auto-currying) is a good fit for JavaScript the language (which can never become auto-curried due to backwards compatibility). The upsides of point-free style in F# are weaker in JavaScript due to the fundamental auto-currying difference. And, at the same time, F#’s reasons to avoid point-free style (except judiciously in internal code) are still valid.

This does not mean that using point-free style is terrible and invalid. (F#’s advice is wise. Point-free style should be avoided in general, but point-free style can be wonderful when used sparingly and in internal code. Point-free style is not the same as functional programming; in fact, most functional programming arguably should be pointful, as F# itself recommends.)

Nor does it mean that point-free style is impossible in JavaScript. Point-free style is still already possible in JavaScript with userland libraries. What we are currently doing is deciding that syntactic tacit programming is out of scope of this proposal.

Tacit programming is already possible with userland libraries, and tacit programming should be generally avoided anyway (except judiciously in internal code—as recommended by F# itself, for good reason). I respect F# a lot, and I find its design inspiring and reasonable (after all, I’m planning to adapt F# computation expressions into a TC39 proposal!). F#’s conscientiousness about point-free style is merely one reason why I respect its design.


Again, though, I really do appreciate @treybrisbane’s issue cutting to the meat of the matter, and we should have addressed this directly in the explainer the first place.

lightmare commented 3 years ago

@tabatkins

I didn't mention it because it has nothing to do with point-free invocations. ^_^

Sorry for nagging you, but I feel like I'm either missing, or failing to convey something. Here's an excerpt from your reasoning that I presume lead to the conclusion that "encouraging point-free style is awkward in JS because JS isn't auto-curried".

That is, if you wrote your function intending it to be pipelined, like const add = a=>b=>a+b, then if the user wants to call it normally they have to write add(1)(2), which is foreign to JS and quite weird. If you wrote your function intending it to be called normally, like const add = (a,b)=>a+b, then if the user wants to use it in a pipeline they have to write 1 |> x=>add(x,2). Either way, this is an inconvenience to the user, ...

If you define the function as const add = (a, b) => a + b, then there's no inconvenience with Elixir pipeline, they can call it like add(1, 2) or in pipeline 1 |> add(2). Hence I think the conclusion does not follow.

without more syntax special-cases, I'm not sure how you'd, say, add one to the topic value,

Yes, that'd be either ugly or require additional/automagical syntax. Not as concise as Hack, but also not impossible.

tabatkins commented 3 years ago

If you define the function as const add = (a, b) => a + b, then there's no inconvenience with Elixir pipeline, they can call it like add(1, 2) or in pipeline 1 |> add(2). Hence I think the conclusion does not follow.

Right, Elixir-style doesn't suffer from these problems. I wasn't writing my comment as an argument against Elixir-style, I was writing it in support of "encouraging point-free style is awkward in JS because JS isn't auto-curried". Elixir-style isn't point-free, and thus the argument is irrelevant for discussions about Elixir-style.

Yes, that'd be either ugly or require additional/automagical syntax. Not as concise as Hack, but also not impossible.

Yeah, I presume that if we'd wanted to pursue Elixir-style, then "arrow functions are magically called" would be the way to go. Unsure if that'd get thru committee, so the IIFE style might have ended up being it instead, which I agree is ugh. ^_^

Jopie64 commented 3 years ago

This does not mean that transplanting F# semantics (which fit with language auto-currying) is a good fit for JavaScript the language (which can never become auto-curried due to backwards compatibility).

Nor does it have to be. Pipable libs just have to be data last and only the last argument must be curried like how it's done in RxJS. Very easy to do in JS, even easier than declaring functor prototype members like how it's currently done. But the latter is not extensible, and the former is.

The upsides of point-free style in F# are weaker in JavaScript due to the fundamental auto-currying difference. And, at the same time, F#’s reasons to avoid point-free style (except judiciously in internal code) are still valid.

I still think the discussion about point-freeness is rather technical than semantic. At operator design time you can (and usually will) still name all arguments explicitly. So not point free. The only time where you could argue that it is point free is at usage time. Because you technically create a function without the last argument, and the pipe operator then immediately calls it, just like in F#. But I argue this is only technical, but semantically you supply the last argument immediately, just before the function instead of after. The F# docs reason for avoiding point-free is about readability. And here semantics count, not technicalities. Semantics at usage time are the same in F# and JS. And at design time, it doesn't have to be designed point-free.

tabatkins commented 3 years ago

The F# docs reason for avoiding point-free is about readability. And here semantics count, not technicalities. Semantics at usage time are the same in F# and JS. And at design time, it doesn't have to be designed point-free.

Unless you're using an auto-currying library to modify your function, or are willing to write the auto-currying boilerplate yourself, this is wrong - you do need to design your function to be invoked point-free ahead of time.

I'm pretty sure there's some talking-past-each-other going on here, so let me clarify a bit - my objection here is in introducing/promoting a new calling convention, which requires library authors to actively choose which calling convention they intend their functions to use, and library users to remember which is in use.

JS currently has two calling conventions: functions, and methods. Library authors have to choose ahead of time whether they're writing a function (taking all arguments in the arglist) or a method (taking one special argument as this and the rest in the arglist), and library users have to remember which form each callable is expecting to know whether to write foo(obj) or obj.foo(). This is so drilled in as a fundamental difference that you might not have really ever considered it as something that could be different, but some languages do blur this distinction. For example, Python "methods" take all their arguments in the arglist, including the self argument; attaching a function to an object and calling it as obj.foo() just magically inserts obj as the first argument to foo(). (Try ripping a method off a Python object and calling it normally, or attaching a function directly to an object and calling it as a method; it works!) In this way, while Python functions and methods are biased toward one calling convention depending on how the function is exposed to the user, ultimately there's no difference and users can use them in either way.

Point-free programming (or rather, last-argument-via-unary-function) introduces a third calling convention to JS - you must pass the final argument via a second invocation, rather than with the rest of the arglist: add(1)(2) rather than add(1,2). Similarly to JS methods, the library author has to choose this calling convention at the time they write the function, and library users have to know which convention the function is expecting in order to call it correctly. Unlike methods, there's not even an syntactic/organizational hint about which calling convention is expected - you can tell from glancing at code that obj.foo() is a method-type function, but you can't tell from foo() whether it's a "normal" function or a point-free one, and thus you don't know whether the function has been fully invoked or is waiting for an additional argument.†

The general intention is that a point-free function is meant to be invoked in a pipeline, not directly - 2 |> add(1) rather than add(1)(2) - but that doesn't change the argument; it's still a distinct calling convention that's different from normal functions (with would have to be invoked as 1 |> x=>add(x,2)), and requires the user to know it's meant to be invoked that way.

†: This technically applies to point-free programming in any language, but in languages that automatically curry, this sort of question is expected and part of the standard semantics; invoking a function with less than its "full" set of args is often seen as invoking a different function entirely, one that returns a function with some different behavior. fmap foo and fmap foo obj are fundamentally different things to write (one "upgrades" foo into a version that works on functors, the other maps foo over a functor), and both the language and the culture around the language support that, in ways that JS does not share.

kiprasmel commented 3 years ago

my objection here is in introducing/promoting a new calling convention, which requires library authors to actively choose which calling convention they intend their functions to use, and library users to remember which is in use.

Point-free programming (or rather, last-argument-via-unary-function) introduces a third calling convention to JS - you must pass the final argument via a second invocation, rather than with the rest of the arglist: add(1)(2) rather than add(1,2). Similarly to JS methods, the library author has to choose this calling convention at the time they write the function, and library users have to know which convention the function is expecting in order to call it correctly.

but as a library author, no matter how much of an FP fanatic I "am", I would not want to write my add function like this:

const add = (a) => (b) => a + b;

because obviously the usage of add(1, 2) is way more prevalent and natural.

I would write it exactly like you would right now:

const add = (a, b) => a + b;

Now, regular users can use it like they always have - add(1, 2).

Meanwhile, when we're in a pipeline, we can just do this:

// F# + PFA
1
 |> add(?, 2)

or for another arg:

2
 |> add(1, ?)

in fact, for any amount of arguments.

Or, you can just use an arrow function:

1
 |> (x) => add(x, 2)

The point is - there is 0 motivation to start writing functions in a curried/point-free/whatever you call add = (a) => (b) => a + b manner, because users of add(1, 2) would be negatively impacted, and nobody wants that.

(unless you're making a FP-specific lib, but that already exists; it's just less commonly used, and it's opt-in, not opt-out. Thus the argument of encouraging (a) => (b) => ... does not apply in this FP-specific lib case).


Thus, I think we are not introducing a new calling convention. Right?

@tabatkins, also cc @js-choi regarding this great & related point from here - same reasoning applies.

Jopie64 commented 3 years ago

I'm pretty sure there's some talking-past-each-other going on here, so let me clarify a bit - my objection here is in introducing/promoting a new calling convention, which requires library authors to actively choose which calling convention they intend their functions to use, and library users to remember which is in use.

I agree with that. Indeed authors need to decide beforehand what convention should be used. But I argue that lib authors designing a piping (or operator) library actually know how their libs should be used. Also the users of that lib.

In your add example, this is quite ambiguous. But consider a lib with operators that can be used for generators, so they can be used like arrays can be used today.

const sum100Primes = range(0, Infinity)
  |> Lazy.filter(x => isPrime (x))
  |> Lazy.take(100)
  |> Lazy.sum()

Here it is quite obvious that the operator lib should be used this way. For the designer and for the user. The same way as that it is very clear that you should use Array.prototype.map on an array with a ..

I agree that this is some new kind of calling convention. (Although not entirely new because pipe like structures are used more and more, even with the current bloated syntax tax.) I argue (but am not really sure tbh) that the F# pipe syntax was mostly born because of the shortcomings of the class prototype calling convention. Namely that it is hard to extend for existing objects. (It's hard to design a lib from where you can import Array.flatMap and use it on an array.) And with F# this becomes possible. But instead of ., users must indeed remember to use |> instead.

So, I agree that authors need to know beforehand how they want their libs to be used. But I argue that this is not new. They only now have a third option to choose from (which is also not new btw, only currently used less often).

Jopie64 commented 3 years ago

Also, to come back to your add example: It already is unclear how a lib designer should define add. E.g. is it mostly used for stuff like this:

const four = add(3,1);

Or is it gonna be used mostly for this:

const fourAndFive = [3,4].map(add(1));

?

In such an ambiguity a lib designer can just choose to design it the normal JS way. Users of map can then just use an arrow function. But exactly the same reasoning can be used for F# pipes, cause there you can also simply use an arrow function.

runarberg commented 3 years ago

I want to reiterate my previous point here how important it is to provide library authors the freedom to provide the API they want. I’ve seen jQuery been brought up several times as an excellent library that helped further JavaScript as a language and as a community. What jQuery brought primarily was an alternative API in working with the DOM. It did so by providing an wrapper object around DOM nodes which came with a bunch of operators which were provided as methods on that object. The methods were often used point free at call time, that is we seldomly referenced the actual nodes inside the operators:

$('.my-nodes').show().addClass('foo');

However I believe a library like jQuery would never be written today. We care too much about bundle size and providing all the operators as methods is really hard to tree-shake (this point has been raised multiple times). The next best thing is exporting the operators as a bunch of functions which can be imported as needed and otherwise tree-shaken out of the final bundle:

import { $, show, addClass } from 'jQuery';

addClass('foo', show($('.my-nodes')));

This API is weird and I doubt many users will be sold on it while browsing my README. However the pipeline operator will fix it:

import { $, show, addClass } from 'jQuery';

$('.my-nodes')
  |> show(^)
  |> addClass('foo', ^);

As a library author I don’t like how my users are constantly forced to write the topic marker ^. It goes without saying that they are always operating on the special DOM wrapper that provides them the improved API. When teaching my library to new JavaScript users they might ask “Why are we always typing that ^?” And my best answer would be: “That’s just how pipelines work.” As a library author I’m not very happy.

If the pipeline operator would allow point free programming I have much more freedom to provide the API to my users that I want. And that is a good thing.

import { $, show, addClass } from 'jQuery';

$('.my-nodes')
  |> show
  |> addClass('foo');
voronoipotato commented 3 years ago

Thank you @runarberg I really appreciate this. I felt like everyone was acting like it was already decided and the whole time I was hearing about pipes, I had assumed they were consuming functions so I never thought I needed to speak up. By the time I knew to speak up it felt like everyone had already made up their minds. To me consuming functions is so much more flexible, and expressions can bleed out into global scope (especially in the hands of a novice). I can always use mutable code if I'm deeply passionate about performance, but function pipes in my opinion allow for less coupling in way that is a bit awkward with expression pipes.

baetheus commented 3 years ago

my objection here is in introducing/promoting a new calling convention, which requires library authors to actively choose which calling convention they intend their functions to use, and library users to remember which is in use.

Point-free programming (or rather, last-argument-via-unary-function) introduces a third calling convention to JS - you must pass the final argument via a second invocation, rather than with the rest of the arglist: add(1)(2) rather than add(1,2).

As I read it your objection is that you want neither to introduce nor promote point free programming via the pipeline operator, please correct me if my summation here is incorrect. It's likely the following is slightly pedantic, but it's really a side comment that I'd like to make.

I would like to point out that the point-free calling convention isn't new in javascript (as you likely know). Lodash has had a point free versions of its api for over 7 years, rxjs added the pipe operator in 2017 (its 4th birthday being the 22nd of this month), and prior to promises the best way to handle callback asynchrony IMO was the seq utility in async that has been around since 2015. There are many more examples of both straightforward and unique compose operators in javascript's history, but ultimately my counterpoint here is that point free programming is unlikely to be introduced to a programmer via the pipeline operator except perhaps by name. That said, without actual information about the usage of unary functions and point free programming this is all just bloviating.

As for promoting point free calling conventions I can understand that in spite of disagreement. My disagreement here would be entirely aesthetic.

When I started this comment I was firmly of the stance that F# pipeline is a better choice, since it's what I (and I assume most other proponents of the F# syntax) already use when we call compose(value, fn1, fn2, fn3). I've never seen a Hack style pipeline operator in the wild. That said, this conversation has made me realize two things -- First, I don't need a pipeline operator since I've already got a function that does that. Second, I no longer think it makes sense to add |> to the language at all! Weird turn of events for me. Thanks for putting so much work into this @tabatkins !

ken-okabe commented 3 years ago

The essence of "point-free programming" is function-composition.

IF we smartly have a function-composition-operator such as . in addition to FP style |>

a |> f |> g equals a |> (f . g)

I can teach FP beginners in my book based on Math/algebra. This is essentially independent of language specification. You can learn the concept from Haskell,Scala, or F# tutorial book because Math/algebra is common.

In Hack, a |> f(^) |> g(^) equals a |> (f . g)(^)

This is not math, and lose simplicity and robustness against the complexity. What is ^ ? What about context? How can I teach to beginners? Will they adopt? I don't think so.

Having said that, the Hack style has a serious problem. https://github.com/tc39/proposal-pipeline-operator/issues/208

It seems like the presence of a |> in a child function would give new meaning to the ^ on its RHS until the end of the function's context?

This is correct: the |> operator introduces a new expression scope, so the (^) on line 6 refers to what you expect it to.

https://github.com/microsoft/TypeScript/pull/43617#issuecomment-816850211

Personal opinion: The hack-style pipeline operator is bad because it introduces a new context variable, like this, in the form of #. These don't nest in intuitive ways. In addition, the operator doesn't actually save much space for many usages. You could rewrite....

The functional pipeline operator, on the other hand, exists to deliver a point-free style that otherwise is impossible in JS - while some people may hate this style, that is what the true functional pipeline operator truly enables. (Plus, in the common functional style, each pipeline phase saves at least 4 characters, which is a much greater savings than the hack style). Moreover, the functionality of the "hack style" is replicatable (without implicit variables) using the functional style by embedding arrow functions into the pipeline, at a cost of 3-5 (depending on whitespace preferences) characters per pipeline phase which required adapted arguments. (Which shouldn't be too often, as if you find yourself adapting the same function's argument's repeatedly, in the case of either operator form, you should probably wrap it with an adapted form for clarity. For example, if Math.pow(#, 2) appeared more than once or twice, you should probably extract it into a single-argument square function.)

this is a product of OOP, and the artificial design failed to sustain robustness against software complexity. We the programmers-human can't control the full of context variables, and now again, Hack-style-pipeline operator provide us new nesting context.

"point-free programming" is essentially eliminates context variables that is the major reason of BUG.

If a programmer smartly understands the fact context variables are to be avoided (because we don't want BUG), the orientation of point free is the way to go, in other words, function composition world, FP.

Looks like Hack-style pipeline operator does not respect such a productive orientation.

@runarberg commented yesterday

@tabatkins So what you are saying is that it is the purpose of this committee to dictate which API library authors should choose based on what you believe is better for the general user of the language?

It already happened: https://github.com/tc39/proposal-pipeline-operator/issues/208#issuecomment-918613987

Yeah, the general solution is "don't chain that much"; you're over-nesting in an operator meant to reduce nesting. ^_^

voronoipotato commented 3 years ago

Also I keep hearing that you can use await in the pipes with the hack pipes, I was looking at the syntax in hack for it and I was curious why you explicitly can't do that in Hack.

https://docs.hhvm.com/hack/expressions-and-operators/pipe

I'm beginning to think that these expression pipes are not as simple as they are being sold, and that in practice they won't be able to safely do what they are advertised.

ken-okabe commented 3 years ago

Also I keep hearing that you can use await in the pipes with the hack pipes, I was looking at the syntax in hack for it and I was curious why you explicitly can't do that in Hack.

https://docs.hhvm.com/hack/expressions-and-operators/pipe

I'm beginning to think that these expression pipes are not as simple as they are being sold, and that in practice they won't be able to safely do what they are advertised.

Please remember await is a statement which is nothing to do with Math. https://en.wikipedia.org/wiki/Statement_(computer_science)

On the other hand, operator is an expression, a term of Math. https://en.wikipedia.org/wiki/Expression_(computer_science)

What's very wrong I feel here is that people confuse or mix those concepts, and as a result, looks like they don't care too much about that JavaScript community is violating basic math rules. This is the most fundamental fact.

In mathematics, function f(x) does not have context.

f(x) equals x |> f

this is the bottom line, Algebra. nothing more, no extra specification needed. x |> f does not have context either, of course.

in Hack,

f(x) equals x |> f(^)

this ^ so called "place-holder" is not math concept. We never see such a weird thing in other FP languages such as Haskell, Scala, F# etc.

The language developers respect Mathematics that is why their language application is robust, and rock-solid (Twitter now uses Scala and Algebra library for their own system). https://twitter.github.io/algebird/ https://github.com/twitter/algebird

Why only JavaScript community doesn't care? because of confusion between "statements" and "expressions"? Now, what we try to do here is inventing "Operator" that is actually not math expressions at all.

f(x) has no context. Math fact. Hack style insists f(x) equals x |> f(^)

and as I mentioned earlier the "operator" introduces context, which clearly violates the basic mathematics/algebra rule.

Generally, this is an unacceptable fact.

mAAdhaTTah commented 3 years ago

But I argue that lib authors designing a piping (or operator) library actually know how their libs should be used. Also the users of that lib. - @Jopie64

Introducing a third calling convention, asking library authors to design their APIs for that calling convention, asking users to specifically choose this calling convention, will further cleave functional JavaScript from the rest of the community & ecosystem. It will make it more difficult to use functional tools or work with functional techniques without a thorough understanding of several related concepts and adopting specific libraries. It will make it harder to adopt functional techniques in non-functional codebases because of all of the associated baggage. This is why libraries have to provide separate /fp variants: because mainstream API design doesn't work with your current tooling. A proposal specifically designed to enable & encourage point-free programming exacerbates the division.


It goes without saying that they are always operating on the special DOM wrapper that provides them the improved API. - @runarberg

From @js-choi's F# wisdom:

In general, point-free programming is not a virtue in and of itself, and can add a significant cognitive barrier for people who are not immersed in the style.

Going without saying is a significant cognitive barrier and you very much should go with saying. The visual overhead of a single point is small by comparison.


The essence of "point-free programming" is function-composition. - @stken2050

However, the reverse is not true: the essence of function composition is not point-free programming. And most of the benefits of functional programming are the result of function composition, not specifically its point-free variant. This is why I pointed out "enabling point-free programming is not a goal"; the goal is to unnest & linearize code. Function composition, not point-free programming, enables this.

Reject point-free; let's mainstream function composition

ken-okabe commented 3 years ago

the goal is to unnest & linearize code. Function composition, not point-free programming, enables this.

@mAAdhaTTah

Then what do you think Hack-style pipeline-operator violates the algebra rule? This one introduces context variables

runarberg commented 3 years ago

It goes without saying that they are always operating on the special DOM wrapper that provides them the improved API. - @runarberg

From @js-choi's F# wisdom:

In general, point-free programming is not a virtue in and of itself, and can add a significant cognitive barrier for people who are not immersed in the style.

Going without saying is a significant cognitive barrier and you very much should go with saying. The visual overhead of a single point is small by comparison. — @mAAdhaTTah

If the original authors of jQuery had taken this wisdom at heart and tried to avoid point free programing with their library it might have lookged something like this:

const wrapper = $('.my-nodes');
show(wrapper);
addClass('foo', wrapper);

However that is not what they choose. When they designed their library they saw value in allowing their users to operate point free on their wrapper. And this was to a tremendous success, and their users loved it. Point free might not be a virtue in and of it self, but diversity in API design might. Certainly giving authors the freedom to choose which API design suites their library is. Would we have gotten jQuery if point free was somehow forbidden with the dot operator? I doubt it.

mAAdhaTTah commented 3 years ago

@runarberg If jQuery were written with a pipe operator, they very well might have. But the goal was to linearize code with method chaining, not to write point-free JavaScript.

runarberg commented 3 years ago

In fact, lets take this further and start listing a few popular libraries which encourage point free style:

Library Example
jQuery $(".selector").show().addClass("foo")
D3 d3.selectAll("p").style("color", "blue")
RxJS fromEvent("click", document).pipe(throttleTime(1000), scan(n => n + 1, 0)).subscribe()
lazy.js Lazy(people).pluck('lastName').filter((name) => name.startsWith('Smith')).take(5)
moment.js moment().min(start).max(end)
Chai expect(tea).to.have.property('flavors').with.lengthOf(3);
Temporal Temporal.Now.zonedDateTime(browserCalendar).withTimeZone(tankTimeZone).startOfDay().toInstant()
runarberg commented 3 years ago

Method chaining used to be a popular way of providing an API that was point free until we started worrying about bundle sizes, and it remains a popular option where bundle sizes don’t matter (e.g. in assertion libraries like Chai). However in production code that is delivered on the world wide web I want the same freedom as a library author to provide similar APIs without my users having to have to worry about their bundle sizes suffering.

samhh commented 3 years ago

It should be noted that even fp-ts started out with a prototypal, "fluent" method-chaining API in 1.x. The primary limitation it exposed to me as an end user is that inserting your own functions into the pipeline is never very ergonomic. At best it's something like this:

x.map(f).pipe(g).pipe(h) // hope you didn't need to cross type-boundaries!

Whereas now that same code would be expressed as follows:

pipe(x, map(f), g, h) // pipeline application
flow(map(f), g, h) // function composition
mAAdhaTTah commented 3 years ago

You're both missing the point: none of those fluent method libraries used point-free method chaining because they wanted to write point-free code. They wrote them that way because they wanted write linear, unnested code. A point-free approach is a tool for writing code in that way. With a pipe operator, it becomes feasible to unnest & linearize a sequence of function calls, not just methods, which is possible regardless of the pointed-ness of the functions at play here.

voronoipotato commented 2 years ago

We didn't ask for that though. The original thing people were excited about were function pipes. I don't want expression pipes. If you think function pipes are bad for the language, don't add function pipes, but don't use function pipes as a way to slide through expression pipes when what we asked for is function pipes. I don't trust that they will be as safe or as easy as you say they will. I trust and use function pipes, I don't know what the implications for expression pipes even are. I think there is a very good chance that I will have to tell newcomers to avoid expression pipes because there are dangerous and confusing edgecases.

samhh commented 2 years ago

Functions are already a powerful unit of expression.

In any F#-style pipeline I can take any lambda, name it, and then reference back to it without modifying the value itself. I can likewise do the inverse. The function can contain whatever arbitrary expressions I'd like without the cost of additional, specialised syntax. This is really helpful for refactoring. I don't think Hack can compete here; functions are the unit of code reuse at this granular level because they already capture the essence of data input and output in a more generalised way.

With regards point-free, referencing functions as values without wrapping them in new lambdas is something that in my experience is already widespread outside of functional circles, and as I've said previously ironically the counter-idiom not to do this only exists because the functions might not be unary.

ken-okabe commented 2 years ago

I think there is a very good chance that I will have to tell newcomers to avoid expression pipes because there are dangerous and confusing edgecases.

As long as Hack-style pipeline-operator, you are right. Since we already know Hack-style pipeline-operator introduces new context variables like OOP this which is dangerous and confusing, no question about that.

https://github.com/tc39/proposal-pipeline-operator/issues/206#issuecomment-919623740 https://github.com/tc39/proposal-pipeline-operator/issues/208 https://github.com/microsoft/TypeScript/pull/43617#issuecomment-816850211

Personal opinion: The hack-style pipeline operator is bad because it introduces a new context variable, like this, in the form of #. These don't nest in intuitive ways. In addition, the operator doesn't actually save much space for many usages. You could rewrite....

On the other hand, F# style pipleline-operator is proven to be robust because it's simply a binary operator of Binary operation in Algebra.

I think the most of the Hack advocators here don't understand that pipeline-operator is for binary operation because in this 48 hours reading through here I have read "pipeline-operator is syntax sugar.... easy to read the nest" etc.

1 + 2 is an binary operation and + is a binary operator. 2 x 3 is an binary operation and x is a binary operator. a |> f is an binary operation and |> is a binary operator. Ok? F# style (pure math style) Safe. Hack-style (I don't know what this is) Dangerous.

https://github.com/tc39/proposal-pipeline-operator/issues/208#issuecomment-920057657


In terms of point-free style, this is also basically Math. and a way to write in FP. As long as we stick to Math/Algebra or F# operator style, point-free style is automatically possible because it's the same league of Math/Algebra, so not to be a goal but as a matter of course. However, Hack-Style, since this is something other than math, any intricate concept of Math cannot be achieved.

runarberg commented 2 years ago

I have a question to the TC39 members:

  1. If you really don’t want people to write point free with the pipeline operators,
  2. and if you could be convinced that library authors still deserve the freedom to provide a point-free API,
  3. but you still believe that authors should not be exporting functions that return a unary function,
  4. would you then be open to new proposals which grants us that freedom again without the downsides of the current way of extending the prototype.

I will pull something out of a hat now to demonstrate what I mean:

Proposal: Scoped Methods

Similar to Operator overloading, building on prior art from rust, and reviving the dreaded with statement we could temporarily extend the prototype of any object by doing something like:

import { foo, bar, baz } from "./string-methods.js";
with { foo, bar } on String.prototype;

// Now this will work but only inside this module
"my string".foo();
"my string".bar();

{
  // This will only be applied inside this scope.
  with { baz } on String.prototype;

  "my string".baz();
}

"my string".baz();
// => Type error: String.prototype.baz is not a function.

If I wanted to write an operator library for iterators I could write my operators as functions that operate on this like this:

export function* map(fn) {
  for (const item of this) {
    yield fn(item);
  }
}

export function* filter(p) {
  for (const item of this) {
    if (p(item)) {
      yield fn(item);
    }
  }
}

And instruct my users to use it like this:

import { Iterator, range, map, filter } from "my-iter-lib";
with { map, filter } on Iterator.prototype;

range(0, 10)
  .filter(isPrime)
  .map((n) => n * 2)

Now I’m not saying this is a good idea, scoped prototype extension is only something I pulled out of a hat in order to demonstrate what I mean. So to summarize my question: Would the committee be open to something like this, or are you likely to block any proposals which would allow us to write libraries which encourages point free operations?