tc39 / proposal-bind-operator

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

Generalize both binding/selection and pipeline operators #26

Closed ssube closed 8 years ago

ssube commented 8 years ago

Based on the recent discussion with @zenparsing and @impinball in this thread and issue #25 , I think it might be sanely possible to make the pipeline operator (let's say ~>, for this example) and the binding/selection operator (let's use ::) into two variants of the same general syntax.

Using the example of fetching a list of books, we would write:

getBooks() ~> map(Book::new) ~> forEach(shelf::addBook)
// instead of
forEach(map(getBooks(), data => new Book(data)), book => shelf.addBook(book))

// and
getBooks() ~> map(::author)
// instead of
map(getBooks(), book => book.author)

Using something like the rules:

That gives us four distinct (useful) expressions and two with very limited usage:

  1. scope :: property and scope :: [property]
  2. scope :: new
  3. :: property and :: [property]
  4. :: new (provided for completeness, but not very useful)
  5. target ~> property (and potentially target ~> [function])
  6. target ~> new (provided for completeness, but not very useful)

We can very naively implement them as:

  1. With scope and property:

    return function(...args) {
     if (scope[property].apply) {
       return scope[property].apply(scope, args);
     } else {
       return scope[property];
     }
    }
  2. With scope and new:

    return function (...args) {
     return new scope(...args);
    }
  3. Without scope, with property:

    return function(scope, ...args) {
     if (scope[property].apply) {
       return scope[property].apply(scope, args);
     } else {
       return scope[property];
     }
    }
  4. Without scope, with new:

    return function (scope, ...args) {
     return new scope(...args);
    }

For the pipeline operator, the inner invocation of scope[property].apply(scope, args) would be replaced with scope[property].apply(this, [scope].concat(args))

Breaking down the examples from the start:

getBooks() ~> map(Book::new)
// becomes
(function (left) {
  return map(left, function (...args) {
    return new Book(...args);
  });
})(getBooks())

and

getBooks() ~> map(::author)
// becomes
(function (left) {
  return map(left, function (obj) {
    return obj.author;
  });
})(getBooks())

I'm sure there are some edge cases (or even obvious cases) I'm missing here, but figured I would throw this out for folks to poke holes in.

dead-claudia commented 8 years ago

@ssube

You beat me to it...I was about to file this myself. :wink:

Pipeline operator

Binding operator


FWIW, I see these two features becoming landmark features when it finally stabilizes and more people hear about it. People are going to love the shorthands and pipelining. It would almost render Lodash's _.flow useless. It could also be used without changing the API for current libraries.

// Maybe future JS
import {map, forEach} from 'underscore';

class Foo {
  constructor(value) { /* body */ }
}

const classes = new Set();
const instances = new Set();
const objs = [/* entries */];
objs
  ->map(::foo)
  ->forEach(classes::add)
  ->map(Foo::[new])
  ->forEach(instances::add);

Another thing I like is that with the current state of this idea, types can easily be statically checked.

// TypeScript-like syntax for exposition

type PropGetter<T> = (obj: {prop: T}) => T
::prop

// By sheer coincidence...
type BoundMethod<T, ...US> = (obj: {prop(...args: US): T}) => (...args: US) => T
                           = PropGetter<(...args: US) => T>
obj::prop

// These are identical, and can be type checked as such:
obj->foo()
foo(obj)
dead-claudia commented 8 years ago

Another interesting thing with this syntax in this gist.

dead-claudia commented 8 years ago

And for precedence, if both binding and the pipeline operator can be made with the same precedence as method application, that would probably be best.

@zenparsing @ssube WDYT?

zenparsing commented 8 years ago

@impinball

My thoughts are more-or-less in agreement with what you've presented, with the following caveat:

I don't think introducing new as a special case computed property name thing is going to fly. One day we might get something like Symbol.construct, but that's just speculation. It might be better to punt on the new variant for now.

As far as operator precedence goes, we need to think about how these operators interact with "new".

new C::foo();
  (new C)::foo();  // 1
  new (C::foo)();  // 2
new C->foo();
  (new C)->foo();  // 1
  new (C->foo());  // 2

I would probably argue for 2 in the case of :: and 1 in the case of ->.

