tc39 / proposal-bind-operator

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

Wouldn't partials be better than virtual methods? #19

Closed aaronshaf closed 8 years ago

aaronshaf commented 9 years ago

I originally posted this to the wrong repo:

https://github.com/gsklee/bound-native-methods/issues/3

Compare:

// ES7

getPlayers()::map(x => x.character())

// ES6

let _val
_val = getPlayers()
_val = map.call(_val, x => x.character())

With:

// ES7

getPlayers()::map(x => x.character())

// ES6

let _val
_val = getPlayers()
_val = map(_val, x => x.character())

I may not have phrased my proposal correctly, but consider that with lodash I could:

import {flatten, zip, takeWhile} from 'lodash/arrays'

let stuff = [...]
stuff::zip()::flatten()::takeWhile({'active': false})

Bound virtual methods assumes we want to be using context, this, etc. My proposal is perhaps more "functional"?

Perhaps there is more convenience in chaining utility functions than reusing prototype methods?

@zenparsing responded already here https://github.com/gsklee/bound-native-methods/issues/3#issuecomment-109432573, but I'd like to move the conversation here. He wrote:

@aaronshaf some people feel that using "this" as a general-purpose parameter is...icky. So I definitely understand where you're coming from.

As your lodash example indicates, the downside of binding to "this" is that such "virtual methods" can't really be used ergonomically without the :: syntax. And the :: syntax can't be used with non-method functions.

On the other hand, in my opinion the syntactic gap we are filling here is not simply putting the first parameter on the left of the function identifier. (That would be controversial anyway, since many "functional" programmers are just fine with right-to-left composition.) Instead, it is providing a the user with a convenient way to express the notion of "extension methods". And for extension methods it is natural to use "this".

In practice, I've found that using "this" feels completely natural in this context. That could be a result of personal bias, though : )

Regarding lodash specifically, I imaging that it would be pretty easy to rewrite lodash as a library of extension methods.

ssube commented 9 years ago

In the second example (map(_val, x => x.character())), what would this be set to?

I think there are three main benefits to binding over partials:

  1. Consist operator for binding-related reference (both extension with foo::bar and bound-reference with ::foo.bar)
  2. Allows compatibility with older code that already expects this, which seems like a significant portion of event handlers and the like
  3. Developer familiarity with similar syntax to C#'s extension methods (the equivalent of this) and Java's method reference (equivalent of bind), both of which use ::
bmeck commented 9 years ago

If we think of :: as an unary operator that places binding, ::(...) would only be binding the arguments as it does not have a holder to refer to as the this value. It would not bind the holder and thus not a this value. This even gives a slight advantage of doing something that Function.prototype.bind does not, partially completing methods of classes.

zenparsing commented 9 years ago

@bmeck As currently specified, ::(expr) actually does bind the this value. expr must be a "Reference" (think member lookup expression), and the parens are unwrapped before applying the operation to the reference.

This is actually how other Reference-taking prefix operators, like delete work:

let obj = { x: 100 };
delete ((((obj.x))));
obj.x; // undefined

And :::

let obj = { x: 100, method() { return this.x } };
let m = ::((((obj.method))));
m(); // 100
bmeck commented 9 years ago

@zenparsing

your operand to :: and delete is obj.prop.

my thought was when the operand is a [[FormalParameters]]:

let obj = { f() {} };
delete obj.f(); // doesn't make sense when we are dealing w/ fn invocation / cannot specify the operand is the [[FormalParameters]]
obj.f; // f() {}

or

global.x = 'global';
let obj = {method(prefix) {return prefix + this.x}, x: 'property'};
m = obj.method::('is a');
m(); // global
lukescott commented 9 years ago

For the currently defined object::function syntax, I somewhat agree. So instead of:

function map(callback) {
    return this.map(callback);
}

You mean:

function map(array, callback) {
    return array.map(callback);
}

From a documentation perspective that does make a lot of sense. It also allows for the functions to be used two different ways:

var a = [...];
map(a, function() {...});
a::map(function() {...});
bmeck commented 9 years ago

@zenparsing did my comment make sense to you?

benjamingr commented 9 years ago

For what it's worth, the "first parameter" notation is what C# does with extension methods, and the this proposal is what Java does with default interface implementations.With swift, the this approach (self) is taken.

Overall I think this shows that both approaches are viable. I think dynamic this in JS is more like the current proposal, the current proposal makes sense, and both ways can be called anyway. Just like people do [].map.call(... today, they'll be able to use ::.

zenparsing commented 9 years ago

@benjamingr @aaronshaf If I just look at this issue in isolation, then it seems to me that the "first param" approach is slightly better: slightly easier to grok, since it doesn't rely on dynamic this, and slightly easier to use, since you can use the resulting function both with and without the ::.

However, it then wants a different operator. The best that I can think of would be thin-arrow:

iterable
  -> map(...)
  -> takeWhile(...)
  -> forEach(...);

And that would leave :: for method extraction, similar to Java:

Promise.resolve(123).then(console::log); // instead of ::console.log

Not bad, but I'm not sure we can justify taking thin-arrow for a pipe operator. What do others think?

benjamingr commented 9 years ago

Honestly I think we should just move forward with ::.

Notation in both cases is pretty good, this proposal is really a substential improvement of JS for a lot of people. Multiple people have used it (and abstract refs before it) in babel and really like the concept.

I think taking -> as well has huge bikeshed potential and ::console.log is something people can get used to very fast.

aaronshaf commented 9 years ago

FWIW, I like @zenparsing's proposal. :-\

zenparsing commented 9 years ago

@benjamingr Unfortunately, some TC39 participants are strongly opposed to using this in such a loose way.

The only weird thing about using :: as infix method extraction is computed property names:

console::log; // This is fine

console::['log']; // Is this right?  Looks a little odd... Do we even allow computed names here?
benjamingr commented 9 years ago

@benjamingr Unfortunately, some TC39 participants are strongly opposed to using this in such a loose way.

What do you mean by loose?

The only weird thing about using :: as infix method extraction is computed property names:

Computed property names sound good, but it can be added at a later point.

zenparsing commented 9 years ago

@benjamingr Loose, meaning using this as an arbitrary "zeroth" parameter. Some feel that "this" should only be used in a more traditional OO way, and loose this leads to confusing code. I'm sympathetic to that viewpoint.

e.g. https://esdiscuss.org/topic/named-this-and-this-destructuring

benjamingr commented 9 years ago

It's definitely not "arbitrary", this is about extending objects from the outside a-la C# extension methods or Swift extensions. I do not hold the opinion that :: violates oop, on the contrary - it brings JavaScript the ability to extend objects in a scoped way and puts it on par with other OOP languages which already share that ability :)

It uses this for context.

bergus commented 8 years ago

I completely agree with @benjamingr on that matter. Move forward with ::… as method extraction and …::… as virtual method calls. It's what we need, it's what people are interested in primarily, it's what this proposal was about from the begin. (Although I'm not opposed to make method extraction an infix operator [not :: then?], as it wouldn't change the spirit of the operator).

A syntax for first-parameter-passing (->) is too much bikeshedding, and honestly we don't need it. Chaining works well in an OOP style with method invocations (data as zeroth argument) and in a functional style with data as the last argument (forEach(…, takeWhile(…, map(…, iterable))) with your example) which goes well with currying and composition (if you're into that stuff). Data as the first argument is just awkward (think nested underscore calls), and we don't need extra syntax to encourage the creation of such functions (that don't use this anyway).

zenparsing commented 8 years ago

Sticking with the current proposal.