tc39 / proposal-bind-operator

This-Binding Syntax for ECMAScript
1.74k stars 30 forks source link

Partially applied parameters #15

Closed maxnordlund closed 8 years ago

maxnordlund commented 9 years ago

The proposed syntax doesn't cover the other use of bind, namely partially application of function parameters. I've been thinking how this could be solved, and come up with this:

Either the current call syntax will be repurposed as extra arguments for bind:

foo::bar(a, b, ...rest) // bar.bind(foo, a, b, ...rest)

However this makes the suggested common use awkward,

(document.querySelectorAll("p")
  ::map)((el) => el.id) // Wrap whole expression in parenthesis, then call
  ::filter((id) => /^[A-Z]/.test(id))() // Bind all parameters, then call

The other way I found is to use parenthesis on the left hand side to indicate more parameters, which menas that the common case of only supplying a this value would be seen as having implicit parenthesis.

(foo, a, b, ...rest)::bar // bar.bind(foo, a, b, ...rest)

This case would be more backwards compatible then the above, but might be more confusing to read. It does however clearly distinguish between bound and call parameters.

I came upon this when writing a curry decorator and ended up using Function.prototype.bind.bind(bar, foo)(...arguments) which babel compiles to Function.prototype.bind.bind(bar, foo).apply(undefined, arguments).

What do you think? Anything I missed?

Edit Change currying -> partial application per https://github.com/zenparsing/es-function-bind/issues/15#issuecomment-110892162

shovon commented 9 years ago

May I suggest?

foo::bar:::(a, b, ... rest) // Three colons! Not just two!

The (foo, a, b, ...rest)::bar suggestion can accidentally be conflated with an ugly feature (bug?) with ECMAScript. Often times, the last expression in a comma-delimited set of expressions is considered the final computed value. So, for example, the following expression:

10, 20, 'foo', 'bar', 10 === 20, 3

Would yield the value 3, because 3 happens to be the last expression, in the above expression, per section 12.15.3 of ECMA-262. The only exception seems to be when when supplying a set of parameters. But regardless, if we were to have the following expression:

(10, 20, 'foo', 'bar', 10 === 20, 3, foo)(10)

Not only would 10, 20, 'foo', 'bar', 10 === 20, 3, and foo be evaluated, but also foo(10). And hence why the (foo, a, b, ...rest)::bar suggestion can be conflated for something else, such as (foo, a, b, bar)::baz, where JavaScript will evaluate foo, a, b, and bar, and then evaluate bar::baz.

Of course, there's nothing stopping us from actually suggesting that (foo, a, b, ...rest)::bar be parsed and compiled as bar.bind(foo, a, b, ...rest), but bear in mind that there are programmers out there that use the comma-delimited expression as a language feature, and use it heavily.


From the looks of the standard, though, the colon (:) is only used for separating the key from the value in an object expression. Outside of an object, however, the colon isn't used for anything (or at least, not that I'm aware of), hence why, I guess, the double colon is used as the shorthand for bind. A third colon, perhaps, is available to our imagination. And I'm suggesting that we use the triple colon for partial application of functions.

Edit: some clarification

shovon commented 9 years ago

Bump for email notification

I made some changes to my earlier comment.

shovon commented 9 years ago

Maybe I should also clarify what my suggested foo::bar:::(a, b, ... rest) syntax is.

foo::bar:::(<parameters list>) should compile to bar.bind(foo, <parameters list>).

nathggns commented 9 years ago

I like your idea, I just don’t think that theres a big enough difference between :: and ::: - it’s going to hinder readability. Similar to how Swift used to use .. and … for its range operators, before turning it into >.. and .. I think.

On 26 May 2015, at 15:31, Salehen Shovon Rahman notifications@github.com wrote:

Maybe I should also clarify what my suggested foo::bar:::(a, b, ... rest) syntax is.

foo::bar:::() should compile to bar.bind(foo, ).

— Reply to this email directly or view it on GitHub https://github.com/zenparsing/es-function-bind/issues/15#issuecomment-105544924.

shovon commented 9 years ago

I like your idea, I just don’t think that theres a big enough difference between :: and ::: - it’s going to hinder readability.

True that.

shovon commented 9 years ago

How about the following?

foo::bar::>([parameters list])
shovon commented 9 years ago

Or better yet:

foo::bar:>([parameters list])
domenic commented 9 years ago

IMO this is way out of scope and no syntax is necessary.