We also need to have a story for computed property names in the case of ::, and for more complex expressions in the case of ->.

Do we use square brackets?

obj::[Symbol.iterator];

That makes sense, I suppose.

What about the pipeline operator? Do the square brackets make sense there?

obj->[myLib.func]();

Or would parenthesis be more appropriate?

obj->(myLib.func)();
ssube commented 8 years ago

@zenparsing Do computed property names as part of an accessor make sense? It's my understanding that computed names were introduced primarily for places where you couldn't precompute the name, like object literals, but it's easy to books -> map(::['bar-' + foo]) and use the existing variable name syntax.

W.r.t. removing ::new, I'm not sure how I feel about that. Being able to reference new with an attached type is awfully convenient, although I would agree that something more future-proof could be helpful.

@impinball I agree that the tilde isn't the best choice. Skinny arrows might be better, as they look like the lambda syntax and we are applying a loose function. That's pretty abstract thinking, but they're also easy to type.

zenparsing commented 8 years ago

@ssube Also, I'm not feeling the unary ::.

ssube commented 8 years ago

@zenparsing :: may not be the right choice, as it is often seen as a scope resolution operator, showing up in Java 8, C, C++, and some Ruby DSLs (especially Puppet).

You could make an argument for -> as an indirect reference operator (shows up in C and C++), but that leaves us out a pipeline operator. We could use the -> as a unary operator as well: getBooks().then(-> author) doesn't look terrible. I would lean toward something showing less motion, but -> does fit with how JS uses => to represent loose functions and now accessors.

The |> suggestion for pipeline could work and would continue the _> theme of these unusual application operators. So would +>.

zenparsing commented 8 years ago

@ssube Sorry, I was overly terse there. I meant that I don't see a good justification for syntax supporting those semantics, beyond what can already be done though normal function calling.

getBooks()->map(::author)

// You could just do something like:
getBooks()->mapToProp('author');

// And it's probably clearer what's going on anyway

Syntax proposals work best when they are really tightly focused around compelling use cases.

ssube commented 8 years ago

@zenparsing Oh, I misunderstood that. It's true that ::property is just shorthand for -> pluck(property), which in turn is shorthand for .map(it => it[property]). While I do really like the idea, there are enough options already that we can probably omit it for now.

benjamingr commented 8 years ago

All 99% of people care about is binding a method to an object in a scoped way, that can be with partials or with this and people find it immensely useful.

All other use cases like binding to new, unary :: and other stuff are probably not things the proposal should include. I suspect syntax is also not a big deal for people as long as infix is supported.

dead-claudia commented 8 years ago

@ssube It's synonymous with it => it.property. It's not computed.

@benjamingr That's true except for new (that should be there, because x => new Class(x) isn't that uncommon, and for consistency).

The unary version should probably be put on hold for now. Is that okay, @zenparsing?

zenparsing commented 8 years ago

@impinball Yep

dead-claudia commented 8 years ago

And next question: what should the expected behavior of object->method (i.e. not a call expression) be? I think method.bind(undefined, object) could work in this case, but what do you all think?

On Fri, Sep 25, 2015, 10:31 zenparsing notifications@github.com wrote:

@impinball https://github.com/impinball Yep

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

zenparsing commented 8 years ago

@impinball I think that should probably be a syntax error (at least for now). In other words, -> should only be allowed if the right hand side is followed by an argument list.

obj->foo; // Syntax error
obj->foo(); // OK

In my mind, it's just a different way of calling a function allowing for pleasant chaining.

dead-claudia commented 8 years ago

That can work. It ride another train. I'm fine with it (it's not a common case). Besides, it's effectively partial application, anyways. (unary partial application in this case).

On Fri, Sep 25, 2015, 16:00 zenparsing notifications@github.com wrote:

@impinball https://github.com/impinball I think that should probably be a syntax error (at least for now). In other words, -> should only be allowed if the right hand side is followed by an argument list.

obj->foo; // Syntax error

obj->foo(); // OK

In my mind, it's just a different way of calling a function allowing for pleasant chaining.

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

ssube commented 8 years ago

