Skip to content
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

Consider syntactic marker to indicate a partial application #13

Closed
rbuckton opened this issue Sep 28, 2017 · 34 comments
Closed

Consider syntactic marker to indicate a partial application #13

rbuckton opened this issue Sep 28, 2017 · 34 comments
Labels

Comments

@rbuckton
Copy link
Collaborator

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 rbuckton changed the title Consider syntactic marker to indicate a partial application. Consider syntactic marker to indicate a partial application Sep 28, 2017
@rbuckton
Copy link
Collaborator Author

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
Copy link
Collaborator Author

@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:

  • ^f(g(?)) would be a syntax error
  • ^f(?, { opt: ? }) would be a syntax error
  • ^f(x, ?) would fix the values of f and x

If we choose deferred evaluation, then:

  • ^f(g(?)) would be a => f(g(a))
  • ^f(?, { opt: ? }) would be (a, b) => f(a, { opt: b })
  • ^f(x, ?) would be a => f(x, a)

@gilbert
Copy link

gilbert commented Oct 2, 2017

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

var g = ^f()

@rbuckton
Copy link
Collaborator Author

rbuckton commented Oct 2, 2017

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

@obedm503
Copy link

obedm503 commented Nov 6, 2017

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
Copy link

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
Copy link
Collaborator Author

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
Copy link
Collaborator Author

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
Copy link

Now that you mention it It is confusing

@y-sitbon
Copy link

y-sitbon commented Nov 29, 2017

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 ?

  • x.^add(10, ?)
  • ^x.add(10, ?)
  • ^(x.add)(10, ?)

@rosyatrandom
Copy link

rosyatrandom commented Dec 4, 2017

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
Copy link

obedm503 commented Dec 4, 2017

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
Copy link

@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
Copy link

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
Copy link

@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
Copy link

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
Copy link

fmease commented Dec 8, 2017

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
Copy link

dead-claudia commented Dec 27, 2017

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
Copy link

jEnbuska commented Dec 29, 2017

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
Copy link

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

@trustedtomato
Copy link

@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
Copy link

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

@sarimarton
Copy link

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
Copy link

@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
Copy link

Looks like there are ASI hazard in this code:

const x = a 
^ f()
|> g

@dead-claudia
Copy link

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

@mayorovp
Copy link

mayorovp commented Jan 26, 2018

@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
Copy link

@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
Copy link

btoo commented Jan 27, 2018

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
Copy link

dead-claudia commented Jan 27, 2018

@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
Copy link

btoo commented Jan 27, 2018

@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
Copy link

@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
Copy link
Member

hax commented Mar 2, 2018

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
Copy link
Collaborator Author

rbuckton commented Apr 2, 2019

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 we wanted to allow ? outside of ArgumentList.
  • If we chose to use "Hack"-style pipelines for the pipeline proposal and wanted to use ? as the placeholder for both proposals (with consistent semantics for both).
    • If this were the case, any |>-like prefix sigil is likely out of consideration due to ambiguity.

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests