tc39 / proposal-bind-operator

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

::object.method vs object::function #18

Closed lukescott closed 8 years ago

lukescott commented 9 years ago

The two different uses of :: is a bit confusing. For example someone could write:

::object; // invalid syntax, but "looks correct" given the precedent that :: can precede
::object.method; // valid syntax - the "." is needed.
object::method // method is not a property of object, but the above syntax works with properties

The difference between ::object.method and object::function seems a little too "magical".

Instead of ::object.method how about object->method instead. It would more naturally complement the fat arrow syntax. Then :: is only used for object::function.

BerkeleyTrue commented 9 years ago

object -> method is confusing, looks too similar to args => returnValue with vastly different meanings. As long as it becomes well known that this de-sugars to the bind method it should be ok.

I was thinking of forcing this::object.method and removing preceding :: from the spec might make it more grokable.

Meaning the bind context is always on the left and the binded method is always on the right.

This would still work

getPlayers()
  ::map(x => x.character())
  ::takeWhile(x => x.strength > 100)
  ::forEach(x => console.log(x));

but then this looks weird

class Comp extends React.Component {
  methodA() { /* code requires correct context */
  function render() {
    return <div onClick={ this::this.methodA }>
  }
}

edit: clarify second example

Naddiseo commented 9 years ago

object -> method is confusing, looks too similar to args => returnValue with vastly different meanings.

It may be less confusing if you take out the whitespace like you did using the :: operator: object->method. The -> operator, like the . and :: operators are slightly different that regular binary operators. For regular binary operators, such as addition, the lhs and rhs can exist in isolation, which I think is why a lot of style guides recommend that you write whitespace around them. On the other hand, with the attribute/method operator . the rhs cannot exist in isolation since it's generally scoped to the lhs of the operator. Coming from a Perl background which has both => and ->, albeit with different meanings, have never had a problem distinguishing between the two. Let me back up this anecdotal evidence with some linguistic reasoning: humans are capable of distinguishing very small details in language, and the differences we may not perceive in our native language(s) we can learn to distinguish, for example, nasal vowels aren't distinguished in English, so a native English speaker wouldn't "hear" them if listening, for example, to a French speaker where nasal vowels have a phonemic distinction from oral vowels. I think the same applies to programming languages. Continuing the example, my "native" language is Perl, where I've learnt the distinction between the two operators, and so seeing the two operators in ES doesn't really phase me.

I was thinking of forcing this::object.method and removing preceding :: from the spec might make it more grokable. Meaning the bind context is always on the left and the binded method is always on the right.

I agree with this. Looking only at examples from this spec, there's no easy rule to say what the calling context is. When I first saw examples I was trying to figure out: Does it bind to the left, or to the right? Continuing my linguistic themed arguments, regularity in language makes it easier to learn, to read, and to understand, and I'm sure most people who try to learn a natural language will agree: the exceptions and irregularities are difficult to internalize. So, when I heard there was a proposal for a bind operator, I thought: sweet, no more obj.method.bind(obj), I can use obj::method because the lhs is always bound to the rhs? Then I try to use the operator, and it doesn't do what I was expecting, and not only does it not do what I expect, it behaves in different ways depending on if it's unary or binary, or if it's being called. Basically, it doesn't behave like a syntactic shortcut for the .bind method, which is what I was expecting (given its name), and something I think the language needs.

BerkeleyTrue commented 9 years ago

I see what you are saying. I may change my mind with object->method. I guess I would need play with it.

I like your point about native languages. My native language is ES so I always push for things that make sense from the point of view of a native ES coder.

Are there use cases for preceding :: that cannot be covered by lhs binding to rhs?

for instance: this

Promise.resolve(123).then(console::log);

Is clearer than

Promise.resolve(123).then(::console.log);
lukescott commented 9 years ago

object::method does make more sense than object::function, although changing that would kill object::function. The need for object::method is probably greater than object::function though.

I do prefer object->method over object::method, for the following reasons:

:: and -> is already well established. Using :: in the same way as the other languages with a completely different meaning will confuse a lot of people. -> however is a lot closer to the intent of method binding.

If object->method were memorized #17, then it would be extremely useful, especially when passing class methods to handle events.

BerkeleyTrue commented 9 years ago

I don't like killing object::function as this would kill the getPlayers example above.

I was considering bindingContext::function and bindingContext::object.method

Naddiseo commented 9 years ago

I was considering bindingContext::function and bindingContext::object.method

If bindingContext is the same as object you get object::object.method which is only slightly shorter than object.method.bind(object), but still weird looking.

I agree with @lukescott, object->method feels most natural to me, and allows object to be buried in other objects thing.otherthing.object->method which the current proposal doesn't make easy: In ::thing.otherthing.object.method it's not obvious from the syntax what the :: operator is doing since it's quite far from the area to which it is being applied.

As for the virtual methods/context::function: personally, I've never written code using libraries like that, but I can see how it would be useful in making the code look more uniform with chaining methods.

lukescott commented 9 years ago

@BerkeleyTrue, ok, your console::log example above comment made it seem like you were thinking about killing it. The object::function syntax can be useful in sharing functionality between classes. I have my own implementation of traits, and after hitting a wall with that, I'm now thinking that this would be helpful:

import {doSomethingThisWay} from "./helpers";
class Bar extends Foo {
  doSomething() {
      this::doSomethingThisWay();
  }
}

So perhaps object::function and object->method can both exist as different syntax.

I really don't feel comfortable using :: in this way though, as it looks remarkably similar to the scope resolution operator in several other languages. I'm fluent in several languages, and unless I dug into the details of what :: was supposed to be, I would think it was a scope resolution operator.

What about object->method and object:>function.

zenparsing commented 9 years ago

What about object->method and object:>function.

@lukescott which operator for which semantics?

Naddiseo commented 9 years ago

@lukescott exactly my thoughts too! I agree that it should be two separate operators, but wasn't :> in an existing proposal for calling a prototype method? (I seem to remember seeing the operator somewhere but I can't remember where).

@zenparsing, I would assume object:>function would de-sugar to function.bind(object) and object->method would de-sugar to object.method.bind(object)

lukescott commented 9 years ago

@zenparsing "method" meaning function on the object, and "function" meaning not on the object. So:

I'm still on the fence about object::function/object:>function in general though. It just feels wrong to me, perhaps because function really doesn't belong to object and the syntax makes it seem that way. So even with changing :: to :>, it still doesn't sit right.

If you were to see this without being told, which one does what?

foo->doSomething()
foo:>doSomething()
foo::doSomething()

I think in all three cases you would think doSomething is somehow apart of foo.

Perhaps something like function::object() might be better, but how is that different than function.call(object), besides saving space at the expense of clarity? I'm having a hard time seeing the need.

With this example:

import { map, takeWhile, forEach } from "iterlib";

getPlayers()
::map(x => x.character())
::takeWhile(x => x.strength > 100)
::forEach(x => console.log(x));

Is there any reason that can't be:

getPlayers()
.map(x => x.character())
.takeWhile(x => x.strength > 100)
.forEach(x => console.log(x));

If this is an object, why can't map, takeWhile, and forEach be part of the object? And if it's an array, both map and forEach are builtin. I could see this perhaps being helpful for lodash or jquery, but these libraries already have chaining.

lukescott commented 9 years ago

Just had an idea.. If the goal of object::function is to make chaining easier for libraries like lodash or jquery, how about something like this:

import { map, takeWhile, forEach } from "iterlib";

getPlayers()
.(map, x => x.character())
.(takeWhile, x => x.strength > 100)
.(forEach, x => console.log(x));

So the syntax would be object.(function, ...). It would force you to call the function, but it also looks like function is a helper and not part of object.

Then you just have this for the other example:

Promise.resolve(123).then(console->log);

The -> should have memorization, but the object.(function, ...) syntax wouldn't need it because you couldn't assign it to a variable.

I think that satisfies all the use-cases mentioned in the proposal, while reducing the complexity (I hope!).

Naddiseo commented 9 years ago

@lukescott, I sort of like that, with the exception of using the comma. Here's some ideas I've played with:

import { map, takeWhile, forEach } from "iterlib";

// Current ES6 way of doing it
forEach(x => console.log(x),
    takeWhile(x => x.strength > 100,
        map(x => x.character(),
            getPlayers()
        )
    )
);

// Explicitly binding
forEach.call(
    takeWhile.call(
        map.call(
            getPlayers(),
            x => x.character()
        ),
        x => x.strength > 100
    ),
    x => console.log(X)
)

// Current proposal
getPlayers()
    ::map(x => x.character())
    ::takeWhile(x => x.strength > 100)
    ::forEach(x => console.log(x));

/* idea from @lukescott
I like using the `.()` but using a comma in the list seems strange, it reminds me of the `(0, obj)(args)` idiom
*/
getPlayers()
    .(map, x => x.character())
    .(takeWhile, x => x.strength > 100)
    .(forEach, x => console.log(x));

// Using different separators, they all look strange.
getPlayers()
    @(map, x => x.character())  // Too much like decorator
    #(takeWhile, x => x.strength > 100) // Might be confused with prototype operator
    :>(forEach, x => console.log(x)); // Isn't this operator in use somewhere else?

/* Replacing the first comma with a colon seems pythonic, but seems easier to read for me probably because I use python at $WORK */
getPlayers()
    .(map: x => x.character())
    .(takeWhile: x => x.strength > 100)
    .(forEach: x => console.log(x));

// How does it look with 0 and multiple args? Still readable, though a little bit strange for zero args.
getPlayers()
    .(map: x => x.character())
    .(takeWhile: x => x.strength > 100)
    .(doSomething:)
    .(twoArgs: 1, 2)
    .(forEach: x => console.log(x));