@zenparsing I actually think it would make a lot more sense, looking from the desired behavior, to define both operators as returning functions which can then be called normally. Defining them as a type of call seems more complicated on the standardization side (when have we introduced a new type of call?), where as leaving them as binary operators that return a function is very simple behavior, but also allows a lot more flexibility. This is especially important for the binding operator, which loses much of its power if you can't assign the results.

zenparsing commented 8 years ago

@ssube I don't mean that -> would introduce a new kind of call. I mean that it would just be pure sugar:

obj->foo(1, 2, 3);
// Desugars to:
// foo(obj, 1, 2, 3);

Clearly the :: operator would evaluate to a new function, though.

bergus commented 8 years ago

When the right operand is: callable, both operators return a function taking ...args and invoking the operand otherwise, both operators return a function which returns the value of the operand

Please don't do this. Doing completely different things depending on the type of the argument is error-prone. If the property doesn't resolve to a method, just throw a TypeError, like every call on something non-callable does.

It's not that I would not like a shorthand for _=>_.property (which is already pretty short), but please don't mix this with the method binding operator.

ssube commented 8 years ago

It sounds like we've moved from the original post (which I'll leave for posteriority) to something like:

That is, original example 1.

ES6 desugar for instance::method:

return instance.method.bind(instance);

ES6 desugar for instance->method:

return function(...args) {
  return instance.method.apply(this, [instance].concat(args));
}

Does that accurately represent the current suggestions?

Given the discussion, I feel like it's more appropriate to check for bind and apply rather than just typeof instance.method === 'Function', if that's possible (stick with duck typing). It would allow these to interact with functors much better.

jasmith79 commented 8 years ago

Object with an internal [[Call]] (i.e. an actual Function object) property would be the obvious choice... not sure if its the best one. Leaving the door open to objects that implement .bind or .apply is more flexible.

bergus commented 8 years ago

@ssube OK, I didn't really follow the discussion (just read through everything), thanks for dropping the property access thing. As I just commented on #19, I don't like passing as the first operand, but lets discuss this over there. (I would like infix :: for extraction and -> for binding as separate operators though). And I think you want instance -> method to desugar to method.apply…, not instance.method.apply…. Typo?

I feel like it's more appropriate to check for bind and apply, It would allow these to interact with duck-type functors much better.

Hm, interesting idea, but I'm not sure what "functors" you're talking about here. Not these I guess? Also there's a potential hazard when using the operator on callables that don't inherit from Function.prototype (think console.log in old IE), which I guess is still relvant for transpilation usage. I would have expected that the "desugaring" is only as a simple equivalence showcase, and that the real spec refers to the builtin bind and apply methods - so more like

// extraction
var method = instance[property]
return %bind(method, instance);

// binding (virtual method)
return %bind(function, instance);

// partial (virtual function)
if (! %isCallable(function)) throw new TypeError(…);
return function(…arglist) {
    return %apply(function, this, %cons(instance, arglist));
};

Using the builtin bind method would also have the advantage that it throws the error on non-callables for us automaticallly. Also I would expect that the instance :: function() syntax should be optimisable by the engine into %call(function, instance, …) so that it does not require an actual invocation of bind with an actual creation of a bound function. I think specifiying a check for .bind/.apply methods (why not only apply, btw?) would complicate this optimistion - I could be wrong on that though.

dead-claudia commented 8 years ago

For what it's worth, I've temporarily rescinded the suggestion of the omitted left operand. Here's the status of this discussion to my understanding (the proposal doesn't reflect this yet):

Function chaining: obj->func

Method binding:

On Thu, Nov 5, 2015, 16:01 Bergi notifications@github.com wrote:

@ssube https://github.com/ssube OK, I didn't really follow the discussion (just read through everything), thanks for dropping the property access thing. As I just commented on #19 https://github.com/zenparsing/es-function-bind/issues/19, I don't like passing as the first operand, but lets discuss this over there. (I would like infix :: for extraction and -> for binding as separate operators though). And I think you want instance -> method to desugar to method.apply…, not instance.method.apply…. Typo?

I feel like it's more appropriate to check for bind and apply, It would allow these to interact with duck-type functors much better.