nathggns commented 9 years ago

I'm not sure how this is out of scope. This is about adding a syntax literal for bind, and partials are part of that.

Sent from my iPhone

On 26 May 2015, at 15:45, Domenic Denicola notifications@github.com wrote:

IMO this is way out of scope and no syntax is necessary.

— Reply to this email directly or view it on GitHub.

zenparsing commented 9 years ago

Hi @maxnordlund , @nathggns . Thanks for the suggestion. From my point of view, the purpose of this proposal is to introduce very surgical syntactic sugar for two patterns that are currently more cumbersome than they need to be, or should be.

We don't need to completely cover Function.prototype.bind with new syntax. And I don't think we should attempt to. In my opinion, Function.prototype.bind works well enough for the more complex case of curried arguments and won't benefit greatly from sugar.

I'll post some more on this a bit later...

nathggns commented 9 years ago

I think what bugs me is you have to pass the thisArg when using bind, even if you don't want to change it. So far something like obj.adder.bind(obj, 1, 2) or something, it gets fairly tedious to have to do that often (which you might if you use fp namespaced under an object that refers to this). That's why I would support a syntactical version of it. I also feel it's something people would expect if you can do ::console.log.

Quick question, under the current proposal, what would ::console.log('Hello, World') do? That would be what I would expect to do partial application.

Naddiseo commented 9 years ago

@nathggns, that usage of bind also annoys me. My current use case is JSX templates where I'm binding callbacks to the controller, it can get pretty ugly if the object I want to bind to is pretty deep:

let vdom = <a onclick={ thing.otherthing.ctrl.onClick.bind(thing.otherthing.ctrl, someVariable) }></a>;

However, I do understand why this usage is out of scope for the "::" operator; it's not really a lazy operator like partial application is. Off the top of my head, I think having another operator, say "->" (read as "binds to") could be used: thing->method(arg1) read as "thing binds to method with arg1"

nathggns commented 9 years ago

I just see that the usage for -> and :: would be so so similar. Especially if was is a prefix and one is a suffix. It would be annoying to have to replace :: with -> every time I also want to bind an argument as well as the thisArg. I just see no reason to not support ::someObject.someMethod(a, b, c) as a replacement for someObject.someMethod.bind(someObject, a, b, c), unless that would already do something else under the current proposal.

domenic commented 9 years ago

That indeed would already do something else under the current proposal. It would call ::someObject.someMethod with arguments a, b, and c. See the readme for more details.

nathggns commented 9 years ago

Isn't that a bit... pointless? someObject.someMethod(a, b, c) does the exact same thing as someObject.someMethod.bind(someObject)(a, b, c)...

Naddiseo commented 9 years ago

@nathggns, yes, that usage is the same, but when you currently use bind you don't want the function to execute immediately.

nathggns commented 9 years ago

So I'm confused. What's the purpose of ::someObject.someMethod(a, b, c) being transformed to someObject.someMethod.bind(someObject)(a, b, c) under the current proposal instead of someObject.someMethod.bind(someObject, a, b, c).

Naddiseo commented 9 years ago

For generic methods I think. If someMethod isn't actually a method of someObject, you can still bind the this argument.

Naddiseo commented 9 years ago
::console.log(1);
console::log(1);
Array::ThisMethodIsntOnArray(1,2,3)

Transpiles->


console.log.call(console, 1);
log.call(console, 1);

ThisMethodIsntOnArray.call(Array, 1, 2, 3);
Naddiseo commented 9 years ago

@nathggns, let me present to you _=> the "unary boundcurry operator". Usage: instead of a.b.bind(a, c) use _=>a.b(c)

According to this jsperf it's faster than using bind for both construction and calling.

nathggns commented 9 years ago

Exactly, the ::console.log(1) example is a pointless conversion, it does exactly the same thing as console.log(1). Why not make ::console.log(1) mean console.log.bind(console, 1)?

nathggns commented 9 years ago

Also _=> doesn't pass on extra arguments like bind would

Naddiseo commented 9 years ago

@nathggns, I think issue #18 covers that particular ambiguity to the unary operator; and I tend to agree with that use case. ::obj.method() vs ::obj.function() does seem rather magical and confusing.

Re: _=> yes, it was meant to be a bit tongue-in-cheek. It works for simple cases though, and is shorter.

nathggns commented 9 years ago

There just doesn't seem to be a substantial reason for not supporting ::console.log(1) transforming to console.log.bind(console, 1)

