tc39 / proposal-bind-operator

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

Drop the `this` binding altogether #44

Open dead-claudia opened 7 years ago

dead-claudia commented 7 years ago

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

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 commented 7 years ago

@lukescott I strongly disagree with both the idea and syntax, especially when considering the current method call syntax of object.method():

In addition, all languages that have similar function call and value syntaxes (s-expression-based languages and most primarily functional languages) conflate function and method application while still keeping the context either adjoined or to the right, leaving no ambiguity:

Let's not confuse people with an ugly syntax.

lukescott commented 7 years ago

@isiahmeadows

How do you address that with object::foo()::bar()it looks like foo and bar and members of object? Other languages that use :: it would be.

Also, how do you prevent object::foo::bar? Do you throw an error? If it's an error, shouldn't it be more obvious? For example, object::foo( is an error, but object::foo doesn't look like one.

The syntax should be intuitive enough to solve those problems.

dead-claudia commented 7 years ago

@lukescott

How do you address that with object::foo()::bar() it looks like foo and bar and members of object? Other languages that use :: it would be.

By explaining object::foo() as a syntactic form rather than object::foo as binding this to object in foo, you avoid the issue altogether. If we eventually accept @robotlolita's suggestion, it'll emphasize the intent as a function call-only form even more.

Here's some existing precedent for that:

So yes, it's possible to avoid confusion by merely explaining it as syntactic sugar, which is what Lua's community already does for object:foo().

robotlolita commented 7 years ago

@isiahmeadows sorry, I was just replying to @lukescott's comment. I should've been clearer.

(There's a separate proposal for that, though I'm unsure if championed by anyone on TC39, https://github.com/mindeavor/es-explicit-this)

dead-claudia commented 7 years ago

@robotlolita No problem

zenparsing commented 6 years ago

@isiahmeadows Just saw this post (I was taking a break for a while).

I think you're right.

I'll have to go through the old issues to remind myself of why memoization was rejected.

ljharb commented 6 years ago

The prefix form is one of the primary draws; as part of discussions on the pipeline operator, the committee agreed that even if only one of pipeline and bind landed, we’d still need a method extraction operator.

In other words, if the prefix form is dropped, I’m not sure what this operator adds over the pipeline operator.

dead-claudia commented 6 years ago

I'm with @ljharb: given they're now pretty separate, we could go and drop the infix chaining variant instead.

Although...then there's the upcoming partial application strawman floating about (there's a link in the proposals repo, but I don't remember it off-hand), which could make the bind proposal even less relevant.

My thought now, though: if we drop the current infix variant, we could make the binding infix instead. So where currently, ::this.foo binds this.foo to this, we could instead use this::foo. Note that the current infix binding method is pretty useless, since this::foo and foo.bind(this) take roughly the same number of characters. Just an alternative idea in light of the pipeline operator proposal's success.

fatcerberus commented 6 years ago

So where currently, ::this.foo binds this.foo to this, we could instead use this::foo

Yes, this is what I was getting at above - method-binding. It seems pretty intuitive that way given the semantics of :: in other languages. ::this.foo is just really weird to look at.

dead-claudia commented 6 years ago

@fatcerberus I'm aware, it just wasn't at the time initially proposed very tractible*, and would've required redesigning the syntax (which was obviously not something anyone was eager to do). And I agree, it's pretty weird and odd to look at.

* It was probably about a year ago when the this::foo for this.foo.bind(this) suggestion was first presented. It's not an original idea of mine.

fatcerberus commented 6 years ago

I think what confuses me most about the prefix syntax is that ::foo.bar looks like a unary operation on foo.bar but in reality is a binary operation over foo and bar. How does that even work out for precedence? I guess you would parse it as a ternary operator with no first operand, which is strange to say the least.

zenparsing commented 6 years ago

If we can clear up some of the issues with the pipeline operator (multiple arguments and integration with method calls) I would completely be in favor of changing x::y to do method extraction.

acutmore commented 6 years ago

I think what confuses me most about the prefix syntax is that ::foo.bar looks like a unary operation on foo.bar but in reality is a binary operation over foo and bar. How does that even work out for precedence? I guess you would parse it as a ternary operator with no first operand, which is strange to say the least. @fatcerberus

A similar thing could be said about the delete keyword. delete obj.foo operates on obj using 'foo' as an argument. This is achieved because obj.foo returns a reference that has two internal slots: the base (obj) and the referenced name (foo).

That way ::foo.bar is a unary operation on the reference returned by foo.bar.

fatcerberus commented 6 years ago

@acutmore Yeah, I always found delete’s semantics odd for that exact reason. Looking at a delete expression I don’t intuitively expect it to work and always second-guess it.

leemotive commented 6 years ago

in my opinion

function pipelining should be written as obj::func(bar)(foo) and equivalent to func.bind(obj, bar)(foo) also equivalent to func.call(obj, bar, foo)

make obj::func(bar, foo) equivalent to func.call(obj, foo), apply will fill unhappy

hax commented 6 years ago

@leemotive So it's just syntax sugar, what's the benefit of using it instead of func.bind(...)?

Anyway, this proposal seems already dead? What's the future plan, @zenparsing ?

ljharb commented 6 years ago

@hax syntax is robust against delete Function.prototype.bind and similar, which is very critical.

hax commented 6 years ago

@ljharb Most things in JS can be delete , eg. delete Object.is. Will committee consider introduce a is b operator?

ljharb commented 6 years ago

@hax i would love to see that happen, yes. However, the slippery slope argument doesn't invalidate this specific syntactic improvement.

hax commented 6 years ago

the slippery slope argument

@ljharb Sorry, it's not my intention. I gave this example because past TC39 decisions make me a impression that TC39 would very unlikely to introduce any pure syntax sugar which do not have other significant benefit. And I remember Object.is vs a is b operator has been discussed before, so I have a impression that immune from delete Object.is was not a significant benefit.

I'm wondering if the taste of TC39 has changed now?

acutmore commented 6 years ago

@hax one of the interesting properties of syntactic sugar in a dynamic language is that it can open up compiler improvements. So not only can the code be clearer to the reader but also it gives the compiler more information to work with.

For example adding await to javascript could be seen as just sugar over using .then but it allowed v8 to produce much clearer call stacks: https://mathiasbynens.be/notes/async-stack-traces

I would imagine this bind syntax might also open the door for some optimisations

hax commented 6 years ago

@acutmore await is not a pure sugar as my understanding. For example, it's very troublesome to convert await to .then if there is flow control like throw/return/break/continue...

hax commented 6 years ago

If we plan to repurpose a::b to method extraction as https://github.com/tc39/proposal-bind-operator/issues/44#issuecomment-359215966 , there is also another problem: we need to support both member access (a.b) and computed member access (a[b]).

In the original proposal, we use ::a.b and ::a[b] which is consistent, but if change to a::b and a::[b], we will face the same problem like currently controversial syntax issue of optional chaining operators.

For the consistency, we'd better drop :: syntax. I think we could use a.☐b and a.[☐b] which ☐ is a possible char. For example, use &, it will be a.&b a[&b]. (Groovy use a.&b for similar purpose.)

zenparsing commented 6 years ago

@hax

In the original proposal, we use ::a.b and ::a[b] which is consistent, but if change to a::b and a::[b], we will face the same problem like currently controversial syntax issue of optional chaining operators.

Great observation! And I agree completely. I would still like to see if we can provide some kind of syntactic support for method extraction, but it will likely need to be a unary operator. We probably don't want to have a sigil inside of the brackets, though.

Maybe just a prefix &, somehow?

let a = &obj.x;
let b = &obj['x'];
let c = &obj->x; // ;)
fatcerberus commented 6 years ago

I could get behind unary &. Double-colon :: as a unary operator puts me off big time since I don't think there's any other mainstream language where the operator works that way. By contrast & is the addressof operator in at least C/C++ so I think &obj.x would be pretty nice.

hax commented 6 years ago

@zenparsing Prefix & will introduce new ASI problem 😢 Though it seems we will unlikely meet it in current code.

a  // <- missing ;
&obj.x // useless now...

But maybe future:

a  // <- missing ;
&obj.x |> accept_a_func
zenparsing commented 6 years ago

@hax Yes, I thought about the ASI issue, but (as with prefix + and -) it may be acceptable depending on usage patterns. Semicommon-light linters would have to be upgraded to warn on & on a newline without a preceding ;.

hax commented 6 years ago

@zenparsing Yes, it just add another new token to -+/([ list 😅

Another reason I prefer a.&b to &a.b is: when people program, it seems many would realize they need extract the method after they entered a. so it's a little pain to backwards the cursor.

This is my subjective feeling, but groovy's a.&b design may reflect my guess 🧐

I even consider postfix operator may be better though there is not much choice of token...

lukescott commented 6 years ago

Since the pipeline operator/proposal handles the chaining use-case, what's wrong with obj->x? It closely resembles the fat arrow => syntax, so it could easily be explained that "arrows mean bind".

hax commented 6 years ago

@lukescott obj->x seems good, but it also face the inconsistent problem of obj->[x], see https://github.com/tc39/proposal-bind-operator/issues/44#issuecomment-372593028

cshaa commented 6 years ago

To me, the most important use case of the function pipelining a::b() are extension methods. In languages like C#, you can define extension methods that look as if they were part of the object itself. This would pair nicely with ES6 modules:

import { distinct, join, sort } from 'linq';

let a = [ 8, 42, 5, 9, 2, 5 ];

a::distinct()
 ::sort( x => -x )
 ::join(", ")
 |> console.log;

The reason why I prefer the :: syntax over the -> operator is that it is similar to the . operator used to access properties – a::sort() is almost like a.sort(). However I agree that the proposal is getting very close to the pipeline proposal and the syntax should probably reflect the similarity.

Therefore I propose changing the operator for “this pipelining” from :: to .>, as it's similar to both the pipeline operator |> and the member access operator .. Then we should also merge it with the proposal-pipeline-operator and leave this repository for the unary operator only.

cshaa commented 6 years ago

On tc39/proposal-pipeline-operator#110 I've done a summary of the similarities and differences between the Bind Operator and the Pipeline Proposal and I found out that the part promoted by @isiahmeadows is really the only part we need. The rest can be quite easily done with the Smart Pipelines. Be sure to check the issue out, as this is IMHO the most probable way of getting the “function pipelining”, as Isiah calls it, to the specs!

hax commented 6 years ago

IMO, pipeline is different and can't replace bind operator. Check my comment at https://github.com/tc39/proposal-pipeline-operator/issues/101#issuecomment-374513960 .

michaeljota commented 5 years ago

This proposal maybe refocus to cover #5 mainly, as I think that use case can't be covered by the pipeline operator.

TL; DR: Use bind operator on destructive functions assignment.

function foo({ ::doA, ::doC }) {
  doA();
  doC();
}

const bar = {
  a: 1,
  c: 2,
  doA() {
    console.log(this.a);
  },
  doC {
    console.log(this.c);
  }
}

foo(bar); 
// Output: (With bind operator)
// 1
// 2

// Output: (Without bind operator)
// undefined
// undefined

I think this would fit nicely between functional programming, and the oop spirit of Javascript.

This currently could be done with something like this

function foo(bar) {
  const { doA, doC } = bar;
  doA.bind(bar);
  doC.bind(bar);
  doA();
  doC();
}

But it's not practical, as I would need n + 1 lines for every function I would like to do this. This could even be used with renamed assignment

function foo({ ::doA: baz }) {
  baz
}

In #5 was a discussion about where the binding should belong, and I think the binding should belong to the destructive property (doA), as baz is just a variable assignment.

Don't know what you think about this, or if you just want to let this die completely.