Hm, interesting idea, but I'm not sure what "functors" you're talking about here. Not these https://github.com/fantasyland/fantasy-land#functor I guess? Also there's a potential hazard when using the operator on callables that don't inherit from Function.prototype (think console.log in old IE), which I guess is still relvant for transpilation usage. I would have expected that the "desugaring" is only as a simple equivalence showcase, and that the real spec refers to the builtin bind and apply methods - so more like

// extractionvar method = instance[property]return %bind(method, instance); // binding (virtual method)return %bind(function, instance); // partial (virtual function)if (! %isCallable(function)) throw new TypeError(…);return function(…arglist) { return %apply(function, this, %cons(instance, arglist)); };

Using the builtin bind method http://www.ecma-international.org/ecma-262/6.0/#sec-function.prototype.bind would also have the advantage that it throws the error on non-callables for us automaticallly. Also I would expect that the instance :: function() syntax should be optimisable by the engine into %call(function, instance, …) so that it does not require an actual invocation of bind with an actual creation of a bound function. I think specifiying a check for .bind/.apply methods (why not only apply, btw?) would complicate this optimistion - I could be wrong on that though.

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

zenparsing commented 8 years ago

@impinball Right. That's the basic idea behind the two-operator counter-proposal.

I'm still on the fence about whether this is actually any better than the original proposal. The original proposal was very simple and elegant. In any case, I'll try to get a feel from the committee members on which alternative will have a better chance of advancing later this month.

dead-claudia commented 8 years ago

@zenparsing It's more about the use of this. Whether we should bind the argument to this or not. Which would you prefer?

bergus commented 8 years ago

I'm much in favour of option 2. Especially because you can do

const {map, reduce} = Array.prototype;

and be done. I would expect this to work (i.e., be useful) on many other classes as well.

dead-claudia commented 8 years ago

@bergus

**Edit: I mis-remembered Mori's API...Feel free to s/Mori/Lodash/g and s/m/_/g throughout.***

That is a great bonus, if you're mostly interfacing with native APIs. You could even do the same with hasOwn, hasOwn.call(obj, prop)obj::hasOwn(prop).

The bonus for option 1 is for libraries like Lodash, Underscore, and especially Mori. I do feel it's more ergonomic to use Option 1, since you can also use arrow functions to create the helpers, and they don't bind this. It's still easy to wrap either one, though.

To wrap natives for Option 1:

const wrap = Function.bind.bind(Function.call)
const wrap = f => (inst, ...rest) => Reflect.apply(f, inst, rest)

To wrap third party libraries for Option 2:

const wrap = f => function (...args) { return f(this, ...args) }

If you want an automagical wrapper, you can always use a very simple Proxy:

function use(host, ...methods) {
  const memo = {}
  return new Proxy(host, {
    get(target, prop) {
      if ({}.hasOwnProperty(memo, prop)) return memo[prop]
      return memo[prop] = wrap(host[prop])
    }
  })
}

Example with Option 1 + wrapper:

const m = mori

m.list(2,3)
  ->m.conj(1)
  ->m.equals(m.list(1, 2, 3))

m.vector(1, 2)
  ->m.conj(3)
  ->m.equals(m.vector(1, 2, 3))

m.hashMap("foo", 1)
  ->m.conj(m.vector("bar", 2))
  ->m.equals(m.hashMap("foo", 1, "bar", 2))

m.set(["cat", "bird", "dog"])
  ->m.conj("zebra")
  ->m.equals(m.set("cat", "bird", "dog", "zebra"))

// Using Array methods on utilities
const {map, filter, forEach, slice: toList} = use(Array.prototype)
const tap = (xs, f) => (forEach(xs, f), xs)

document.getElementsByClassName("disabled")
  ->map(x => +x.value)
  ->filter(x => x % 2 === 0)
  ->tap(this::alert)
  ->map(x => x * x)
  ->toList()

// Same example with Option 2 + wrapper:

const m = mori
const {conj, equals} = wrap(m)

m.list(2,3)
  ::conj(1)
  ::equals(list(1, 2, 3))

m.vector(1, 2)
  ::conj(3)
  ::equals(m.vector(1, 2, 3))

