tc39 / proposal-partial-application

Proposal to add partial application to ECMAScript
https://tc39.es/proposal-partial-application/
BSD 3-Clause "New" or "Revised" License
1.02k stars 25 forks source link

Consider syntactic marker to indicate a partial application #13

Closed rbuckton closed 5 years ago

rbuckton commented 6 years ago

It was brought up by @waldemarhorwat at the September meeting that it can be confusing if you change this:

foo(x++, y(g, h, i, j), m * n, "hello", bar, ...a)

To this:

foo(x++, y(g, h, i, j), m * n, "hello", bar, ...)

As it requires the developer to follow the entire argument list to see the ... to understand this isn't a function call but is rather a partial application.

He suggested we consider some type of syntactic marker either before the call or before the argument list (i.e. □foo(?) or foo□(?), where is some as-yet-specified token).

He also indicated that there is no mechanism to partially apply a call with no arguments, which such a token would address.

rbuckton commented 6 years ago

One option is to have a prefix ^ to indicate a partial application:

// on its own:
const addOne = ^add(?, 1);

// pipeline:
const countOfBooksByAuthor = library 
    |> ^descendantsAndSelf(?)
    |> ^filter(?, node => node.category === "programming")
    |> ^groupBy(?, node => node.author);

// expressions:
const g = ^f(?, { option: ? });

Prefix ^ is unambiguous outside of ASI. ASI introduces a slight hazard:

const x = a 
^ f()

This would always be interpreted as const x = a ^ f(), as a stand-alone partial function is likely not what the user intended.

rbuckton commented 6 years ago

@waldemarhorwat also pointed out that if we allow ? in any expression position when combined with ^, then we must defer evaluation of the argument list. We can only support eager evaluation if we only allow ? in the immediate argument list of the partial function. We probably don't want to partially evaluate an argument, as it could have highly confusing semantics.

If we choose eager evaluation, then:

If we choose deferred evaluation, then:

gilbert commented 6 years ago

Another question to consider: Should ^ be allowed with no ? placeholder? e.g.

var g = ^f()
rbuckton commented 6 years ago

That was one of Waldemars reasons for wanting a syntactic marker in the first place.

obedm503 commented 6 years ago

changing ... to ...? might help

foo(x++, y(g, h, i, j), m * n, "hello", bar, ...)

becomes

foo(x++, y(g, h, i, j), m * n, "hello", bar, ...?)

and so ? becomes what identifies the statement as partial application and not a function call.

bonus: adding the ? will make it look more like the normal spread operator

danculley commented 6 years ago

The decision of whether to go this direction seems to hinge on whether ^f() is useful or is an edge case. Unless I'm missing something, the only purpose of doing const g = ^f() is to force ignoring any arguments passed to g, that is, g(1, 2, 3) would actually call f().

While a valid use of partial application and similar semantics to arrow functions, it also seems likely to cause confusion. It would be useful to know whether enforcing ignoring of arguments by the partially-applied function is a core use case. If not, @obedm503 's proposal of ...? seems to make more sense.

One other consideration is that there are only so many available syntatic markers that can be used in this position, so we should carefully consider whether this is worth using one for, forever.

rbuckton commented 6 years ago

The initial reason that a syntactic marker was requested was the "garden path" problem, that you wind your way along the "garden path" of a function call and only at the end see a trailing ? (or ...) argument. ArrowFunction already exhibits the same issue, in that you can right a fairly complex ArrowFunction head that could be easily be mistaken for a parenthesized expression until you reach the trailing =>.

I don't see much value in the syntactic marker. While the "garden path" concern is valid for very complex cases, the more frequent cases will be using partial application with pipeline (|>) or as a replacement for Function.prototype.bind. For the corner-case of ^f(), this can still be solved with an arrow function (as () => f()).

rbuckton commented 6 years ago

changing ... to ...? might help

...? doesn't solve the "garden path" problem, and I think could be confusing. To me ...? looks like "spread the placeholder" not "spread the remaining arguments".

