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

Sugar for function binding, uncurrying this and setting this context #2136

Closed
shesek opened this issue Feb 24, 2012 · 16 comments
Closed

Sugar for function binding, uncurrying this and setting this context #2136

shesek opened this issue Feb 24, 2012 · 16 comments

Comments

@shesek
Copy link

shesek commented Feb 24, 2012

Somewhat based on a syntax proposed on es-discuss, I would like to suggest the following additions:

  • Setting the this context for function applications:

    func@obj a, b, c # Desugars to `func.call obj, a, b, c`
    func@obj a..., b # Desugars to `func.apply obj, [a..., b]`
  • Binding functions to objects:

    func@obj # Desugars to [the ES3-equivalent of] `func.bind obj`

    Possibly even func@@ as sugar for func@this? Could be useful as a fat-arrow version for non-literal functions.

  • Uncurrying this: (as proposed on es-discuss)

func@ # Desugars to [the ES3-equivalent of] `Function::call.bind func`
# A possible use-case is for passing-in a function that expects `this` rather than an
# argument, to somewhere that passes the data as an argument, like `map`:
['foo', 'bar', 'baz'].map String::trim@

For complex expressions on the RHS or LHS of the @, parentheses should be used, e.g. func@(foo bar), (baz qux)@obj, (baz qux)@(foo bar)

@jashkenas
Copy link
Owner

Whoa. We've had an explicit bind operator in the past (<-) -- but this mirroring of @ for setting this is both punny and clever. Let's pursue @shesek's idea...

What if func@context were simply an expression that evaluated to the context-bound version of the function. As shown above, implicit calls would still work ... arbitrary expressions would be allowed as both the LHS and RHS. Can anyone see any potential problems or ambiguities with this syntax?

@jashkenas
Copy link
Owner

Oh, wait ... just thought of one. This wouldn't play very nicely with function literals -- at least without making a special case...

((x)->
  @value * x
)@context

... hideous.

@satyr
Copy link
Collaborator

satyr commented Feb 25, 2012

So basically:

  • f@f.bind()
  • f@xf.bind(x)
  • f@x yf.bind(x)(y)

@goomtrex
Copy link

I like this idea, mostly because it unifies binding and call/apply with a single syntax.

Just wanted to point out that a binding/currying operator can achieve a similar thing...

The operator in that proposal, currently <~, applies the function on the LHS to its receiver (if present) and partially applies the arguments:

receiver.fn <~ 1, 2, 3

Becomes:

__curry.call(__bind(receiver.fn, receiver), 1, 2, 3);

We bind by currying call with no arguments:

String::trim.call <~

Becomes:

__bind(String.prototype.trim.call, String.prototype.trim);

So we can write the binding examples above...

func@obj
func@@
func@
['foo', 'bar', 'baz'].map String::trim@

As:

func.call <~ obj
func.call <~ @
func.call <~
['foo', 'bar', 'baz'].map String::trim.call <~

Becoming:

__curry.call(__bind(func.call, func), obj);
__curry.call(__bind(func.call, func), this);
__bind(func.call, func);
['foo', 'bar', 'baz'].map(__bind(String.prototype.trim.call, String.prototype.trim));

There's a bit more to the operator, such as left-associativity, so if you're interested I recommend checking it out.

@showell
Copy link

showell commented Feb 25, 2012

@jashkenas Definitely not worth the complexity and obscurity. If a function is intended to be used by multiple objects, then it should stand on its own.

print_name = (obj) ->
  console.log obj.name

obj1 =
  name: 'CoffeeScript'
  author: 'jashkenas'

obj2 =
  name: 'New York'
  visit: -> console.log 'Welcome to the Big Apple!'

# now just call print_name functionally
print_name obj1 # CoffeeScript
print_name obj2 # New York

# Now that you have print_name, you can still add
# it to class APIs as needed
class Animal
  constructor: (@name) ->
  print_name: => print_name @

dog = new Animal("Rex")
dog.print_name()

Of course, I understand you might be dealing with third-party code that implements a nice algorithm, and the original authors might not have understood the wisdom of decoupling algorithms from objects. It would be nice to see a few more concrete cases where that technique actually comes up. It seems like calling forEach and trim for foreign objects would be brittle to begin with.

@shesek
Copy link
Author

shesek commented Feb 25, 2012

@jashkenas

Treating the func@context as a standalone expression is definitely simpler and easier to add to the grammar, but isn't the performance gain (especially on ES3 that doesn't have native support for Function::bind) of compiling func@context a,b,c directly to .call() (rather than binding first than calling -func.bind(context)(a,b,c)) worth it?

And yes, it doesn't play very nicely with function literals, but I don't think that'll be very common. you can just use the context directly in the function body or the fat-arrow when binding to this (which is probably the common case for function literals).

@alexkg

I know some of this can be achieved with the partial application operator (except for calling directly with the context) - but I find it much less readable than a dedicated operator, having .call all over the code isn't very elegant and it seems like (unfortunately, I really want it too) the partial application operator isn't going to be added to CS any time soon.

@showell

I do agree with your point, and I personally prefer functional-style code over OOP-style, so I would hardly ever use it to call my own functions.

But, as you said, not everyone agrees with that and a lot of libraries (and native functions, mostly Array methods) are exposing functions that expect to be run on directly from an object, but are also useful as a standalone functions that could run on other objects too.

Also, I have lots of higher-order functions that delegate to functions passed as arguments (like in partial/compose implementations, function decorators, various functions that combine other functions together, etc) where its common to let the this context pass-through rather than nullifying it (or "windowying" it non-strict mode).
For those cases, its very useful and elegant to simply add @this rather than having to .apply and play with arrays (compare the elegance of partial = (fn, a...) -> (b...) -> fn@this a..., b... with having to fn.apply this, [a..., b...]).