m.hashMap("foo", 1)
  ::conj(m.vector("bar", 2))
  ::equals(m.hashMap("foo", 1, "bar", 2))

m.set(["cat", "bird", "dog"])
  ::conj("zebra")
  ::equals(m.set("cat", "bird", "dog", "zebra"))

// Using Array methods on utilities
const {map, filter, forEach, slice: toList} = Array.prototype
function tap(f) { this::forEach(f); return this }

document.getElementsByClassName("disabled")
  ::map(x => +x.value)
  ::filter(x => x % 2 === 0)
  ::tap(::this.alert)
  ::map(x => x * x)
  ::toList()

Also, Lodash, Underscore, and Mori have already implemented helpers that Option 1 basically negates. Lodash has _.flow which is the composition operator in reverse, and Mori has mori.pipeline. Underscore also has function composition. Option 1 integrates better with existing libraries, IMHO.

// Mori
m.equals(
  m.pipeline(
    m.vector(1,2,3),
    m.curry(m.conj, 4),
    m.curry(m.conj, 5)),
  m.vector(1, 2, 3, 4, 5))

// Option 1
m.equals(
  m.vector(1,2,3)
  ->m.conj(4)
  ->m.conj(5),
  m.vector(1, 2, 3, 4, 5))

// Option 2
const conj = wrap(m.conj)

m.equals(
  m.vector(1,2,3)
  ::conj(4)
  ::conj(5),
  m.vector(1, 2, 3, 4, 5))

Well...in terms of simple wrappers, Node-style to Promise callbacks aren't hard to similarly wrap, either. I've used this plenty of times to use ES6 Promises instead of pulling in a new dependency.

// Unbound functions
function pcall(f, ...args) {
  return new Promise((resolve, reject) => f(...args, (err, ...rest) => {
    if (err != null) return reject(err)
    if (rest.length <= 1) return resolve(rest[0])
    return resolve(rest)
  }))
}

// Bound functions
function pbind(f, inst, ...args) {
  return new Promise((resolve, reject) => f.call(inst, ...args, (err, ...rest) => {
    if (err != null) return reject(err)
    if (rest.length <= 1) return resolve(rest[0])
    return resolve(rest)
  }))
}

// General wrapper
const pwrap = (f, inst = undefined) => (...args) => {
  return new Promise((resolve, reject) => f.call(inst, ...args, (err, ...rest) => {
    if (err != null) return reject(err)
    if (rest.length <= 1) return resolve(rest[0])
    return resolve(rest)
  }))
}
bergus commented 8 years ago

Option 1 integrates better with existing libraries

Yes, I can see that. However I think when we are proposing new syntax for the language, we should do the right thing™ (whatever that is) rather than limiting us to the aspect of usefulness for code that was written without such capability. Ideally we want new patterns to emerge that bring the language forward, towards a more consistent/efficient/optimal language. I don't know whether Option 2 is better than Option 1 (who does?), but if it is we should go for it despite Option 1 being closer to existing patterns.

benjamingr commented 8 years ago

Adapting between options 1 and 2 is trivial. Both options are good - we should just go with whichever one the TC is cool with since this is the bike shed of the proposal anyway.

The important thing is to be able to extend an object from the outside in a scoped way.

dead-claudia commented 8 years ago

The one thing I will continue to maintain is that chaining and binding should have visibly different operators. Not just unary/binary, but completely different operators. I would prefer an infix operator to an unary operator that requires a member expression. Another idea I had in the past to compensate was this:

I just want binding to have something infix with the same precedence as a dot, and clearly different from whatever's used for chaining.

zenparsing commented 8 years ago

@impinball Thanks for the thorough write up and analysis; it helps.

I would prefer an infix operator to an unary operator that requires a member expression.

I would argue that an infix operator ends up being just as weird when dealing with computed properties:

obj::[prop];

Whereas the prefix form just handles that case naturally. The prefix form is a bit odd looking, but everyone seems to say that they get used to it quickly.

In any case, I agree with @benjamingr that either option will work. I'll try to get some committee feedback and let everyone know how it goes.

dead-claudia commented 8 years ago

@zenparsing Thanks.

