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.

Artazor commented 7 years ago

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....

MeirionHughes commented 7 years ago

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

dead-claudia commented 7 years ago

@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.

bathos commented 7 years ago

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.

mgol commented 7 years ago

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.

InvictusMB commented 7 years ago

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) ?

Artazor commented 7 years ago

@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.

dead-claudia commented 7 years ago

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 notifications@github.com 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 .

Artazor commented 7 years ago

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.

dead-claudia commented 7 years ago

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 notifications@github.com 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 .

InvictusMB commented 7 years ago

@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.

Artazor commented 7 years ago

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

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

InvictusMB commented 7 years ago

@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:

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

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 ](https://en.wikipedia.org/wiki/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.
MeirionHughes commented 7 years ago

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.

bathos commented 7 years ago

@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.)

Volune commented 7 years ago

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.

InvictusMB commented 7 years ago

@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.

dead-claudia commented 7 years ago

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

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 notifications@github.com 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 .

InvictusMB commented 7 years ago

@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.

Volune commented 7 years ago

@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.

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

InvictusMB commented 7 years ago

@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.

bathos commented 7 years ago

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?

hax commented 7 years ago

@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.

dead-claudia commented 7 years ago

@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.

bathos commented 7 years ago

@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?

dead-claudia commented 7 years ago

@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.

fatcerberus commented 7 years ago

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.

Alxandr commented 7 years ago

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.

fatcerberus commented 7 years ago

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.

dead-claudia commented 7 years ago

For what it's worth, your binding example could be also done like this (which is 100% equivalent for most use cases):

(...args) => this::method(...args)

On Wed, Feb 8, 2017, 13:56 Bruce Pascoe notifications@github.com wrote:

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.

— 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-278426130, or mute the thread https://github.com/notifications/unsubscribe-auth/AERrBFkIIOBAi4GiwZ_hAS2ihlqbMvLvks5rag_egaJpZM4Lt7f3 .

fatcerberus commented 7 years ago

True, but that's even more verbose than the venerable this.method.bind(this).

robotlolita commented 7 years ago

@fatcerberus I'm not sure adding functionality in JS to make the concepts more familiar to C++ developers is a good idea, but in any case allowing this::someMethod or even ::this.someMethod is a pretty bad idea for reasoning because you get back a function that has different semantics AND identity.

Which means that something like:

someObject.on('event', this::someMethod);
someObject.remove('event', this::someMethod);

Will not work, as each expression constructs a new function object, with a new identity. At the very least this.method.bind(this) and (...args) => this.method(...args) are both more explicit when it comes down to figuring out that the expression results in a new function with different identity.

fatcerberus commented 7 years ago

[...] pretty bad idea for reasoning because you get back a function that has different semantics AND identity.

This I can agree with, and it's indeed a mistake I could even see myself making often. The only way to solve that would be some kind of binding registry or something, which is way outside the scope of this proposal.

Regarding the symbology issue, if :: acts completely differently from how that symbol is interpreted in other common C-like languages (and let's face it, JS still looks a lot like C++ syntactically), then that's a potential source of confusion for multilingual developers. It's a conversation worth having IMO, but maybe not in this thread. ;)

Alxandr commented 7 years ago

It's also likely to lead to anti-patterns such as creating new instances of functions in every render of react components. Not that people doesn't already do that, but if you make syntax for it...

InvictusMB commented 7 years ago

@isiahmeadows

I'm proposing dropping the binding half of the proposal, though.

OK, so you are suggesting to drop the binding from the binding operator to advance the binding operator proposal. Nevertheless, there is already an operator like that and it is called pipeline operator and is explored in depth here.

@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?

Yes. It is heavy with baggage from other languages. Specifically C++, Ruby and PHP being among those. It has slightly different meanings in various languages but what is common is that it is always about doing scope resolution. Therefore using :: to pipeline this context will be intuitive and easy to adopt.

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.

Yes, it is part of the discussion since the original proposal includes unary form of ::. ::console.log does resolve log in scope of console and then it does the binding. And I cannot disagree that it's ugly and weird. It's a pity that binary and unary forms of :: have so different semantics. Binary form of :: as per current proposal enables binding with lexical scope resolution. While unary form is doing instance scope resolution. The mismatch is confusing and it indeed prevents this proposal from progressing. But that doesn't change the fact that both forms have a value to various programming styles.

So the question should not be about which version to drop but rather about how to sort this out so that everything is clear, unambiguos and extensible in future.

In my opinion, It's easy to start with :: doing instance scope resolution. You can then add ::@ for augmenting scope resolution to lexical scope. And further improve upon this with adding ::[] for computed property resolution as @Volune hinted. With all those sharing one thing in common: they are predictable in a way you get this context passed to RHS.

Functional pipelining without this in the picture is a complex topic in itself and should be a separate discussion. For example, there is a debate on currying and if you should prepend or append the piped argument. You don't have those kind of issues with this.

@robotlolita

you get back a function that has different semantics AND identity.

Identity is an implementation detail. It is technically feasible to enforce identity. In fact there are only two things you need to enforce the identity:

Both of those are already available.

@Alxandr

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.

I'm sorry. There are people who still prefer to build applications in OOP manner. For them this has a huge value and is one of the core concepts to their mindset. Having this-pipelining operator for them is as valuable as having functional pipelining operator for functional programming people.

robotlolita commented 7 years ago

@InvictusMB to clarify: as specified right now (::a.b), you get objects with different identities.

In any case, having all a::b code return the same object always is tricky when you have Realms, and cross-realm interactions. That would have to be tracked internally in the VM, in a way that all code in all Realms use the same mapping — this could be added as an internal slot in all objects, but then all objects become more expensive whether you use or not this feature.

Since identity is observable, this is a more semantic than optimisation concern (different from string interning), and one has to question whether users would want different identities in some cases and if enforcing the same identity in all Realms could result in some security or implementation problem.

fatcerberus commented 7 years ago

@InvictusMB

:: doing instance scope resolution

To be clear, this means what I was suggesting, right? foo::bar == foo.bar.bind(foo)?

fatcerberus commented 7 years ago

Basically my suggestion is that a::b should do what ::a.b (which I find to be awkward, syntactically) does now, and have a separate operator like -> for functional pipelining.

robotlolita commented 7 years ago

I really like a::b(c) as sugar for b.call(a, c) because then we could finally have some symmetry in function signatures:

// introduction form
function a::add(b) {
  return a + b;
}

// elimination form
1::add(2); // => 3

// introduction form
const Math = {
  a::add(b) {
    return a + b;
  }
};

// elimination form
1::Math.add(2); // => 3

But a different syntax for functional pipelining would probably be less confusing if we don't get syntax for explicit this parameters as well.

Volune commented 7 years ago
  • 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)

I was wondering, if we kept both object::func(arg) to be func.call(object, arg) and object::func to be func.bind(object), would there be a case where object::func(arg) and (object::func)(arg) have a different behavior?

robotlolita commented 7 years ago

@Volune they couldn't have different behaviour. You'd need something like (0, object::func)(arg) there to get it to mean func.bind(object) because parenthesised expressions don't change the meaning of the AST (they're only used to define precedence, so it's not even reified in the AST — see https://astexplorer.net/ for how common parsers handle object.func(arg) and (object.func)(arg)).

That could be changed and have parenthesis modify semantics, but that would break pretty much every other feature that exists today, which is not acceptable.

Volune commented 7 years ago

Thanks, I guess my question was differences between object::func(arg) and (0, object::func)(arg) (except going through Function.prototype.bind)

You now have me wonder, if we drop the object::func, then the operator would not be :: (or whatever symbol) but the couple :: (), like the ternary operator, and (object::func)(arg) would not make sense?

bathos commented 7 years ago

@Volune I can see how that’s intuitive looking, but I think the fact that ? always indicates a ternary is significant to why your proposition remains grammatically complex while ternaries are not — the RHS then could be any assignment expression, yet what you want is a different interpretation for cases where an initial member by depth of that assignment expression is a call expression. That’s paradoxical because it means you cannot know the AST until after you have the AST. There’s probably a way to alter expression grammar more broadly that would permit this, but my gut is saying it would be a big cascading change that is unlikely to go over well (seems like @robotlolita would be able to say with more confidence whether that’s true).

Edit: possibly I misread the last comment. Without a medial binding form, yeah, (object::func)(arg) would be invalid. The RHS is expected to be a call expression. This is the current grammar of the proposal I believe.

InvictusMB commented 7 years ago

@robotlolita I don't see any security or complexity concerns with regard to realms. It all boils down to identities of used symbol, function and argument. If they match across realms then bound function will have the same identity across realms.

@fatcerberus

To be clear, this means what I was suggesting, right? foo::bar == foo.bar.bind(foo)?

Yes. Precisely.

@Volune, @bathos If you always do bind then you don't have any issues with AST. As I mentioned before it is nice to substitute bind by call where possible but that should be a compiler optimization. And circumstances for that substitution are clear: if the identity of the bound function doesn't make sense in current scope you can throw it away and do only call.

May be it would not even be bindinternally. I can imagine this piping with lexical scope resolution @:: this way

const SymbolThisPiped = Symbol('ThisPiped');

function createPiped(fn) {
  const piped = fn[SymbolThisPiped];
  if (piped) {
    return piped;
  }
  return fn[SymbolThisPiped] = (context, ...args) => fn.apply(context, args);
}

//foo::@greet
createPiped(greet)(foo)
//bar::@greet
createPiped(greet)(bar)
//baz::@greet(qwerty)
createPiped(greet)(baz, qwerty)

And foo::@greet, bar::@greet, baz::@greet(qwerty) will reuse the same bound instance of greet.

Instance scope resolution :: can be built on top of that

function createBindings(fn) {
  const bindings = fn[SymbolContextBindings];
  if (bindings) {
    return bindings;
  }
  return fn[SymbolContextBindings] = new WeakMap();
}

function createBound(fn, context) {
  const bindings = createBindings(fn);
  const bound = bindings.get(context);
  if (bound) {
    return bound;
  }
  const newBinding = (context, ...args) => createPiped(fn)(context, ...args);
  bindings.set(context, newBinding());
  return  newBinding;
}

So that foo::bar(a, b)::baz(c, d)::bang(e, f) will translate to

compose(
  piped => createBound(piped.bar, piped)(piped, a, b),
  piped => createBound(piped.baz, piped)(piped, c, d),
  piped => createBound(piped.bang, piped)(piped, e, f)
)(foo);

Altogether with instance scope resolution for computed properties ::[]

/*
foo
  ::[bar + baz](a, b)
  ::@bang(c, d)
  ::ping(e, f);
*/

compose(
  piped => createBound(piped[bar + baz], piped)(piped, a, b),
  piped => createPiped(bang)(piped, c, d),
  piped => createBound(piped.ping, piped)(piped, e, f)
)(foo);
robotlolita commented 7 years ago

@Volune that depends on how you define the grammar. You could do it in a way that (object::foo)(bar) is recognised in the same way as object::foo(bar). The current proposal seems to go for disallowing useless parenthesis, but with:

CallExpression ::=
  Callee "(" ArgList ")"

Callee ::=
  BindExpression
| …
| GroupExpression

BindExpression ::=
  LHSExpression "::" MemberExpression

GroupExpression ::=
  "(" Expression ")"

Then object::foo(bar) is just another case for CallExpression, and the stuff to the left of the parenthesis can be anything recognised by Callee. Since Callee ends with GroupExpression. As long as GroupExpression allows BindExpression to happen inside of it, (object::foo)(bar) would be recognised by the grammar, and then handled in the same way stuff like (object.foo)(bar) is handled today (see https://gist.github.com/robotlolita/bf7b01119ecc347a3d38a562b16c29ff for a detailed explanation of that).

But in this case it'd be pretty much useless, and only serve to add complexity to the grammar, really. I would assume that JavaScript only allows (object.foo)(bar) to mean the same as object.foo(bar) because object.foo is also a valid expression on its own — that's the only reason my own language, Canelés, supports it, at least.

@InvictusMB each Realm has a separate copy of all intrinsic objects and globals, mostly so one iframe in a browser can't mess up objects in another iframe by doing stuff to its own objects. SpiderMonkey also seems to have some additional security stuff throw into that, which has made it hard for them to implement things like proper tail calls across realms.

I don't know what would be the implications of this on those VMs, but since there are cases where new features that shouldn't really impact security have interfered with security concerns in some VMs, that's something those VM writers should look into.

My comment about complexity was wrt implementing it in user land, because each realm has different intrinsics and globals. Adding it to the object itself works, but then it makes objects more expensive. Also AFAIK weakmaps also add some GC pressure, but that might be just v8's implementation of it, not really familiar with how other VMs handle this.

InvictusMB commented 7 years ago

@robotlolita I share your concerns but they still look to me like hairy implementation details of specific environments. On the other hand from a consumer standpoint I don't really care how this stuff would be handled by GC or interperter, or compiler, or VM. I will be absolutely fine if this stays in Babel for ages as long as I can write a clean code and get support from static analyzers. To me the only important things would be semantics, readability, cognitive load and ultimately the maintainability.

Speaking of semantics, identity is important and it's good that you brought this concern up. But I don't quite get the issues of cross realm interop and security here. Symbols are immutable and I believe it should be safe to share their identity across realms. So bound function should preserve identity across realms if the identities of original function and bound arguments can be shared. But then again this is a separate topic of marshalling rules and their implementation details and I don't see how is that relevant to language constructs. The only relevant fact is that the rules of marshalling should be clear. Therefore my question: how is marshalling foo☺>bar between realms different from marshalling bar.bind(foo)?

lukescott commented 7 years ago
  • 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)

I agree with this. The issue I have with ::, besides it being confused with other languages, is func looks like a name, not a variable. For example, object.func is basically object["func"], whereas object::func isn't.

Syntax like this would make more sense to me:

object.(func)

I like the above syntax because:

Downsides are:

robotlolita commented 7 years ago

This problem would go away entirely if we had a symmetric way of declaring this parameters in functions:

function self::add(x) {
  return self + x;
}

1::add(2);

This also solves problems with nested this where you have to access outer this values, but those don't happen as often now that arrows are a thing.

dead-claudia commented 7 years ago

@robotlolita I really like that sugar, but that doesn't solve this issue - this issue is specifically just proposing to drop the binding functionality. So feel free to file a new bug suggesting that.