// Using a completely new operator
getPlayers()
    @>map(x => x.character())
    @>takeWhile(x => x.strength > 100)
    @>forEach(x => console.log(x));

// Another operator, that looks like an arrow. although, it really is very close to -> depending on the font.
getPlayers()
    ~>map(x => x.character()) ~>takeWhile(x => x.strength > 100)
    ~>forEach(x => console.log(x));

Of all these, I really like .(function: args), followed by ~>, but having -> and ~> would probably make the learning curve a bit steeper.

lukescott commented 9 years ago

@Naddiseo The issue with ::, :>, @>, ~>, etc is you end up having to make this work:

import {foo} from "./lib"
var fooBindObj = object::foo; // doesn't matter if its ::, :>, @>, ~>, <insert new operator here>

The problem object::function is trying to solve, from my understanding of the proposed spec, is chaining without wrapping. The above example is a side effect.

This syntax forces you to call the method, so the assignment part is no longer a problem:

var result = object.(foo);

The comma could be fine because .( would be more like call:

// Chaining
getPlayers()
    .(map, x => x.character())
    .(takeWhile, x => x.strength > 100)
    .(forEach, x => console.log(x));
// could be thought of as
getPlayers()
    .boundCall(map, x => x.character())
    .boundCall(takeWhile, x => x.strength > 100)
    .boundCall(forEach, x => console.log(x));

But if a colon was used, you could simply omit the : when no arguments are used:

object.(doThis: "foo")
object.(doThat)

The fat arrow syntax also has abbreviated syntax - {return ...} is optional.

The ::object.method (aka object->method), even though it looks similar, is really meant to solve another problem: Passing an object/class method without changing the binding.

Naddiseo commented 9 years ago

@lukescott I see your point. So, really, this current proposal should be split into two separate ones: one that enables the fluid style interfaces for generic functions; and one that is a syntactic replacement for .bind. @zenparsing, what do you think to this? Keeping the proposal more focused?

Regarding the comma: when you say "more like a call," what are you conceptualizing as being called? I am assuming you see some hidden "boundCall" function as being called. For me, I see that first item/name as the thing to be called, so the comma after it seems misplaced as I read that item as the first in a list, but my brain wants to interpret that first item as the function. An alternate syntax for binding to generics could be:

getPlayers()
    .map:(x => x.character())
    .takeWhile:(x => x.strength > 100)
    .forEach:(x => console.log(x));

Which doesn't look quite so strange, but you again run into the problem that map/takeWhile/forEach aren't really part of the return from getPlayers and are actually unrelated functions, but still look like they are.

lukescott commented 9 years ago

@Naddiseo yeah, object.(function, ...args) is like a hidden function. The first argument would be the function to call. That "hidden function" would look something like:

Object.prototype.boundCall = function(fn, ...args) {
    return fn.call(this, args);
};

Although it isn't really a function, so it would desugar the same way:

// from
getPlayers()
    .(map, x => x.character())
    .(takeWhile, x => x.strength > 100)
    .(forEach, x => console.log(x));

// what babel does now
var _context;
(_context = (_context = (_context = getPlayers(), map).call(_context, function (x) {
    return x.character();
}), takeWhile).call(_context, function (x) {
    return x.strength > 100;
}), forEach).call(_context, function (x) {
    return console.log(x);
});

For me, I see that first item/name as the thing to be called, so the comma after it seems misplaced as I read that item as the first in a list, but my brain wants to interpret that first item as the function. An alternate syntax for binding to generics could be: ... .function:( ... but you again run into the problem that map/takeWhile/forEach aren't really part of the return from getPlayers and are actually unrelated functions, but still look like they are.

I think we can both agree that .(function coveys the intent better. A colon after the function name could be ok .(function: ...args). But, if with the comma .(function, ...args) your brain (correctly) interprets the first item as the function, then why not keep the syntax simple and use a comma?

Regarding object->method, one concern I do have is Coffescript uses -> as function(){} similar to ES6 fat arrows, but without the bind. That would prevent ES from using -> in the same way. So there's that to consider... Since the goal is to pass class methods as callbacks, perhaps something like this would be better:

class Foo {
    handleClick() => {
        // ...
    }
}
var foo = {
    handleClick() => {
        // ...
    }
}

A nice bonus to that is memorization in #17 wouldn't actually be needed.

Naddiseo commented 9 years ago

Something occurred to me: if we replace :: with | and this with stdin, you get what looks like a cli, or similar syntax to some templating languages. Perhaps ES should do something similar with the pipe, although I'm not sure how you'd use it with the bit-or operator.

Regarding object->method: that is a good point, but I'm not sure how often that particular use case comes up; so it might not be worth shortening function () {} to ->. (Although I do see a good symmetry argument to be made.)

The was ES currently binds methods to classes is one of the things I think was poorly thought through in ES6. Methods on classes should by default be bound to them; if you didn't want them bound, then use a plain function. Instead of method() => {} being bound, I'd rather advocate for the current spec to be changed so that it's bound by default, and then maybe introduce other syntax for creating unbound methods, or use a decorator a la python's @staticmethod

lukescott commented 9 years ago

@Naddiseo

Re: method binding:

I would agree it would be beneficial for methods to be bound by default, but isn't it too late for that? ES6 is done. Also if they were bound by default, wouldn't that cause a 2x performance decrease in the ES5 emulated versions? Even if it were possible to change, it would be far better to leave it as is and later allow something like this:

class Foo {
    boundMethod() => {
        // ...
    }
    notBoundMethod() -> {
        // ...
    }
}

With a linter you can ensure either -> or => is used.

Re: -> and Coffeescript: I would consider C++ and CofeeScript to be the main influences on ES/JavaScript syntax wise. Even though I'm not a fan of CofeeScript, fat-arrows are in ES6 and they are rather neat. Using -> ("skinny-arrows") seems like a natural extension, especially when you can write a function in an object/class without the keyword function.

Re: |: Besides | being a bitwise operator, I see where you're going with it, but ES/JavaScript uses () to call a function. So you'd end up with players|map() which still leaves the possibility of var fn = players|map. I'm also not sure how relatable a shell is to a programming language, at least in this case.

I still think .(function, comma or colon aside, is the best option considering that it limits the syntax to the use-case we're trying to solve. Also it's very clear what's going on, where the functions come from, and uses similar/familiar syntax to normal method calls. Personally - I prefer the comma. One less special token for the parser to look for, and one less symbol for the programmer to remember/use.

Re: The above, and any features added:

Overall new features should be intuitive enough that the meaning is obvious without looking at a manual. In order to do that existing syntax needs to be leveraged, whether it's part of the language being augmented, a sister language with similar syntax, or other languages widely adopted by those using the original language (like CoffeeScript, within reason).

IMO, inventing new syntax, or re-purposing existing operators for other purposes, should be avoided when possible. ES6 has done this rather well.

Naddiseo commented 9 years ago

@lukescott

Re: method binding: given how ES is design by committee and tries to preserve backwards compatibility, yes it's too late. And, yes, it does look like the ES5 emulations would suffer quite a bit performance wise.

Re: ->: I've only ever used Coffeescript when submitting PRs to projects that use it, but it does seem to have some nice syntax sugar. Python is also an influence on ES, I believe the generator/comprehension syntax was borrowed.

Re: |: shell was the first example that came to mind. The metaphor is in other languages too, usually as "source" and "sink", another example is the feed operators in perl 6. It basically comes down to chaining the output of one function as the input of another, or put another way, changing the syntax from head-first, to head-last, which in this case seems natural as it also follows English, and temporal, ordering: I have thing, and I want to do fn1 to it, then fn2, then fn3, then fn4. I agree, .(function is currently the best option to address this particular usage, I'd like to hear from @zenparsing about it. As for comma vs colon, I'd get used to it either way, and would have to be decided by the people that actually write and implement the official specs. I think I favour the colon also because it's familiar from python, it reminds me of thing.something(lambda: somethingelse).

Overall new features should be intuitive enough that the meaning is obvious without looking at a manual. In order to do that existing syntax needs to be leveraged, whether it's part of the language being augmented, a sister language with similar syntax, or other languages widely adopted by those using the original language

100% this. Unfortunately, I think it's also the biggest argument against the .(function syntax, and also the :: syntax since it's current use is not seen in any language I know.

lukescott commented 9 years ago

@Naddiseo :: is commonly known, see http://en.wikipedia.org/wiki/Scope_resolution_operator

.(function isn't really "new" syntax per-sey if you think of it as a nameless function. Using a comma instead of a colon solidifies that interpretation. No additional symbols are used. Just the absence of a function name, which causes your brain put more significance on the first argument.

Somewhat off-topic, but avoiding the colon would leave the door open for (optionally) named parameters:

function sayHello(name String) {
    console.log(name, "says hello!");
}
sayHello("John");
// vs
function sayHello(fromName: name String) {
    console.log(name, "says hello!");
}
sayHello("John"); // cause a linting error
sayHello(fromName: "John"); // works
class GameObject {
    // abbreviated so name and variable are the same
    move(x: Number, y: Number) {
        this._x = x;
        this._y = y;
    }
}
// ...
gobj.move(x: 20, y: 30)

(Assuming Go-like types, see https://github.com/lukescott/es-type-hinting)

Naddiseo commented 9 years ago

@lukescott, yes, I know it as the scope resolution operator, I meant using :: as a shortcut for .call(lhs, ...args) isn't known in any language I've seen.

.(name is new in that I've not seen . introduce a hidden function that's not related to the lhs of the operator.

Okay, you've convinced me, that's a good point. (Is there a spec floating around for named parameters?) It might also look like it's missing a type annotation (although they're not allowed in that context)

getPlayers()
    .(fn: map, p => p.character())

Actually, I really like that.

lukescott commented 9 years ago

@Naddiseo There isn't a spec yet that I know of. I could start one :)

The closest thing is Swift (which came from Objective-C). They do it like this:

// : is used for type hinting
func sayHello(fromName name: String) {}

I really like Go types, which look like:

// without named parameter
func sayHello(name string) {}
// built-in types in Go are lowercased - custom types are capitalized

Combining the two concepts:

// with named parameter
function sayHello(fromName: name String) {}
sayHello(fromName:"John")
// shorthand named parameter:
function sayHello(name: String) {}
sayHello(name:"John")

I quickly put together https://github.com/lukescott/es-type-hinting because of preferring a simple space over a colon for type hinting. It really plays better with other potential features. If there is any interest I could use some help with it.

zenparsing commented 9 years ago

Hi everyone. Thanks for keeping this discussion going! I've been listening in, although I've been busy with other spec-y things recently.

Leaving syntax proposals aside for the moment, the assertion brought up in this thread is that using :: for both infix this-binding and method extraction is confusing. In particular, I think the special significance of . on the right-hand-side of the prefix :: bears special attention:

 ::a.b.c; // The second dot has special significance in this context
x::a.b.c; // But not in this context

Another issue with prefix :: (from the other thread) is that some users will expect the following to hold:

assert(::a.b.c === ::a.b.c);  // Not actually the case

Which will lead to the following bug:

element.addEventListener("click", ::obj.method, false);
element.removeEventListener("click", ::obj.method, false); // Not the same handler!!

Also, in other languages (e.g. Python, Ruby) extracted bound methods are equal in this sense.

So I think we definitely have some semantic and principle-of-least-surprise issues with the prefix form.

The question is, can we resolve those issues by splitting the syntax? I don't think so. Even if you used -> for the method extraction use case, you'd still have the method identity problem:

obj->x !== obj->x;

Let's say we fixed that problem by returning a memoized frozen bound function (which has problems of it's own, by the way). Then we're left with a different problem: having to justify the introduction of two new operators into the language. There's a pretty heavy burden of proof for adding syntax, and I don't think we've passed the bar.

As far as the other syntax proposals go (e.g. obj.(func, ...args)), while they appear solid, I don't find that they are an improvement over using :: for the extension method case.

Basically, I think we have some more work to do on the method extraction side of things. One possibility would be to make this into a more minimal proposal where we drop either the prefix or the infix form for the time being.

Naddiseo commented 9 years ago

@zenparsing, syntax aside, my vote would be to drop the prefix form until the memoization can be sorted out, and focus on the infix form. I would favour splitting this proposal into two, and renaming the infix :: proposal to something like "es-extension-functions" or "es-virtual-functions" to better reflect what you're trying to accomplish. Once we've done that, then we can revisit the syntax issues.

lukescott commented 9 years ago

@Naddiseo memorization wouldn't be necessary with the object.(function, ...args) infix form. It would force the function to be called, so a .bind would not be necessary. Am I correct in assuming the infix was to solve utility function chaining?

I don't think "prefix" form should be dropped. Instead, perhaps @zenparsing could split these into two new repos? es-method-binding (prefix), es-function-chaining (infix).

The prefix form problem can be solved with expanding method definitions, such class Foo{boundMethod() => {...}}.

Naddiseo commented 9 years ago

@Naddiseo memorization wouldn't be necessary with the object.(function, ...args) infix form. It would force the function to be called, so a .bind would not be necessary.

Sorry, that's what I was meant to convey. The memoization is specifically for the prefix form.

Am I correct in assuming the infix was to solve utility function chaining?

That was how I was reading it.

I don't think "prefix" form should be dropped. Instead, perhaps @zenparsing could split these into two new repos? es-method-binding (prefix), es-function-chaining (infix).

:+1:

dtinth commented 9 years ago

No one seems to mention Java here...

In Java 8, Method References uses the :: syntax, same as this proposal. This Java code:

Arrays.sort(rosterAsArray, Person::compareByAge);

desugars into:

Arrays.sort(rosterAsArray, (a, b) -> Person.compareByAge(a, b));

I feel that Person::compareByAge meaning completely different things in Java and JavaScript would make it very confusing. With this current proposal, it has to be rewritten into ::Person.compareByAge, which still looks kinda weird and confusing.

My opinion: it may be a good idea to use :: operator for method binding (as the same as Java), and then find another operator for the function bind syntax (which provides chaining similarly to extension methods).

I also have some proposals about function chaining here:

maxnordlund commented 9 years ago

I find the foo.(bar, 1, 2, 3) syntax hard to read, but :> reminded me of Elixir and LiveScript pipe operator |>.

Elixir:

[1, [2], 3] |> List.flatten |> Enum.map(fn x -> x * 2 end) # [2, 4, 6]

LiveScript:

[1 2 3] |> map (* 2) |> filter (> 3) |> fold1 (+) # 10

Suggestion for ES:

getPlayers()
    |> map(x => x.character())
    |> takeWhile(x => x.strength > 100)
    |> forEach(x => console.log(x));

This solves the iterlib example, and leaves the door open for .bind syntax.

lukescott commented 9 years ago

@dtinth I'm not familiar with Java 8, so that syntax is new to me. However it does show that, even for Java users, using :: to bind an external function would be surprising. The :: operator in Java 8 is similar to how the scope resolution operator is used in C++.

Hopefully pointing out the obvious here: Java and JavaScript are not related. It's like comparing a Car with a Carpet. The name "JavaScript" came from Netscape. JavaScript is actually "ECMAScript".

object.(fn)(args) could be expressed as object.(fn) which looks like a function call but isn't. That could end up being more confusing. With object.(fn, args...), object.(fn) would be a function call with no arguments.

@maxnordlund

There isn't a significant reason to allow for a .bind syntax. Any .bind syntax causes some foot-gun potential, as shown in issue #17. A .bind syntax is no more helpful than using .bind itself, but at least the behavior of .bind() is obvious and already known.

object::function, object~>function, object |> function, etc.. all allow for the possibility of .bind. It doesn't matter what the characters are. "leaving the door open" means that using these options without calling the function would be invalid. Syntax that looks correct, but is actually invalid, is surprising!

The object.(fn, ...args) syntax intentionally does not allow for .bind. It's obvious you can't do object.(fn. There is no surprise there.

The user shouldn't have to learn invalid syntax by running into a syntax error. It should be obvious, as much as missing a ) would be.

Which one of these look like an error to you:

var fn = object::fn; // object~>fn , object |> fn, ... doesn't matter

(Above assumes "leaving the door open" means the above is an error)

var fn = object.(fn;

To me it's easy to skip over the first one. The second is easier to spot.

maxnordlund commented 9 years ago

Actually @lukescott I would assume that object |> fn became fn.call(object), not a syntax error at all. That's what I meant with "leaving the door open", e.g. not defining a .bind syntax at all. As I and others pointed out in #15 there is a need for partial application without binding of this. That is what a I think this specialized syntax should solve.

For binding syntax you could use :: in some form or other. However, if it should support foreign this values, foo::bar where bar is not on foo, then it might need to be something more like the current proposal. But then foo.bar.baz::spam.oven.timer() isn't obvious what should happen.

lukescott commented 9 years ago

So object |> fn and object |> fn() are the same thing - both are function calls with 0 arguments. Not to crazy about that. Someone could still do this:

var objFn = object |> fn

They would just get the result of fn, but could look differently. I'm not sure that a function should be called without a set of () - somewhere. That's already well established in the language.

zenparsing commented 9 years ago

@dtinth Thanks for the pointer to Java 8's usage of ::. I find the semantics a little odd, in particular the difference between SomeClass::instanceMethod and someInstance::instanceMethod. In general, I don't care too much what Java does, but similarity across languages would be nice (all other things being equal).

@maxnordlund A couple of questions regarding your suggestion of |>:

a |> b(c);
// 1.  b.call(a, c);
// 2.  b.call(undefined, a, c);
maxnordlund commented 9 years ago

@zenparsing while I take inspiration from Elixir and LiveScript, here we're talking about .call,.apply and .bind syntax for ECMAScript. My suggestion would therefore turn a |> b(c) into b.call(a, c).

@lukescott and @zenparsing I don't differentiate between foo |> bar() and foo |> bar. This would be analogous to new Foo without parenthesis, or optional braces after if, for, => etc. So I think there is precedent for both.

Again, this is a suggestion just for .call and .apply, not bind. To give an example for .apply, it would turn foo |> bar(..rest) into bar.apply(foo, bar).


The .bind syntax is trickier, but as far as I understand the Java example, it does dot property resolution and the binds the resulting value. E.g. foo::bar becomes foo.bar.bind(foo), and foo.bar.baz::deep.meth becomes foo.bar.baz.deep.meth.bind(foo.bar.baz), correct @dtinth?

This might be viable for binding method references when giving them to third party API:s, and open up the possibility for partial applied parameters using parenthesis. E.g. foo::bar(a, b) would become foo.bar.bind(foo, a, b) and ::baz(1, 2) would become something like baz.bind(undefined, 1, 2) but not actually binding this for baz.

ssube commented 9 years ago

@maxnordlund The Java version of the :: operator is described in section 15.13 of their spec. If I'm reading it correctly, they forbid arguments after the method reference (no a::b(c)), but it otherwise follows your example. I'm not sure why they disallow arguments, but suspect it covers some limitations on how references are turned into invokable byte code that probably aren't an issue in JS.

Using :: to return bound methods to outside callers, especially for callbacks or events, would be quite helpful. Something like:

class Foo {
  handleEvent(e) {
    ...
  }

  bindEvent(trigger) {
    trigger.addEventListener(this::handleEvent);
  }
}

Going out on a limb, if this::handleEvent were to return equal references each time it was called (not quite aligning with .bind), that would allow adding and removing event handlers using the this::handler syntax.

Either way, having the bind operator work with super (super::handleEvent) and arbitrary references ('foo'::length, using one of the Java examples) would greatly simplify passing around callbacks, attaching classes to DOM events, and other common uses.

Allowing arguments could also be useful:

class Cache {
  constructor() {
    this.data = {};
  }

  addItem(id, data) {
    this.data[id] = data;
  }

  cacheResponse(id, req) {
    return req.then(this::addItem(id)); // desugars to (data) => this.addItem(id, data)
  }
}
lukescott commented 9 years ago

@ssube The issue with this::handleEvent returning the same reference is talked about in issue #17. It would require bind memorization. Given your example, this would work without needing memorization:

class Foo {
 // use => to cause handleEvent to always be bound to this
  handleEvent(e) => {
    ...
  }

  bindEvent(trigger) {
    trigger.addEventListener(this.handleEvent);
  }
}

This is similar to issue #16, but extends the existing fat-arrow syntax to class methods.

maxnordlund commented 9 years ago

Yeah, it might not be obvious that :: (or whatever syntax you choose) returns a freshly bound function reference, but consider this example:

class Foo {
  _bar() {
    console.log("Real implementation of bar, always bound to a Foo", this instanceof Foo)
  }
  get bar() {
    if (!this.hasOwnProperty("_bar") {
      Object.defineProperty(this, "_bar", { value: this._bar.bind(this) })
    }
    return this._bar // Will always point to the same `_bar` for a given instance of Foo
  }
}

By (ab)using getters and Object.defineProperty you can achieve a lazy evaluated, memorized, bound method in ES6 right now. The trick here is to use hasOwnProperty to see if it has been bound, and act accordingly. Also remember that any dot property resolution walks up the prototype chain, but hasOwnProperty specifically avoids that.

The same trick could be inlined into the method itself, or something to that effect, if the extra _bar is considered too invasive. See https://github.com/maxnordlund/pico.js for a more involved example. (It's an experiment to see what I could to with current browsers)

lukescott commented 9 years ago

@maxnordlund Issue #16 already has a solution for handleEvent(e) => {} / ::handeEvent() {} :

var Foo = (function() {
    var Foo = function() {
        this.handleEvent = this.handleEvent.bind(this);
    };

    Foo.prototype.handleEvent = function() {
        // this will always be instance of Foo
    };

    return Foo;
})();

Unless I misunderstood what you were trying to do.

zenparsing commented 9 years ago

@ssube We are currently disallowing super references in the prefix form.

let fn = ::super.foo; // Currently a runtime error

But I think we can relax this restriction.

Otherwise, the semantics you are proposing for an infix :: is already covered by the prefix form.

dtinth commented 9 years ago

@lukescott I understand that Java and JavaScript are totally different thing, and I like to use the analogy “ham and hamster” to explain that :wink:. Nevertheless, I still think it's always a good idea to look at prior work of what has been done in other languages (especially similar ones).

In my opinion, what Java and ECMAScript is accomplishing through the :: operator is essentially the same — creating a callable which is bound to some object. That's why I think using them differently would cause confusion.

As for the second case, you have a good point that .() would cause confusion. How about .[]? The square brackets gives the feeling of wrapping something, in this case, creating a function that wraps the function inside the brackets by bounding its this to some object.

ssube commented 9 years ago

@lukescott That global memoization is pretty ugly. It sounds like you're suggesting that => methods desugar into a constructor-time binding or the example @maxnordlund posted. Like:

class Foo {
  bar() => {
    console.log(this instanceof Foo);
  }
}

// becomes

var Foo = (function() {
    var Foo = function() {
        this.bar = function() {
            console.log(this instanceof Foo);
        }.bind(this);
    };

    return Foo;
})();

@zenparsing What was the logic behind disallowing that? I'm not sure binding super methods would be the most useful feature, but sounds easy enough to implement if we have method binding in general. FWIW, I'm a fan of the infix binding operator for consistency with JS' property access and C#/C++/Java's method reference syntax (and tremendously excited about this as a language feature).

zenparsing commented 9 years ago

@ssube Regarding disallowing super references, I think it was just an oversight on my part.

Thanks for your interest! C++ uses :: as a scope resolution operator, which is pretty different from method extraction. C# uses it in much the same way (namespace lookup). It would be nice if JS and Java's usage of :: were aligned, but that means that we'd need a different operator for extension methods. Unfortunately I haven't seen any compelling alternatives.

ssube commented 9 years ago

@zenparsing C++ and Java behave very closely for Foo::bar when Foo is some class with a method bar, although C++ does make you jump through a lot of hoops to actually invoke the function pointer. Those both end up being somewhat equivalent to Foo.prototype.bar in JS (would it make sense for :: to access constructor prototypes?).

While Java allows instance methods with 'foo'::bar, C++ doesn't really have anything there. Would it be insane to use the :: operator for both reference and extension?

When evaluating foo::bar(baz):

  1. If bar is a name in the current scope, then
    1. invoke bar as bar.call(foo, baz)
  2. If foo.prototype has an enumerable property bar, then
    1. return a bound and partially-applied function equivalent to (...args) => foo.prototype.bar.apply(foo, [baz].concat(args));
  3. Else, throw a undeclared reference exception?

I imagine that could be ambiguous in a few situations and might run afoul of JS' scoping, but IMHO it's the closest to how Java and C# handle this sort of thing. Apologies if this overloading has already been suggested.

benjamingr commented 9 years ago

@ssube a hierarchy was actually part of the original proposal 2 years ago https://esdiscuss.org/topic/scoped-binding-of-a-method-to-an-object and https://esdiscuss.org/topic/protocol-library-as-alternative-to-refinements-russell-leggett

justinbmeyer commented 9 years ago

I agree that object.method.bind(object) is more important. I have an immediate need to implement something like this in a templating language. So to help move this along, the following is a summary of the options I've found. Please let me know if I've missed something.

object.method.bind(object)

fn.call(object)

There's also a proposal to change these to chain the first argument instead of this, making the previous examples the equivalent of:

fn.call(null, object)

In this case, I believe the ones with a . would not make sense:

zenparsing commented 9 years ago

@justinbmeyer At a high level, the options are:

  1. The current design, where :: is used for binding and the prefix form does method extraction.
  2. Splitting method extraction from "pipelining".

For 2, my feeling is that the only options that have a chance are the following:

// Method Extraction (kinda similar to Java syntax)
obj::method;

// Pipelining Option 1 (Similar to other languages but UGLY)
obj |> fn();

// Pipelining Option 2 (Looks nicer but goes against precedent)
obj->fn();

I would love to break pipelining out into it's own operator, but I'm not thrilled about the syntactic options.

justinbmeyer commented 9 years ago

Thanks @zenparsing. I think they should be split. I'll go with obj::method for extraction. I do think:

getPlayers()
    .[map](x => x.character())
    .[takeWhile](x => x.strength > 100)
    .[forEach](x => console.log(x))

Would be quite understandable to beginners who have seen obj[prop] as a way to read an arbitrary property. This would be a way to call an arbitrary method with a given this. And, having a starting [ and ending ] token would help make getPlayers().[ mapping("name") ]() clear in:

// A mapping method generator
// @param {String} name A property name
// @return {function()} A function that maps each item in `this` to its property `name`
var mapping = function(name){
  return function(){
    return this.[map](function(item){
      return item[name]
    })
  }
};

getPlayers().[ mapping("name") ]()
justinbmeyer commented 9 years ago

Do the following allows for sub-expressions that evaluate to the method to be called?

obj->fn();
obj |> fn();

I don't think so, at least not un-ambiguously. What should the following become?

obj -> fn()()

// option 1
fn.call(obj)()

// option 2
fn().call(obj)

I assume option 1. But this prevents using functions that could return functions like in my previous example.

zenparsing commented 9 years ago

@justinbmeyer Forgot to mention: if they are going to be split, then I think the "pipelining" operator needs to inject the left-hand side into the first argument position, instead of the this value.

Reasons being:

  1. Using this in such a free manner is controversial with some.
  2. With a first arg, you can do destructuring in the parameter list.
  3. With a first arg, you can name it something self-describing in the parameter list.
  4. The function is generally useful on it's own (without a special operator).
lukescott commented 9 years ago

I do like object.[function](...) better than the other alternatives presented. It seems clearer that function is being bound to object before being called. However, I would be careful with the possibility of object.[function]. For that reason I somewhat prefer object.(function, ...) as it forces a function call - and it's less to type. But object.[function](...) is nicer looking than the |> ~> alternatives that have been mentioned.

Either way I definitely think the two should be split.

zenparsing commented 9 years ago

Do the following allows for sub-expressions that evaluate to the method to be called?

They could, depending on how you specify the operator precedence. If |> is lower than ., then you could do something like:

obj |> X.y();

// Maybe??
X.y(obj);

But it's pretty ugly.

Other than that, you could allow parens:

obj |> (something.arbitrary)();
// or:
obj->(something.arbitrary)();

// To:
(something.arbitrary)(obj);
justinbmeyer commented 9 years ago

@lukescott I think the following:

object.[function]

would still be useful. Instead of:

import {function} from "functions";
import {object} from "objects";

var methodOnObject = method.bind(object)

one could do:

import {function} from "functions";
import {object} from "objects";

var methodOnObject = object.[function]