I can get used to whatever it ends up being. I have a strong preference, but I don't have any sort of religious attachment to it. Either way, I want the feature more than anything else. :smile:

ariporad commented 8 years ago

@zenparsing, @impinball: Sorry if this is explained somewhere else/obvious (I haven't been following this closely), but is the ::myObj.method syntax (which is equivalent to myObj.method.bind(myObj)) going away? (pleasesaynopleasesaynopleasesayno).

Thanks!

bergus commented 8 years ago

@ariporad No, there will always be an equivalent for myObj.method.bind(myObj), it's not going away. We're just not sure whether a prefix :: operator is the best choice for that, and are pondering the alternatives.

dead-claudia commented 8 years ago

@ariporad @bergus is right, and the primary proposed alternative is myObj::method. Do note that it's most likely at the moment that nothing will change, though.

On Sun, Nov 29, 2015, 18:25 Bergi notifications@github.com wrote:

@ariporad https://github.com/ariporad No, there will always be an equivalent for myObj.method.bind(myObj), it's not going away. We're just not sure whether a prefix :: operator is the best choice for that, and are pondering the alternatives.

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

spion commented 8 years ago

The reason why I prefer option 2:

  1. this is the natural default argument in JS, but there is no syntax to express that. Once we actually get the syntax, libraries will be happy to adapt.
  2. JS-made libraries (lodash) tend to favour a callback-last design (when there are callbacks to be had) But, argument order isn't seen as important in JS other than aesthetic reasons: ramda breaks that convention to get more useful partial application.
  3. How does Option 1 work with mori and Ramda? As far as I can tell in those libraries the first argument is the function, not the list. So if we do
import {map} from 'ramda'

[1,2,3]->map(x => x + 1)

this will be translated to

map([1,2,3], x => x + 1)

which is wrong

If we're not going to be using this, then the pipeline operator may be a better proposal: https://github.com/mindeavor/es-pipeline-operator

zenparsing commented 8 years ago

@spion I agree completely and prefer option 2 as well.

dead-claudia commented 8 years ago

@spion Mori is actually somewhat inconsistent. In some places, it's collection-first, while in others, it's function-first. Lodash and Underscore are always collection-first, though. Rambda is always function-last, but it also heavily uses currying and partial application, making it a good one to use in languages like LiveScript (which makes frequent use of its pipeline operator).

jasmith79 commented 8 years ago

You mean ramda is function-first?

@spion using ramda you could always use flip. I like ramda better than the alternatives but it is less popular by an order of magnitude.

dead-claudia commented 8 years ago

@jasmith79 Yes. And no, you can't use R.flip that way if the function takes more than 2 arguments. Their example explains why pretty well.

jasmith79 commented 8 years ago

@impinball Sorry I was perhaps a bit too implicit with my point. Ramda users are smart enough to work around the limitations. Just by virtue of their popularity the same is not necessarily true of lodash/underscore users. Also, I mentioned flip because R.map (which the spion's example used) is binary.

dead-claudia commented 8 years ago

@jasmith79 And most JS language features are used by people who aren't necessarily smart enough to work around language or library limitations. ES6 classes were meant to help alleviate a set of footguns and simplify a common pattern, which for beginners, now instead of having to ask why for ... in enumerates prototype methods, or be confused over the many object creation patterns (class-like constructor, factory, OLOO-based, stamps, etc.). It was one of the additions to simplify mental models. There's also arrow functions, which don't screw with this (a common source of confusion and bugs, despite how simple of a concept it really is).

My wrapping function for the pipeline idea (Option 1 and a recent, separate proposal in es-discuss) was this:

const wrap = Function.bind.bind(Function.call)

Admittedly, it's quite cryptic in how it works, and it's not exactly obvious to the novice JavaScripter. The more obvious implementation is this:

function wrap(f) {
    return (inst, ...args) => f.apply(inst, args)
}

Aside: If we use option 2, there really needs to be a way to alias this somehow. And that will become more urgent once this proposal gets to Stage 1.

spion commented 8 years ago

My opinion is that you can't really escape the weirdness of this until you explain it for what it is - the hidden function argument:

import {map} from 'lodash-bound'

thisArg::map(firstArg, secondArg)

And then give the ability to bind it (when the function isn't called).

import {map} from 'lodash-bound'

let mapOverThisArg = thisArg::map // bind lodash `map` to the object `thisArg`
let incremented = mapOverThisArg(x => x + 1)

promise.then(::console.log) // autobind to owner object, sugar for console::(console.log)

Finally you can say that arrow functions are pre-bound to the current (lexical) this:

let inc = x => x + this.incVal
// is equivalent to
let inc = this::(function(x) { return x + this.incVal })

You can even explain the dot-invocation in terms of it:

obj.method(argument)
// equivalent to
obj::(obj.method)(argument)
// equivalent to (although it creates a bit more garbage)
(obj::obj.method)(argument)
// equivalent to
(::obj.method)(argument)

Now the bind operator is the missing link that helps explain how this really works and how it relates with other features.

edit: clarified that its lodash map in the second example

jasmith79 commented 8 years ago

@spion that's not quite how this works in arrow functions. Its not pre-bound, arrow functions are not passed the 'hidden function argument', so this is resolved via scope-chain lookup just like any other binding. Although to be fair, that misconception is rather popular, even on MDN, which probably speaks to @impinball's point.

spion commented 8 years ago

@jasmith79 can you explain whats the observable difference between resolving this via scope-chain lookup and binding this?

(I do realize that arrow functions additionally don't have a prototype and cannot be used as constructors but in terms of explaining how this works in them, the bind operator is sufficient)

Whats the observable difference between:

In an arrow function, this behaves as if the hidden this argument was already partially applied (bound) to the function and

and

In an arrow function, this is looked up in the current lexical scope.

Not sure why you are quoting "hidden function argument" either. Thats precisely what this in JS is.

gilbert commented 8 years ago

I have a clarification question. When I read this example:

import {map} from 'lodash-bound'

let mapOverThisArg = thisArg::map // bind `map` to the object `thisArg`
let incremented = mapOverThisArg(x => x + 1)

I immediately wonder if this is possible with arrays:

let mapOverArray = [10,20,30]::map
let incremented = mapOverArray(x => x + 1) //=> [11, 21, 31]

Is this possible, assuming map is not a variable in scope ? Put another way, can :: bind method properties, or does it only work for variables?

zenparsing commented 8 years ago

@mindeavor There are two "flavors" of the bind operator.

The infix form works like your first example:

import { map } from 'lodash-bound';
thisArg::map; // Binds thisArg to the map function

There is also a prefix form, which does method extraction:

let mapOverArray = ::[10,20,30].map; // Binds the array to the map property of the array
let incremented = mapOverArray(x => x + 1) //=> [11, 21, 31]
jasmith79 commented 8 years ago

@spion only one I know of that would matter to a developer is that you can't re-bind it with Function.prototype.bind/call/apply the way you would if you had simply closed over var that = this; in your returned function: there's no implicit this to be bound. See http://blog.getify.com/page/5/ for a more thorough explanation.

spion commented 8 years ago

@jasmith79 Its not possible to rebind functions that have a bound this

> function f() { return this.a; }
> var o = { a: 1}
> var f2 = f.bind(o)
> f2()
1
> var o2 = {a: 2}
> var f3 = f2.bind(o2)
> f3()
1
> f2.call(o2)
1
> f2.apply(o2)
1

I looked at getify's explanation, and it doesn't look like a convincing, logical argument to me. Also, no observable difference is being demonstrated there AFAICT

Maybe @dherman can chime in with an explanation of what exactly was meant? If I had to guess, its the fact that there is no prototype and calling the new operator on an arrow function is a syntax error (while doing the same on a bound function isn't) and "lexicalness" comes to play if you consider this to simply be just another function argument (its not a "dynamic" argument, its what was passed at call time "left of the dot" - if you don't pass it, it will be undefined)

gilbert commented 8 years ago

Hi, I have another clarification question. What would it mean to write ::console.log(4) ? Would that be equivalent to console.log.bind(console, 4) ?

zenparsing commented 8 years ago

@mindeavor No, it would be roughly console.log.bind(console)(4).

Syntactically, the call parens can't be a part of the unary bind operand. It gets parsed like this:

( :: (console.log) )(4)