proposal-bind-operator icon indicating copy to clipboard operation
proposal-bind-operator copied to clipboard

Drop the `this` binding altogether

Open dead-claudia opened this issue 8 years ago • 84 comments

Edit: Here's specifically what I'm proposing:

  • Keep the function pipelining: object::func(foo, bar, baz), equivalent to func.call(object, foo, bar, baz)
  • Drop the binary binding operator: object::func, equivalent to func.bind(object)
  • Drop the unary binding operator: ::object.func, equivalent to object.func.bind(object)

Note that this is independent of the syntax or semantics of the function pipelining itself (see #26 for an in-depth discussion on that and the old binding operator, and #40 for a more recent proposal specific to that), and is exclusively focused on dropping the binding syntax.


The resource usage for general this binding leads to a lot of confusion:

// This works
var listener = ::console.log

elem.addEventListener("click", listener, false)
elem.removeEventListener("click", listener, false)

// But this doesn't
elem.addEventListener("click", ::console.log, false)
elem.removeEventListener("click", ::console.log, false)

Those of us familiar with the proposal would immediately know this, but those less familiar would expect the opposite (which would be much more wasteful of memory, because JS doesn't have the lifetime management awareness of Rust and C++).

Also, for the binary version, object::method without an immediate call doesn't really help much at all for expressiveness.

Here's what I propose: drop the this binding altogether, and keep it limited to just immediate calls. I'm not proposing anything about where the object is passed (using this/first argument/etc.), or even the operator used, just not including binding in the proposal.

First, here's the equivalents for each one, using Function.prototype.bind and arrow functions, just for comparison:

// Property binding
::console.log
console.log.bind(console)
value => console.log(value)

// Function binding
object::method
method.bind(object)
value => method.call(object, value)

Function.prototype.bind is already easily optimized for a single argument. In addition, property and method binding is rarely chained (I've never seen it done, ever). How often do you see anything like this? Chances are, probably never, or if you have, it was likely an obvious refactor.

method.bind(console.log.bind(console))
(::console.log)::method // Using the current proposal

Additionally, if you need to bind after chaining, it's fairly easy to just create a new variable, and naming isn't usually hard for simple cases like these.


But the method chaining still has its benefits:

  1. Method chaining will avoid ugly nested calls like this:

    // Now
    var list = _.uniq(_.flatten(_.map(items, v => v.getName())))
    // Better
    var list = items
        ::_.map(v => v.getName())
        ::_.flatten()
        ::_.uniq()
    

    That would make things way easier to read, because the steps are more sequential, and fewer parentheses are involved. Functional languages frequently have something like this for similar reasons, like Elm/LiveScript/OCaml/F#'s x |> f operator. They helpfully avoid parentheses due to their low associativity and left-to-right application, making logic much easier to read.

  2. Wrapper libraries can leverage ES modules and Rollup's tree-shaking feature to not ship more than necessary, yet still retain the convenient pseudo-method syntax. You could create a Lodash clone that only calls the methods you need, so if you only use map, filter, reduce, and forEach, you only bundle those at runtime, even when you install the whole library from npm. Basically, what you don't use, you don't pay for.


So I still want the binary version to allow calls, but let's get rid of the this binding. It's confusing and unintuitive for newcomers, and hardly provides any benefit in practice.

dead-claudia avatar Jan 25 '17 19:01 dead-claudia

Or even return to the earlier proposal for the foreign properties (not a bind at all for the obj::prop)

var prop1 = Symbol();
var prop2 = { get: function() { return this::prop1 + 1 } };
var x = {};
x::prop1 = 123; // assign (same mechanics as WeakMap)
console.log(x::prop2); // 124  - retrieve (like if prop2 is defined as property on x)
// and x is still empty 

postulate that obj::f === f if typeof f === "function" and forget abound bind. the only obj::f(...args) should be threated as f.call(obj, ...args)

just thoughts....

Artazor avatar Jan 25 '17 21:01 Artazor

The main issue as I see it is the chaining. Its the easy sell because you can clearly define a problem that needs solving.

Given something along the lines of:

function* where($this, predicate) {
  for (var item of $this) {
    if (predicate(item))
      yield item;
  }
}
function* select($this, selector) {
  for (var item of $this) {
    yield selector(item);
  }
}
function first($this) {
  for (var item of $this) {
    return item;
  }
  throw Error("no items");
}

if you try to chain these at present you get:

let ugly = first(select(where([1, 2, 3, 4, 5], x => x > 2), x => x.toString()));

which reads in complete reverse of what actually transpires. I think its quite clear that the full power of generators and Iterables are being held back because of this nesting.

A simple solution would be a mechanism that simply passes the left-operand, as the first parameter, to the right-operand function.

let better = [1, 2, 3, 4, 5]::where(x => x > 2)::select(x => x.toString())::first();

Also, he other criticism was that :: looked odd. Is single colon a better (available?) operand?

cc:@zenparsing

MeirionHughes avatar Jan 25 '17 23:01 MeirionHughes

@MeirionHughes I'm only proposing dropping the rest, and just keeping the chaining. I'm intentionally steering away from talk regarding the :: syntax itself (and even its own semantics), because that's still a major point of contention that has gone absolutely nowhere in the past year. Feel free to comment on #42, and read up on the past discussion in #26, where the operator choice was discussed at length.

dead-claudia avatar Jan 27 '17 04:01 dead-claudia

For what little it’s worth, as someone who’s enjoyed using :: via babel for over a year, I have seen that the chaining form is, in addition to being very useful, pretty intuitive to other devs of various backgrounds. The unary form on the other hand has been a source of "huh" more than once. So while I personally saw no problem with the unary form, this (anecdotal) experience has led me to agree that dropping the unary is a good idea.

bathos avatar Jan 27 '17 16:01 bathos

Yet another argument in favor of dropping the unary form is it will be less controversial for the general public to swallow the binary form and it poses less spec dilemmas so it has better chances of landing in a spec quicker. The unary form could still land as a separate addition in a future ECMAScript version but coupling them in one proposal delays both of them.

mgol avatar Jan 31 '17 19:01 mgol

So is this suggestion about dropping the whole proposal in favor of giving :: a meaning of functional pipelining? Is foo::bar going to be equivalent to _.partial(bar, foo) ?

InvictusMB avatar Feb 04 '17 15:02 InvictusMB

@InvictusMB, it means only that

we allow only the following syntax:

PrimaryExpression "::" PrimaryExpression "(" ArgumentList ")"

And treat foo::bar(a,b,c) as bar.call(foo, a, b, c)

Standalone foo::bar is prohibited (not yet specified)

Thus, no bindibg ever involved, and no new closure formed respectively.

Artazor avatar Feb 04 '17 16:02 Artazor

Yes, I'm proposing to drop all binding in general, both instance and argument binding, from the proposal. Those parts are still very contentious, so IMHO they should be considered separately. People are still uncertain whether it should even be an operator, since it looks like it should be persistent when it doesn't.

But with pipelining, it's something people are way more interested in. The only real disagreement I've seen is the operator of choice (to a small extent) and whether it should involve this.

On Sat, Feb 4, 2017, 10:05 InvictusMB [email protected] wrote:

So is this suggestion about dropping the whole proposal in favor of giving :: a meaning of functional pipelining? Is foo::bar going to be equivalent to _.partial(bar, foo) ?

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/tc39/proposal-bind-operator/issues/44#issuecomment-277451600, or mute the thread https://github.com/notifications/unsubscribe-auth/AERrBEG-qkSC2y3qt0TVZLaWe2nb2l9iks5rZJO4gaJpZM4Lt7f3 .

dead-claudia avatar Feb 04 '17 16:02 dead-claudia

And yes, it is kind of pipelining, but it uses a hidden this parameter.

I feel, that TC39 has very strange implicit resistance against any improvements related to any this usage cases.

Artazor avatar Feb 04 '17 16:02 Artazor

I wouldn't quite go that far. They are just aware of the fact this isn't super intuitive to many. They aren't completely anti-this, especially considering their apparent view of the private state proposal. They just appear highly pragmatic.

On Sat, Feb 4, 2017, 11:07 Anatoly Ressin [email protected] wrote:

And yes, it is kind of pipelining, but it uses a hidden this parameter.

I feel, that TC39 has very strange implicit resistance against any improvements related to any this usage cases.

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/tc39/proposal-bind-operator/issues/44#issuecomment-277455495, or mute the thread https://github.com/notifications/unsubscribe-auth/AERrBIdRjKXFJuaNYi6JD1RbdJL9ORUKks5rZKIwgaJpZM4Lt7f3 .

dead-claudia avatar Feb 04 '17 17:02 dead-claudia

@Artazor bar.call(foo, a, b, c) doesn't fit the provided example from first post:

var list = items
    ::_.map(v => v.getName())
    ::_.flatten()
    ::_.uniq()

That example implies that result of LHS expression is partially applied to function in RHS and the resulting function is invoked with arguments in parentheses. Essentially LHS expression is being pipelined as a first argument to RHS instead of being passed as this in case of binding. As per provided example foo::bar(a, b, c) should be interpreted as bar.call(null, foo, a, b, c) Prohibiting standalone usage doesn't make sense as it is easy for an interpreter to omit partial application where possible and simplify to fn.call.

@isiahmeadows I can agree that pipelining as a parameter is more appreciated than this proposal. But I see value in both options. They cover different use cases and programming styles.

Pipelining as a parameter will help people coding in functional style, while pipelining as this with bind operator (as per this version) is more helpful in the realm of OOP style.

In functional style you want to start a chain with a value and transform it through the chain of functions. You don't care about this at all as you probably never use function keyword and prefer => functions. The only context for your functions is lexical scope. You pass around a state as plain JS objects or with closures.

But in OOP style you want to start a chain with an object and invoke a chain of methods. You care about this a lot. It's a core concept to your mental model. You rely less on closures and more on passing the context around in this. Your functions are mostly methods. They are tightly coupled to their instances and not meant to be used without an instance. Sometimes you want to invoke functions as if they where own methods of particular instance. In that particular situation you want to temporary extend your instance with new capabilities but creating a decorator for that is too much of a boilerplate. That is where extension methods are handy.

Occasionally you also want to pass around methods while preserving their binding to the owning instance. This happens when OOP style code meets functional style code.

InvictusMB avatar Feb 04 '17 17:02 InvictusMB

@InvictusMB I'm afraid that in the initial post :: is used like a functional pipelining operator. Actually, it was not a proposed semantics. I'd rather use another operator for that, e.g. ~> Where a~>f(b) is the same as f(a,b). However the initial intention of :: was a::f(b) as f.call(a,b). And exactly that implementation was already tested by Babel and TypeScript team. Everybody who used that form found that it is super-convenient. Especially for Observable operators. @isiahmeadows correct me if I'm wrong -)

Artazor avatar Feb 04 '17 17:02 Artazor

(sorry, just reformated my message - typing on phone is terrible)

Artazor avatar Feb 04 '17 17:02 Artazor

@Artazor I was initially as confused by this thread as you are. Therefore my original question.

Nevertheless, I can envision the need for 3 different operators:

  • Functional pipelining (elm |> operator or sperm operator ~>) for functional style chaining a~>f(b) -> f(a,b) a~>f -> _.partial(f, a) Note _.partial as in lodash instead of f.bind(null, a) to not affect this

  • this pipelining with method resolution (:: operator) for OOP style chaining a::f(b) -> (a.f).call(a,b) a::f -> (a.f).bind(a)

  • this pipelining with foreign method (::@ operator) for method extension a::@f(b) -> f.call(a,b) a::@f -> f.bind(a)

They all have their respective use cases and fit for different programming styles.

  • Functional
const {map, flatten, uniq} = _;
const names = items
  |> map(item => item.value)
  |> flatten()
  |> uniq()
  • OOP
const foo = {
  bar() { return this; }
  bang() { return this; }
};

function bang() {
  return this::foo()::bar();
}

foo
  ::bar()
  ::@baz()
  ::bang()

I don't have a preference for either |> or ~>. But I have a strong opinion on :: operator. It is convenient to use :: for method access and it is widely accepted as a scope resolution operator . For extension methods I would go for ::@ or ::$ or anything else starting with :: to emphasize their semantic connection.

In addition to that, always using :: for method access will clearly state an intention of accessing a method of an instance in contrast to using . for accessing a function and treating object as a namespace. So seeing _.mapValues(foo) will tell you that _ is an utility library and mapValues is not using this. While seeing console::log(foo) will tell you that log relies internally on this being compatible with console. Also arrayLike::@Array.prototype.sort() will tell you that it extracts a sort method from Array.prototype and invokes it as arrayLike's own method and that sort relies on the fact that during execution its this has to be array-like.

InvictusMB avatar Feb 04 '17 19:02 InvictusMB

I don't want to put words in ts39's mouth but if they found :: to (quote) look weird (unquote) then hopefully they'll say the same for |> because I'm not a fan. Primarily because it requires two hands to type |>, while :: or ~> can be done solely by the right hand.

MeirionHughes avatar Feb 04 '17 22:02 MeirionHughes

@MeirionHughes You’ve got me curious: how is it that ~> may be typed with one hand yet |> cannot? I imagine we must have different kinds of keyboards or different typing styles, because for me, ~> implies two hands, :: implies one, and |> could go either way. (This factor doesn’t matter to me personally one way or the other; they’re all equally fine to me, but it made me wonder.)

bathos avatar Feb 05 '17 00:02 bathos

It is convenient to use :: for method access

I'm not a fan of :: to access methods, as there are other ways to access a method. How will this operator work in these cases:

const foo = [ a => a + 1 ];
// How to do foo::0(42) or foo::[0](42) ?

const symbol = Symbol();
const bar = {
  [symbol](a) { return a + 1; },
  symbol() { throw new Error(); },
  baz(a) { return a + 2; },
};
// How to do bar::symbol(42) or bar::[symbol](42) ?

// How about bar::.baz(42) ?

If :: does not care about accessing methods, at least we can probably already do:

foo
  ::foo.bar()
  ::baz()
  ::foo.bang()

// Or maybe
foo
  .bar()
  ::baz()
  .bang()

And to me that's a great improvement by itself.

Volune avatar Feb 05 '17 08:02 Volune

@MeirionHughes @bathos It's indeed keyboard layout specific. But unless you are producing write-only code and getting paid for the number of characters this should be the least of concerns. The major attention should go to readability, semantics and cognitive load which all contribute to maintainability of a code.

@Volune Why would you pull [] operator into this discussion? Introducing :: neither prevents you nor changes the way you use . or [] operators. :: should be responsible only for reliable pipelining of context through this. That's the only difference from . and you don't use . with symbols either. If you want to use symbols or strings for property access, you would still need [] operator. But if you really want context pipelining with brackets notation then you are talking of another operator ::[]. Which may be a valid suggestion on its own but doesn't help this discussion to conclude.

InvictusMB avatar Feb 05 '17 08:02 InvictusMB

To clarify, my main suggestion here is to break apart the two proposals, so they can be considered more independently.

  • Function pipelining: great for virtual methods, etc., and makes functional logic more linear.

  • Partial application/binding: great for event listeners, concise functional callbacks, etc., while being able to ignore the arguments a function takes at the call site.

These are two very independent concepts, and not everyone agrees with both. So I feel it's best to separate the two proposals formally.

On Sun, Feb 5, 2017, 03:20 Jeremy Judeaux [email protected] wrote:

It is convenient to use :: for method access

I'm not a fan of :: to access methods, as there are other ways to access a method. How will this operator work in these cases:

const foo = [ a => a + 1 ];// How to do foo::0(42) or foo::0 ? const symbol = Symbol();const bar = { symbol { return a + 1; }, symbol() { throw new Error(); }, baz(a) { return a + 2; }, };// How to do bar::symbol(42) or bar::symbol ? // How about bar::.baz(42) ?

If :: does not care about accessing methods, at least we can probably already do:

foo ::foo.bar() ::baz() ::foo.bang() // Or maybe foo .bar() ::baz() .bang()

And to me that's a great improvement by itself.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/tc39/proposal-bind-operator/issues/44#issuecomment-277504426, or mute the thread https://github.com/notifications/unsubscribe-auth/AERrBFL-0L5m3xRTFtzuyOaZIb-McDABks5rZYZogaJpZM4Lt7f3 .

dead-claudia avatar Feb 05 '17 09:02 dead-claudia

@MeirionHughes

I don't want to put words in ts39's mouth but if they found :: to (quote) look weird (unquote)

If I remember correctly that was related to the unary prefix form of ::. And in that context I agree with them. ::console.log is weird and crappy in many ways.

InvictusMB avatar Feb 05 '17 09:02 InvictusMB

@InvictusMB I'm not really pulling the [] operator. Having the "this pipelining operator" doing method resolution is already part of the discussion. I'm just pointing out that, with the current description, it will not cover all the possible ways of resolving a method on an object (i.e. not methods accessed with a symbol or number)

If :: (or whatever syntax, ☺> if you want) "this pipelining operator" never does method resolution, then it's simple.

  • A::B() gives B.call(A), never A.B::A(); A::B gives B.bind(A), never A.B.bind(A)
  • Need method resolution, do it the way you want: A::A.B(); A::A[symbol](), A::A['method'](), ...

And I doubt it prevents a future introduction of an operator that does method resolution.

Volune avatar Feb 05 '17 10:02 Volune

@Volune :: doesn't have to support all possible ways of method resolution. The use case you are describing still involves the name resolution it's just different. You are talking of lexical scope name resolution. That is what I called ::@ operator here. It doesn't make things simple it just presents another use case. There is a use case for pipelining with lexical scope name resolution and there is a use case for pipelining with instance scope resolution. I would use :: for instance scope resolution and ::@ for lexical scope resolution. But that's a matter of naming. It doesn't change the fact that both versions have value.

InvictusMB avatar Feb 05 '17 10:02 InvictusMB

Is method resolution really being considered? It seems pretty much orthogonal to the point of this proposal to me and I don’t get how it entered the picture. Aside from seeming like a weird feature to graft on here, I believe the only precedent for manipulating scope resolution in JS is the deprecated with statement — would this not suffer from the same problems?

bathos avatar Feb 06 '17 16:02 bathos

@InvictusMB I suggest use :: for lexical scope resolution in the discussion because most people familiar with this semantic thank to Babel.

Another reason is instance scope resolution seems not have very big value compare to lexical scope resolution (just my feeling). And I believe (and agree) it's why this issue suggest to drop them at current stage.

hax avatar Feb 07 '17 10:02 hax

@bathos According to the proposal, object::func(...args) is equivalent to func.call(object, ...args). I'm proposing dropping the binding half of the proposal, though.

I've also updated my original proposal to clarify.

dead-claudia avatar Feb 08 '17 02:02 dead-claudia

@isiahmeadows That was my understanding, and I think it’s a wise plan.

What I was commenting on is that recently, both in this thread and in other threads on this board, there’s been discussion of using :: for runtime scope resolution that may be property access or regular binding resolution, a concept which seems pretty unrelated to this proposal to me(?). While I’m not worried that TC39 would approve that, I was worried by the statement that "doing method resolution is already part of the discussion", since it’d be a pity to see the original idea fail on account of adding such.

Do we know why this is coming up repeatedly? Is it that the :: symbol too heavy with baggage from other languages that use it with a different meaning?

bathos avatar Feb 08 '17 04:02 bathos

@bathos

Do we know why this is coming up repeatedly? Is it that the :: symbol too heavy with baggage from other languages that use it with a different meaning?

I think it's mostly the reliance on this that's stirring people up, but yes, the symbol is also part of it. I've specifically been discouraging discussing that in this bug to avoid it derailing (as several already have). I'd love to share my theories on why that could be happening, but only in a different issue.

dead-claudia avatar Feb 08 '17 05:02 dead-claudia

As I hinted with #45, I think the binding operator would be more useful (and less confusing to C++ devs) if it were changed to mean lhs.rhs.bind(lhs). Then it would have an actual use case: delegates. One could pass this::method as a callback from within a class. Chaining/pipelining IMO should be a different symbol.

fatcerberus avatar Feb 08 '17 17:02 fatcerberus

Then it would have an actual use case

I'm sorry. Have you met functional programming before? Not to mention that even with this not being accepted in forever, there is already libraries out there written to use the pipelining.

Alxandr avatar Feb 08 '17 18:02 Alxandr

i was specifically defending the binding semantics, which from my reading of the issues in this repo was being proposed to be removed due to not having many real-world use cases.

Pipelining is a great thing to have I agree, but that wasn't what I was referring to.

fatcerberus avatar Feb 08 '17 18:02 fatcerberus