Closed ssube closed 8 years ago
@ssube
You beat me to it...I was about to file this myself. :wink:
:-1: for the tilde. LiveScript makes frequent use of the tilde for binding this
, but it's a little awkward to reach, just below the Esc key on most keyboards and requiring Shift, and gets annoying quick. It's okay for the bitwise negation operator, which doesn't see much use, anyways, but the pipeline operator would be used too often.
I would prefer the hyphen arrow, obj->func()
.
:+1: :+1: for the idea of making it use the first argument instead of the this
context. That would make it immediately usable for Underscore, Lodash, and friends, as well as not requiring a dynamic this
that would usually be unnamed. Also, when compiled, it doesn't require a lot of boilerplate.
import {map} from 'underscore'
// Would "just work" now, with transpiler support.
getBooks()->map(obj => obj.author)
The above would be this in ES6:
import {map} from 'underscore'
map(getBooks(), obj => obj.author)
It would also make it easier to fit into TypeScript, which is still trying to figure out how to fix the this
problem (JavaScript is call-by-delegation, but TypeScript doesn't delegate types).
Class::[new]
as you suggested for binding the constructor (i.e. [[Construct]]). It's easier to parse, and gets rid of a weird edge case. new
isn't that obscure of a property or method name. It's not like __proto__
, which is otherwise a terrible property name to begin with. Also, it's not like two characters would kill anyone. I remember a similar problem had to be solved with arrow functions, to solve the ambiguity problem with objects vs blocks, in which the decision was to require the returned object to be wrapped in parentheses. People complained initially, but they got over it.::__proto__
is equivalent to obj => obj.__proto__
, while ::['proto']
is equivalent to obj => obj['proto']
. I know that's a little nit, but it's something to be mindful of.One other thing: ::[new]
should be equivalent to C => C::[new]
, if it exists (which it should for consistency's sake). The reason why I believe this is because C::[new]
is effectively C.[[Construct]]
. It's also least surprising, as new.target
is already bound to C.
C => C::[new]
C => C.[[Construct]].bind(C)
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)
Another interesting thing with this syntax in this gist.
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?
@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)();
@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.
@ssube Also, I'm not feeling the unary ::
.
@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 +>
.
@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.
@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.
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.
@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?
@impinball Yep
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 .
@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.
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 .
@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.
@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.
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.
It sounds like we've moved from the original post (which I'll leave for posteriority) to something like:
::
) passes it as this
->
) passes it as the first argument...args
and invoking the operandThat 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.
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.
@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
andapply
, 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.
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
host->func(...args)
to
func(host, ...args)
host->ns.func()
means `host->(ns.func)()Method binding:
this::[method]
, like this[method]
),
or explicitly [new]
to bind the [[Construct]] function, like in
Class::[new]
.this::method
is equivalent to %FunctionBind(this.method, this)
obj.prop::foo
is like
%FunctionBind(obj.prop.foo, obj.prop)
, and obj::prop.call
is like
%FunctionBind(obj.prop, obj).call
.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 .
@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.
@zenparsing It's more about the use of this
. Whether we should bind the argument to this
or not. Which would you prefer?
Option 1
function concat(xss) {
return [].concat(...xss)
}
function map(xs, f) {
return xs.map(f)
}
function reduce(xs, f, start) {
return xs.reduce(start)
}
function tap(xs, f) {
xs.forEach(f)
return xs
}
const foo = {
index: 0,
inc() { this.index++ },
}
list
->map(x => [x * 3, x / 3])
->tap(foo::inc)
->concat()
->reduce((a, b) => a + b, 0)
Option 2
function concat() {
return [].concat(...this)
}
function map(f) {
return this.map(f)
}
function reduce(f, start) {
return this.reduce(start)
}
function tap(f) {
this.forEach(f)
return this
}
const foo = {
index: 0,
inc() { this.index++ },
}
list
::map(x => [x * 3, x / 3])
::tap(::foo.inc)
::concat()
::reduce((a, b) => a + b, 0)
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.
@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)
}))
}
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.
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.
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:
->
. For example: ::this.foo
→ this->foo
. I know that would confuse a few C++ developers, but I would prefer that. It's a little more obvious that it's a binding operator. I would also be okay with a tilde (this~foo
, drawing from LiveScript) or something else.I just want binding to have something infix with the same precedence as a dot, and clearly different from whatever's used for chaining.
@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.
@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:
@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!
@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.
@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 .
The reason why I prefer option 2:
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.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
@spion I agree completely and prefer option 2 as well.
@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).
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.
@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.
@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.
@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.
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
@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.
@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 hiddenthis
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.
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?
@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]
@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.
@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
)
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)
?
@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)
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:
Using something like the rules:
::
) passes it asthis
~>
) passes it as the first argument::
) returns a function...args
and instantiating an instance of the left operand...args
and invoking the operandThat gives us four distinct (useful) expressions and two with very limited usage:
scope :: property
andscope :: [property]
scope :: new
:: property
and:: [property]
:: new
(provided for completeness, but not very useful)target ~> property
(and potentiallytarget ~> [function]
)target ~> new
(provided for completeness, but not very useful)We can very naively implement them as:
With scope and property:
With scope and new:
Without scope, with property:
Without scope, with new:
For the pipeline operator, the inner invocation of
scope[property].apply(scope, args)
would be replaced withscope[property].apply(this, [scope].concat(args))
Breaking down the examples from the start:
and
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.