domenic commented 9 years ago

It's extremely confusing for ::console.log to return a function, but calling that function with the call syntax ::console.log(1) does not call it but instead somehow gives a new function. Other confusing things under your proposal:

::console.log(1) // nothing logged??

const bound = ::console.log;
bound(1); // something logged!??!

(::console.log)(1) // something logged!?!

foo::console.log(1) // console.log.call(foo, 1)
console::console.log(1) // console.log.call(console, 1)
::console.log(1) // console.log.bind(console, 1) ???

In other words, using call syntax () for anything besides calling is ... not good.

maxnordlund commented 9 years ago

I'll try to address some of your concerns. First of all, foo::bar(a, b)::baz(...c) currently becomes baz.apply(bar.call(a, b), c), and should probably stay as I talked about above. This would allow things like document.querySelectorAll("p")::Array.prototype.map((p) => p.textContent), and this is something worth making easy I think.

But I also find having an easy way of using curried arguments/parameters is very useful, such as let click = (target, "click", new MouseEvent("click"))::EventTarget.prototype.dispatchEvent. For me this feels clunkier then target::dispatchEvent("click", ...), but as @domenic says, it might be even weirder if it didn't make a call.

I kinda like the foo::bar:::(a, b) from @shovon but would rather let it be just a double colon, foo::bar::(a, b). As for those who like the comma operator, you could still use temporary variable(s). I would be ok with having ::(...) be reserved for argument binding. This does add a new case, bar::(a, b), which should become bar.bind(undefined, a, b).

rauchg commented 9 years ago

@domenic while I agree that this proposal is confusing, it's also somewhat confusing that :: is only partially equivalent to bind, since it doesn't support currying.

domenic commented 9 years ago

Let's not call it bind then so people stop asking for partial application.

gaearon commented 9 years ago

I kinda like the foo::bar:::(a, b) from @shovon but would rather let it be just a double colon, foo::bar::(a, b). As for those who like the comma operator, you could still use temporary variable(s). I would be ok with having ::(...) be reserved for argument binding. This does add a new case, bar::(a, b), which should become bar.bind(undefined, a, b).

I like this! Any drawbacks to console::log::('stuff') becoming console.log.bind(console, 'stuff')? @domenic @zenparsing

domenic commented 9 years ago

I would expect ("stuff") to behave the same as "stuff" in a value context.

gaearon commented 9 years ago

Oh right. It exudes the “extra parens” vibe..

natew commented 9 years ago

From an outsiders perspective I'd find the :: syntax incredibly helpful if it supporting currying in some way. With it, I'd have a whole variety of use cases where I'd be very happy to use it.

For people doing non-class based programming this becomes a sort of default for a lot of use cases.

let x = 0;
const add = ::(x = x + 1) // is this too crazy?
add()
const log = ::console.log('Debug: ')

I'd be ok with some modification of the way to do it to be clear though.

domenic commented 9 years ago

I think the partial application (not currying---learn your terms, people) proposal should be a separate one outside the scope of this repo.

natew commented 9 years ago

Pre-first coffee... Not returning a function so no it's not currying, it's partial application. In effect, I think we understand what we're talking about, but thanks for the reminder.

disjukr commented 9 years ago

how about making function bind syntax (foo :: arg1, arg2) +> bar to support partially applied parameters?

ex1) foo +> bar = bar.bind(foo)

ex2) () +> console.log = console.log.bind(console)

ex3) (arg1, arg2) +> foo.baz = (foo :: arg1, arg2) +> foo.baz = foo.baz.bind(foo, arg1, arg2)

nevir commented 9 years ago