obedm503 commented 6 years ago

Now that you mention it It is confusing

y-sitbon commented 6 years ago

If we use this expression const add10 = ^add(10, ?); then how should it work with nested function properties ? Which one of these expressions is valid ?

rosyatrandom commented 6 years ago

How about a shorthand form of the arrow functions that can be used for partial application? This would make the intent -- a function that passes arguments to another function -- closer to the form. Also, since this is very similar to the functionality of the piping operator |> (which passes a single argument to a function), perhaps we should make that connection in the syntax.

Have (?, 1, ...) =|> fn be equivalent to (_x, 1, ..._y) => fn(_x, 1, ..._y).

obedm503 commented 6 years ago

problem is, (...args) =>> fn looks more like defining a function rather than invoking one. that's why fn(10, ?) works well, because it's similar to calling a function. also, (...args) =>> fn doesn't really add much because just as you can write (10, ?) =>> fn you can use an inline arrow function n => fn(10, n) (which might even be clearer)

rosyatrandom commented 6 years ago

@obedm503 sorry, my post has been edited since the version you saw.

[my proposal] looks more like defining a function rather than invoking one

Well, that's because it is defining a function

that's why fn(10, ?) works well, because it's similar to calling a function

That's why the TC39 panel had an issue with it: it looks like a call instead of the definition it actually is

tabatkins commented 6 years ago

He also indicated that there is no mechanism to partially apply a call with no arguments, which such a token would address.

That's just the function value itself, no? Is there a reason to need some special partial-application behavior for this case?

danculley commented 6 years ago

@tabatkins It's confusing, but no. The partially-applied function with no arguments would ignore any further values passed to it. Take as an example const g = () => f();. Calling g(1, 2, 3) may not return the same result as calling f(1, 2, 3), because g(1, 2, 3) is equivalent to f().

One can of course question whether the token approach is worth having solely for this reason, unless there were other strong reasons to go the token route. Hard to imagine a lot of use for this case.

tabatkins commented 6 years ago

Ah, makes sense. It affirmatively locks down the calling signature. I agree that this, by itself, isn't a strong motivating case for an identifying glyph, but I wouldn't say no to it if the other reasons for a glyph won out.

fmease commented 6 years ago

What about using the backslash \ as a marker? As far as I am aware, this token is currently absolutely illegal outside of string literals. As such, one can place it between the callee and the arguments list:

f\(1, 2, 3); // () => f(1, 2, 3);
f\(1, ?, 3); // (x) => f(1, x, 3);
f\(1, ...); // (...xs) => f(1, ...xs);
(a.f)\(?); // (x) => (a.f)(x):

Why does especially the backslash make sense here? Well similar to escaping characters in string literals,
we "escape"/avoid the execution and only bind arguments to a function.

The major advantage over the circumflex ^ is the infix form which is more logical and less confusing than the prefix form. The programmer directly sees the backslash and recognizes partial application.

dead-claudia commented 6 years ago

Could we use a different sigil than the circumflex? Maybe :: (from the bind proposal, just repurposed here)?

I'll note that the circumflex might provide a syntax ambiguity with bitwise exclusive or when used as an expression statement with parentheses. (It also just looks odd.)

jEnbuska commented 6 years ago

I've been doing React for a while and having a syntactic marker to indicate a partial application would often make a lot of sense in that domain.

Current way of doing things:

const {innerAnimal, color, saying} = this.state;
return Object.entries({innerAnimal, color, saying})
    .map(([name, value]) => (
      <input 
          value={value}
          onChange={e => this.setState({[name]: e.target.value})}
      />      
     ))

Hopefully the futureway of doing things ~

const {innerAnimal, color, saying} = this.state;
return Object.entries({innerAnimal, color, saying})
    .map(([name, value]) => (
      <input 
          value={value}
          onChange={this::setState::({[name]: ?.target.value})}
      />      
     ))
dead-claudia commented 6 years ago

@jEnbuska Infix is not a great idea IMHO. It still looks too much like a function call.

