tc39 / proposal-bind-operator

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

Pipeline + Bind strawman #37

Open bterlson opened 8 years ago

bterlson commented 8 years ago

I am considering an addition to the bind operator as proposed here to also satisfy the pipeline operator use cases. Essentially we allow a single ? in an arguments list to signify the parameter position to pass the LHS to, and if it's absent, use this. For example:

foo::bar(); // as normal
foo::bar(?); // approximately bar(foo)
foo::bar(1, ?); // approximately bar(1, foo)

Any thoughts on this?

bergus commented 8 years ago

I'm not sure how that is supposed to work. Is bar.bind(foo)(?) still the same as foo::bar(?)?

Where else would these special arguments lists be applicable? Only after bind expressions? I'd rather avoid that. If this is really needed (I believe not, it doesn't even simplify the expression), it should use a different operator than ::.

bterlson commented 8 years ago

My proposal is that this is not bound when using a placeholder. Syntactically only valid on the RHS of the bind operator.

Can you explain why allowing ? on the RHS of a bind operator is worth avoiding more than having two completely separate operators, one with the same placeholder syntax I am proposing we add and one without?

deontologician commented 8 years ago

I'm concerned this makes it look very perl-ish. I don't think it's bad personally, but I think it might be hard for people to swallow both :: and ? at once (and therefore lower the chances of this moving forward)

:: by itself is pretty simple to explain and use as it is now, even though it doesn't cover the pipelining case.

bterlson commented 8 years ago

@deontologician I don't think there is much concern about "looking perl-ish" :-P By "hard for people to swallow" do you mean for committee members to agree to more syntax or for users to accept?

bergus commented 8 years ago

I guess my worry is mostly about adding yet another meaning to ::. We already have two distinct ones, will a third make it better? It's already been proposed multiple times that extraction and binding get two different operators (for example object.:extractMethod and object:::bindFunction) which would make them better to distinguish and easier to explain. It was okay to me to use the same :: operator as it always means "binding object as this". Your new proposal doesn't share that common ground any more.

Also, if we introduce special arguments lists with placeholders, shouldn't we look for a more general use case, one that might be better composable with other languge features? Could such a placeholder thing simplify callbacks? Maybe an operator for partial application might solve this whole problem better?

I have not given it much thought specifically, but what about

parseInt(?, 10)     ≈ x => parseInt(x, 10)
bar(??)             ≈ function() { return bar(this) }
bar(1, ??, 2, ?, ?) ≈ function(x, y) { return bar(1, this, 2, x, y) }

Those might be combined with ::, but also be useful somewhere else.

deontologician commented 8 years ago

do you mean for committee members to agree to more syntax or for users to accept?

Well users will just ignore it if it's not useful, so probably committee members. My impression from a couple of comments here is that the new syntax is already "pushing it". But that's just my impression watching from the sidelines.

https://github.com/tc39/proposal-bind-operator/issues/24#issuecomment-141102543 https://github.com/tc39/proposal-bind-operator/issues/30#issuecomment-173977550

bterlson commented 8 years ago

@bergus The way I see it this isn't adding another form - :: always takes the LHS and passes it to the RHS. How it passes the LHS depends on the presence of the placeholders.

IMO: There is no syntactic space for both pipeline operator and bind operator. There is no syntactic space to use two operators for extraction and pipelining. There is no agreement between this vs. first-param pipelining. Unifying all of these into one syntax, assuming its rational, will drastically increase the chances of it being accepted.

Also, if we introduce special arguments lists with placeholders, shouldn't we look for a more general use case, one that might be better composable with other languge features? Could such a placeholder thing simplify callbacks? Maybe an operator for partial application might solve this whole problem better?

We should /always/ look for a more general use case. That's how I arrived at the OP proposal to begin with :) As to your questions, I don't know, what do you think?

@deontologician I see. I don't think size of syntax is the blocking issue for TC39 (and the addition of ? is minor syntactically IMO). Anyway, let's not worry about that aspect for now, I can manage the committee side of things :-P

bergus commented 8 years ago

@bterlson

The way I see it this isn't adding another form - :: always takes the LHS and passes it to the RHS.

Meh, there is not necessarily a RHS that anything is passed into. A BindExpression doesn't need to part of a CallExpression, it can stand on its own. As currently specced, it's really a bind followed by a call. This is what your proposal would need to change, a BindExpression that is part of PlaceholderCallExpression would need to be evaluated in a completly different way.

[Regarding general use case] I don't know, what do you think?