Partial application of functions seems like it's really tangential to binding - I'm not sure we should continue the trend of conflating them (e.g. just because Function#bind does it doesn't make it great)

At least, I've seen a bunch of cases where I want to partially apply a function, but don't care about binding a particular this.

Can we perhaps come up with a syntax that is independent of bind/::? Maybe I'm just being a bit crazy, though :P

maxnordlund commented 9 years ago

Well, one of the reason I would like some syntax support is precisely because of that. Currently you cannot partially apply a function without also binding it's this value, save for returning a function closure. See #18 for more discussion and syntax variants.

I personally like the pipeline from Elixir and/or LiveScript

getPlayers()
    |> map(x => x.character())
    |> takeWhile(x => x.strength > 100)
    |> forEach(x => console.log(x));
MikeMcElroy commented 9 years ago

+1 to @nevir on separating this binding from partial application. If we could figure out some sort of syntax that would complement ::, that'd be +1000.

::foo.bar/* Something? */(arg1, arg2)

I don't want to insert another goofy symbol to the mix, so I won't. But if we can just come up with something to do partial application in this way, it should flow nicely with how :: currently stands (and I think is awesome, btw)

nathggns commented 9 years ago

I haven't read this entire thread but is there anyway we could have :::foo.bar(1, 2) meaning foo.bar.bind(foo, 1, 2)? Notice the three ::: instead of two to signify partial application.

Sent from my iPhone

On 14 Aug 2015, at 23:16, MikeMcElroy notifications@github.com wrote:

+1 to @nevir on separating this binding from partial application. If we could figure out some sort of syntax that would complement ::, that'd be +1000.

::foo.bar/* Something? */(arg1, arg2)

I don't want to insert another goofy symbol to the mix, so I won't. But if we can just come up with something to do partial application in this way, it should flow nicely with how :: currently stands (and I think is awesome, btw)

— Reply to this email directly or view it on GitHub.

MikeMcElroy commented 9 years ago

::: can be very easily confused with ::, IMHO.

shovon commented 9 years ago

::: can be very easily confused with ::, IMHO.

When I initially proposed :::, @nathggns pointed out how it's too similar to ::. Soon after, I suggested the :> syntax, which is far easier to distinguish.

MikeMcElroy commented 9 years ago

@shovon I have no real problem with :>, not that my vote has anything to do with it. Arguing over the symbol to use for this is not something I care to do, just so long as we get a nice easy-to-use symbol for it.

msegado commented 9 years ago

Another big +1 on @nevir's comment.

I had some syntax/implementation thoughts on this after reading Jeremy Fairbank's blog post; re-posting my comment here in case there are helpful ideas, though with a warning that I haven't re-read it to make sure the code examples are correct:

Re. the partial application syntax, what about separating it from the context binding entirely? Like the following:

// syntax based on the ES2015 spread operator:
add2 = add(2, ...);
debug = ::console.log('DEBUG:', ...);

// or, syntax based on the ES2016 bind operator:
add2 = add::(2);
debug = ::console.log::('DEBUG:');

This could translate to something like the following (using ES2015 for brevity):

add2 = (add => {
    return function partial(...args) { add.call(this, 2, ...args); };
}(add));

Note that (1) an IIFE is used to capture the 'add' binding at the time of partial application in case it's re-bound later in the code (seems like the right thing to do), and (2) the context variable is deliberately not bound until the partial function is called, so we can do things like this [contrived example]:

// spread-like syntax:
const concat4 = Array.prototype.concat(4, ...);
const concat45 = concat4(5, ...);
console.log([1, 2, 3]::concat45(6)); // [1, 2, 3, 4, 5, 6]

// bind-like syntax:
const concat4 = Array.prototype.concat::(4);
const concat45 = concat4::(5);
console.log([1, 2, 3]::concat45(6)); // [1, 2, 3, 4, 5, 6]

I've gotta say, I'm quite partial to the spread-like syntax... (I'll see myself out =P)

MikeMcElroy commented 9 years ago

+1 @msegado's spread-like syntax. Declarative and complementary to ::.

shovon commented 9 years ago
add2 = add(2, ...);

@msegado oh, snap! The spread operator wins!

@maxnordlund thoughts?

msegado commented 9 years ago

@MikeMcElroy @shovon Thanks!

By the way, we could hypothetically allow application of final parameters with this syntax too...

longDelay = window.setTimeout(..., 10000);

...though I'm not sure how well that fits with the language design as a whole; a normal rest parameter can only come at the end of a function's parameter list (I assume for reasons of optimizability?), and this is kind of like a function definition.

RangerMauve commented 9 years ago

+1 for the spread operator. Though this should really be defined in a different spec.

msegado commented 9 years ago

@RangerMauve Thanks =) And I agree, it would probably make sense to remove partial application from this spec and advance it separately. @maxnordlund @nathggns, thoughts? (I'm assuming @zenparsing is already leaning toward this given his earlier comment).

maxnordlund commented 9 years ago

Yeah, this does look rather neat. To self promote a bit, it also would complement my suggestion for a application only syntax. But as mentioned by others, both here and in #18, we should have this discussion somewhere separate.

zenparsing commented 8 years ago

Closing this as out-of-scope.