trustedtomato commented 6 years ago

@fmease It's not illegal. See this article. In your case, it is illegal, but you still need lookahead to determine whether its a partial application or not.

trustedtomato commented 6 years ago

If you do accept using a syntactic marker, this proposal will be essentially a subset of the partial expression proposal.

sarimarton commented 6 years ago

Not just arrow functions exhibit garden path, as @rbuckton mentioned, but method shorthands as well:

foo(bar, baz, ...a) {

They pretty much seem like an invocation until you get to the braces or recognize the object context. While I admit that it might be less of a source of confusion, still I think it further lessens the weight of the "garden path" concerns in general.

And if we say that syntax highlighting in editors can eliminate this kind of problems, that reasoning of course would apply to the partial application case as well.

dead-claudia commented 6 years ago

@sarimarton Not from a formal syntactic perspective, and the object context is much harder to miss (as it's explicit from the start). I don't see much different than looking at a large callback body and accidentally mistaking it as a block.

mayorovp commented 6 years ago

Looks like there are ASI hazard in this code:

const x = a 
^ f()
|> g
dead-claudia commented 6 years ago

@mayorovp Already known. 👍 (But thanks for the heads up, anyways! 😉)

mayorovp commented 6 years ago

@isiahmeadows you message contains no code. I constructed the counterexample to that argument:

This would always be interpreted as const x = a ^ f(), as a stand-alone partial function is likely not what the user intended.

dead-claudia commented 6 years ago

@mayorovp Okay...I probably should've linked to @rbuckton's comment instead. But your example would still not be ambiguous, and the ASI hazard is the same as in his. It would be parsed as const x = (a ^ f()) |> g, which IMHO still could be unexpected to some. (In general, using binary operators as prefix beyond member access is exceedingly rare.)

btoo commented 6 years ago

Wouldn't it be much more in the spirit of JavaScript to actually use a keyword operator? Here, I'm using partial as the keyword (which actually ends up being more typing for a single-argument partial application if you compare it to the single-argument arrow function shorthand: x => ... but only in these cases). There might be other better words that smarter people could think of

const partiallyAppliedFn = partial foo(x++, y(g, h, i, j), m * n, "hello", bar, ...)

const countOfBooksByAuthor = library 
    |> partial descendantsAndSelf(?)
    |> partial filter(?, node => node.category === "programming")
    |> partial groupBy(?, node => node.author);

Yea we'd be doing more typing but it's just so much more declarative. After all, this is the same reason we have async/await rather than &, @, $, ^, etc. Also, using a word operator, which would presumably be safely unique to it's specific function, safeguards against any ASI hazards, or even extensions to the language in the future.

dead-claudia commented 6 years ago

@btoo I'm not sure a keyword provides any benefit that a sigil doesn't. More to the point, any solution that provides no material benefit over arrow functions is pointless. (Yours would really be more verbose than arrow functions, so very few would actually use it.)

btoo commented 6 years ago

@isiahmeadows like I said, it is only ever more verbose when compared with the single-argument shorthand for arrow functions, so this may seem less convenient for something like the pipeline operator, but really this is a price I'm more than willing to pay if it means more expressive code.

By the way, Clojure seems to be ok with it - but if you really do care about typing a couple more letters, this could be remedied with a word like fix instead

dead-claudia commented 6 years ago

@btoo That's slightly different, and functions more like JavaScript's Function.prototype.bind. Clojure, however, does also have support for what's being proposed here, namely a shorthand for anonymous functions that doesn't require an explicit parameter.

hax commented 6 years ago

If sigil is required, I suggest reuse |> and allow the abbr form x ||> f(1, ?) or x |>> f(1, ?) which easy to read/write.

rbuckton commented 5 years ago

I am closing this issue as I do not believe a prefix sigil is strictly necessary given the current eager-evaluation and ArgumentList-only semantics. The only reason I believe a sigil might be warranted would be in the following situations:

If either of these situations becomes apparent, I will consider reopening this issue for further discussion.