I'd love to see a partial application operator :-) Admittedly, the one I drafted above would be weird to compose with the bind operator: foo :: (bar(??)) ()bar(foo) so that would either need some special rules (less generality :-/) or a different concept. The problem of combining anything with the bind operator is that one would need to alter the callee, not just the call.

azz commented 8 years ago

Personally not a fan of ? as a placeholder symbol. ? implies a query, or (in TypeScript) optionality.

I think a separate proposal to re-arg/partially apply would be more sensible. Something like:

function foo(a, b, c) { if (a) return b-c }
function bar(f) { return f(3, 4) }
bar(foo@(true, @2, @1)); // --> (4-3) = 1

And use @... to signify variadic args.

calebmer commented 8 years ago

Another consideration (as long as we’re talking about generality) might be the ability to bind arguments. As the bind function not only takes a this argument but other arguments to be applied as well. If there’s a good way to express both argument binding and pipelining in the same syntax, that may be a better sell?

bergus commented 8 years ago

@calebmer We haven't yet used the function::(argument) syntax. That could stand for partial application without this binding, so that you can use it together with the object::function operator to fully simulate the bind method (object::function::(arguments)function.bind(object, arguments)), but that does look weird and still doesn't help a lot with pipelining as it doesn't actually call the function without an extra () call.

bterlson commented 8 years ago

Some good stuff to think about here, thanks!

OP is definitely not very general by necessity - the placeholders are in the argument list is actually part of the bind operator (it's telling the operator what to do with the LHS). In cases without an LHS, placeholders would likely be an error in this strawman.

I am attracted to the idea of a bind operator that composes well with pipelining, but I'm not sure how to do it - I'd still want some control over where my LHS is getting passed (as this or as a formal), and I haven't seen a reasonable syntax for bind yet (one that lets you bind this and any parameter).

Can anyone share prior art on curry/partial application operators?

divmain commented 8 years ago

It strikes me that new syntax may not really be necessary for this, when a helper function could provide the same functionality:

const $ = (fn, ...args) => {
  return function () {
    args = args.map(arg => arg === $ ? this : arg);
    return fn.apply(null, args)
  };
}

Consumed as

foo::$(bar, 1, $)
// vs
foo::bar(1, ?)

It's not quite as clean as the placeholder proposed in this issue, but pretty close.

bterlson commented 8 years ago

@divmain how are you giving special meaning to $ here? foo::$(bar, 1, $) will just pass the value of $ to $, and you can't tell what the caller has called a thing it passed to you (so the check on line 3 won't succeed).

bergus commented 8 years ago

@bterlson It doesn't give a special meaning to the $ identifier, it does give a special meaning to the $ value. Basically the same approach that underscore/lodash use for _ that you can use as a placeholder in _.partial and friends. Of course it's not as clean as a language-level keyword, but works quite well and is easily customisable.

bterlson commented 8 years ago

I see.

Alxandr commented 8 years ago

@divmain you have a slight error in your code, at least with regards to how :: works in babel today.

const $ = function (fn, ...args) {
  args = args.map(arg => arg === $ ? this : arg);
  return fn.apply(null, args)
}

function foo(...args) {
  console.log(args);
}

const three = 3;
three::$(foo, 1, 2, $, 4);

In your case you return a function from $ that never gets invoked.

divmain commented 8 years ago

You're right, of course @Alxandr. Thanks! Serves me right for commenting without testing. In any case, it seems that the bind operator alone, without the proposed parameter substitution syntax, could serve this purpose rather well.

Alxandr commented 8 years ago

What I don't like about this though is that you have to do spread operator and array mapping to make it work. It's not horrible, but I would prefer language support.

divmain commented 8 years ago

That's fair, although the proposed syntax change would de-sugar to something similar (with a Babel transform) until support landed natively. If you want to avoid the spread operator or anything ES6, it could be re-written as:

var $ = function (fn) {
  var args = Array.prototype.slice.call(arguments, 1).map(function (arg) {
    return arg === $ ? this : arg;
  });
  return fn.apply(null, args);
};

I can understand why new syntax is attractive. But a few questions come to mind:

To be clear, I'm less of an opponent to the proposed placeholder syntax, and more a strong proponent of :: in general.

Alxandr commented 8 years ago

I didn't say I preferred no spread because I don't want es6, but because you're forced to allocate a new array of all the arguments, that you then have to sift through for every call, instead of at "compile time".

bakkot commented 8 years ago

Would this allow passing the LHS multiple times, a la x::y(?, ?)? I'd expect it to, but in that case I'd also expect to be able to pass the LHS as this as well as a parameter, which would need some further syntax.