@satyr
Copy link
Collaborator

satyr commented Feb 25, 2012

fn.apply this, [a..., b...]

Or fn.call this, a..., b....

@showell
Copy link

showell commented Feb 25, 2012

@shesek I guess I just think it's already easy enough to decouple functions from "this"-ness with the current syntax. All you need is a generic high-order function called "this_free" (or perhaps some better name).

set_abc = (a, b, c) ->
  @a = a
  @b = b
  @c = c

set_abc 'A', 'B', 'C'
console.log root.a

# Now wrap set_abc with a function that
# can operate on any object.
this_free = (f) ->
  (obj, args...) -> f.call obj, args...

f_set_abc = this_free set_abc

# then try it out on a couple objects
obj1 = {}
obj2 = {}

f_set_abc obj1, 'a1', 'b1', 'c1'
f_set_abc obj2, 'a2', 'b2', 'c2'
console.log obj1.a, obj2.a 

Even if you like the sugar, it's worth asking how often the feature would get used. Ultimately, the syntax would only come up when people normally use "call" and "apply", which I'm guessing is pretty infrequent in most codebases. Also, the sugar doesn't reduce line count, so I'm not seeing a big win in expressiveness.

@shesek
Copy link
Author

shesek commented Feb 25, 2012

@satyr
I used to do that - but I find that even worse, as it compiles to .call.apply which isn't very readable (and probably doesn't perform as well)

@showell
call() and apply() aren't common in JavaScript? I think they're actually quite common... I personally find myself using them quite a lot, and some greping around finds that its used 89 times in jQuery, 25 times in CoffeeScript, 23 times in Backbone, 2575 times in Cloud9, 84 times in npm and 743 times in nodejs (line counts, probably a bit more due to multiple usage in a single line). And there's also bind for the func@context case.

IMHO setting a different context is better expressed using a dedicated operator, as it should stand-out as a special operation rather than yet another function call. I also find that syntax more natural for the developer to write and for his co-workers to read and understand. That being said, expressiveness mostly boils down to personal preference and coding style, so there isn't really right and wrong here.

BTW, this_free is commonly referred to as uncurryThis (and can be simply defined as Function::bind.bind Function::call)

@showell
Copy link

showell commented Feb 25, 2012

@shesek I known that call/apply are fairly common in JavaScript projects, but I was wondering about CoffeeScript projects. An interesting discussion example might be underscore.coffee, which uses "apply" pretty frequently (grep output below):

  (if method then val[method] else val).apply(val, args) for val in obj
  return Math.max.apply(Math, obj) if not iterator and _.isArray(obj)
  return Math.min.apply(Math, obj) if not iterator and _.isArray(obj)
  -> func.apply obj or root, args.concat arguments
  setTimeout((-> func.apply(func, args)), wait)
    key = hasher.apply this, arguments
    memo[key] = func.apply this, arguments
  _.delay.apply _, [func, 1].concat _.rest arguments
  -> wrapper.apply wrapper, [func].concat arguments
      args = [funcs[i].apply(this, args)]
_.isFunction  = (obj) -> !!(obj and obj.constructor and obj.call and obj.apply)
    'var p=[],print=function(){p.push.apply(p,arguments);};' +
    result func.apply(_, args), this._chain
    method.apply(this._wrapped, arguments)
    result(method.apply(this._wrapped, arguments), this._chain)

How do you sugar foo.apply(this.bar, yada, yada, yada)?

@shesek
Copy link
Author

shesek commented Feb 26, 2012

@shesek I known that call/apply are fairly common in JavaScript projects, but I was wondering about CoffeeScript projects

Now that I think about it, to be honest - those numbers aren't very accurate as splats replaces a lot of the cases where apply() is needed. However, call() accounts for roughly 1/3 of them, which is only used when a different this context is needed.

How do you sugar foo.apply(this.bar, yada, yada, yada)?

(assuming you meant .call) I would personally do foo@this.bar yada, yada, yada, but I assume some people would prefer to foo@@bar yada, yada, yada (which I personally find less readable)

@jashkenas
Copy link
Owner

So, closing this one for pretty much the same reasons we removed <- in the first place. Most importantly:

  • Binding a function literal is far and away the most common case, and this syntax works with it particularly poorly: ((x)-> x)@value
  • Usually, when you need to bind a function to a context, you need to bind it to the current value of this -- and the fat arrow already covers this use case -- any other variable that already exists in lexical scope is easily available within the function already.

@shesek
Copy link
Author

shesek commented Feb 27, 2012

@jashkenas This only takes into account the binding use case (which, as I said, isn't very useful for function literals anyhow), but I think that applying with context (as an alternative to call()/apply()) and uncurryThis (func@) are way more useful in this proposal. What about them?

@jashkenas
Copy link
Owner

.call() and .apply() are already good APIs for calling a function with a different context. No sugar necessary.

@michaelficarra
Copy link
Collaborator

@jashkenas: Though, that requires that "apply" in obj && obj.apply === Function.prototype.apply (or a reference to the native apply, if Function.prototype.apply is not one), a guarantee we don't really want the user to have to make. They should just be able to assume support by way of a dedicated syntax. I'm +1.

@GulinSS
Copy link

GulinSS commented Nov 13, 2016

.call() and .apply() are already good APIs

@jashkenas, in same time our competitors are using this: https://github.com/tc39/proposal-bind-operator

I think we need reinvent bind operator again.

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

No branches or pull requests

7 participants