Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generalize both binding/selection and pipeline operators #26

Closed
ssube opened this issue Sep 18, 2015 · 69 comments
Closed

Generalize both binding/selection and pipeline operators #26

ssube opened this issue Sep 18, 2015 · 69 comments

Comments

@ssube
Copy link

ssube commented Sep 18, 2015

Based on the recent discussion with @zenparsing and @IMPinball in this thread and issue #25 , I think it might be sanely possible to make the pipeline operator (let's say ~>, for this example) and the binding/selection operator (let's use ::) into two variants of the same general syntax.

Using the example of fetching a list of books, we would write:

getBooks() ~> map(Book::new) ~> forEach(shelf::addBook)
// instead of
forEach(map(getBooks(), data => new Book(data)), book => shelf.addBook(book))

// and
getBooks() ~> map(::author)
// instead of
map(getBooks(), book => book.author)

Using something like the rules:

  • When the left operand is:
    • provided:
      • the selection operator (::) passes it as this
      • the pipeline operator (~>) passes it as the first argument
    • omitted:
      • the selection operator (::) returns a function
        • invoking its normal behavior but
        • taking the scope as the first parameter
      • the pipeline operator throws
  • When the right operand is:
    • "new":
      • both operators return a function taking ...args and instantiating an instance of the left operand
    • callable:
      • both operators return a function taking ...args and invoking the operand
    • otherwise:
      • both operators return a function which returns the value of the operand

That gives us four distinct (useful) expressions and two with very limited usage:

  1. scope :: property and scope :: [property]
  2. scope :: new
  3. :: property and :: [property]
  4. :: new (provided for completeness, but not very useful)
  5. target ~> property (and potentially target ~> [function])
  6. target ~> new (provided for completeness, but not very useful)

We can very naively implement them as:

  1. With scope and property:

    return function(...args) {
      if (scope[property].apply) {
        return scope[property].apply(scope, args);
      } else {
        return scope[property];
      }
    }
    
  2. With scope and new:

    return function (...args) {
      return new scope(...args);
    }
    
  3. Without scope, with property:

    return function(scope, ...args) {
      if (scope[property].apply) {
        return scope[property].apply(scope, args);
      } else {
        return scope[property];
      }
    }
    
  4. Without scope, with new:

    return function (scope, ...args) {
      return new scope(...args);
    }
    

For the pipeline operator, the inner invocation of
scope[property].apply(scope, args)
would be replaced with
scope[property].apply(this, [scope].concat(args))

Breaking down the examples from the start:

getBooks() ~> map(Book::new)
// becomes
(function (left) {
  return map(left, function (...args) {
    return new Book(...args);
  });
})(getBooks())

and

getBooks() ~> map(::author)
// becomes
(function (left) {
  return map(left, function (obj) {
    return obj.author;
  });
})(getBooks())

I'm sure there are some edge cases (or even obvious cases) I'm missing here, but figured I would throw this out for folks to poke holes in.

@dead-claudia
Copy link
Contributor

@ssube

You beat me to it...I was about to file this myself. 😉

Pipeline operator

  • 👎 for the tilde. LiveScript makes frequent use of the tilde for binding this, but it's a little awkward to reach, just below the Esc key on most keyboards and requiring Shift, and gets annoying quick. It's okay for the bitwise negation operator, which doesn't see much use, anyways, but the pipeline operator would be used too often.

    I would prefer the hyphen arrow, obj->func().

  • 👍 👍 for the idea of making it use the first argument instead of the this context. That would make it immediately usable for Underscore, Lodash, and friends, as well as not requiring a dynamic this that would usually be unnamed. Also, when compiled, it doesn't require a lot of boilerplate.

    import {map} from 'underscore'
    
    // Would "just work" now, with transpiler support.
    getBooks()->map(obj => obj.author)

    The above would be this in ES6:

    import {map} from 'underscore'
    
    map(getBooks(), obj => obj.author)

    It would also make it easier to fit into TypeScript, which is still trying to figure out how to fix the this problem (JavaScript is call-by-delegation, but TypeScript doesn't delegate types).

Binding operator

  • I like how it currently is, but let's for consistency's sake, use Class::[new] as you suggested for binding the constructor (i.e. [[Construct]]). It's easier to parse, and gets rid of a weird edge case. new isn't that obscure of a property or method name. It's not like __proto__, which is otherwise a terrible property name to begin with. Also, it's not like two characters would kill anyone. I remember a similar problem had to be solved with arrow functions, to solve the ambiguity problem with objects vs blocks, in which the decision was to require the returned object to be wrapped in parentheses. People complained initially, but they got over it.

  • Another special case: make sure that ::__proto__ is equivalent to obj => obj.__proto__, while ::['proto'] is equivalent to obj => obj['proto']. I know that's a little nit, but it's something to be mindful of.

  • One other thing: ::[new] should be equivalent to C => C::[new], if it exists (which it should for consistency's sake). The reason why I believe this is because C::[new] is effectively C.[[Construct]]. It's also least surprising, as new.target is already bound to C.

    C => C::[new]
    C => C.[[Construct]].bind(C)

FWIW, I see these two features becoming landmark features when it finally stabilizes and more people hear about it. People are going to love the shorthands and pipelining. It would almost render Lodash's _.flow useless. It could also be used without changing the API for current libraries.

// Maybe future JS
import {map, forEach} from 'underscore';

class Foo {
  constructor(value) { /* body */ }
}

const classes = new Set();
const instances = new Set();
const objs = [/* entries */];
objs
  ->map(::foo)
  ->forEach(classes::add)
  ->map(Foo::[new])
  ->forEach(instances::add);

Another thing I like is that with the current state of this idea, types can easily be statically checked.

// TypeScript-like syntax for exposition

type PropGetter<T> = (obj: {prop: T}) => T
::prop

// By sheer coincidence...
type BoundMethod<T, ...US> = (obj: {prop(...args: US): T}) => (...args: US) => T
                           = PropGetter<(...args: US) => T>
obj::prop

// These are identical, and can be type checked as such:
obj->foo()
foo(obj)

@dead-claudia
Copy link
Contributor

Another interesting thing with this syntax in this gist.

@dead-claudia
Copy link
Contributor

And for precedence, if both binding and the pipeline operator can be made with the same precedence as method application, that would probably be best.

@zenparsing @ssube WDYT?

This was referenced Sep 19, 2015
@zenparsing
Copy link
Member

@IMPinball

My thoughts are more-or-less in agreement with what you've presented, with the following caveat:

I don't think introducing new as a special case computed property name thing is going to fly. One day we might get something like Symbol.construct, but that's just speculation. It might be better to punt on the new variant for now.

As far as operator precedence goes, we need to think about how these operators interact with "new".

new C::foo();
  (new C)::foo();  // 1
  new (C::foo)();  // 2
new C->foo();
  (new C)->foo();  // 1
  new (C->foo());  // 2

I would probably argue for 2 in the case of :: and 1 in the case of ->.

We also need to have a story for computed property names in the case of ::, and for more complex expressions in the case of ->.

Do we use square brackets?

obj::[Symbol.iterator];

That makes sense, I suppose.

What about the pipeline operator? Do the square brackets make sense there?

obj->[myLib.func]();

Or would parenthesis be more appropriate?

obj->(myLib.func)();

@ssube
Copy link
Author

ssube commented Sep 21, 2015

@zenparsing Do computed property names as part of an accessor make sense? It's my understanding that computed names were introduced primarily for places where you couldn't precompute the name, like object literals, but it's easy to books -> map(::['bar-' + foo]) and use the existing variable name syntax.

W.r.t. removing ::new, I'm not sure how I feel about that. Being able to reference new with an attached type is awfully convenient, although I would agree that something more future-proof could be helpful.

@IMPinball I agree that the tilde isn't the best choice. Skinny arrows might be better, as they look like the lambda syntax and we are applying a loose function. That's pretty abstract thinking, but they're also easy to type.

@zenparsing
Copy link
Member

@ssube Also, I'm not feeling the unary ::.

@ssube
Copy link
Author

ssube commented Sep 21, 2015

@zenparsing :: may not be the right choice, as it is often seen as a scope resolution operator, showing up in Java 8, C, C++, and some Ruby DSLs (especially Puppet).

You could make an argument for -> as an indirect reference operator (shows up in C and C++), but that leaves us out a pipeline operator. We could use the -> as a unary operator as well: getBooks().then(-> author) doesn't look terrible. I would lean toward something showing less motion, but -> does fit with how JS uses => to represent loose functions and now accessors.

The |> suggestion for pipeline could work and would continue the _> theme of these unusual application operators. So would +>.

@zenparsing
Copy link
Member

@ssube Sorry, I was overly terse there. I meant that I don't see a good justification for syntax supporting those semantics, beyond what can already be done though normal function calling.

getBooks()->map(::author)

// You could just do something like:
getBooks()->mapToProp('author');

// And it's probably clearer what's going on anyway

Syntax proposals work best when they are really tightly focused around compelling use cases.

@ssube
Copy link
Author

ssube commented Sep 21, 2015

@zenparsing Oh, I misunderstood that. It's true that ::property is just shorthand for -> pluck(property), which in turn is shorthand for .map(it => it[property]). While I do really like the idea, there are enough options already that we can probably omit it for now.

@benjamingr
Copy link

All 99% of people care about is binding a method to an object in a scoped way, that can be with partials or with this and people find it immensely useful.

All other use cases like binding to new, unary :: and other stuff are probably not things the proposal should include. I suspect syntax is also not a big deal for people as long as infix is supported.

@dead-claudia
Copy link
Contributor

@ssube It's synonymous with it => it.property. It's not computed.

@benjamingr
That's true except for new (that should be there, because x => new Class(x) isn't that uncommon, and for consistency).

The unary version should probably be put on hold for now. Is that okay, @zenparsing?

@zenparsing
Copy link
Member

@IMPinball Yep

@dead-claudia
Copy link
Contributor

And next question: what should the expected behavior of object->method
(i.e. not a call expression) be? I think method.bind(undefined, object)
could work in this case, but what do you all think?

On Fri, Sep 25, 2015, 10:31 zenparsing [email protected] wrote:

@IMPinball https://github.com/impinball Yep


Reply to this email directly or view it on GitHub
#26 (comment)
.

@zenparsing
Copy link
Member

@IMPinball I think that should probably be a syntax error (at least for now). In other words, -> should only be allowed if the right hand side is followed by an argument list.

obj->foo; // Syntax error
obj->foo(); // OK

In my mind, it's just a different way of calling a function allowing for pleasant chaining.

@dead-claudia
Copy link
Contributor

That can work. It ride another train. I'm fine with it (it's not a common
case). Besides, it's effectively partial application, anyways. (unary
partial application in this case).

On Fri, Sep 25, 2015, 16:00 zenparsing [email protected] wrote:

@IMPinball https://github.com/impinball I think that should probably be
a syntax error (at least for now). In other words, -> should only be
allowed if the right hand side is followed by an argument list.

obj->foo; // Syntax error

obj->foo(); // OK

In my mind, it's just a different way of calling a function allowing for
pleasant chaining.


Reply to this email directly or view it on GitHub
#26 (comment)
.

@ssube
Copy link
Author

ssube commented Sep 26, 2015

@zenparsing I actually think it would make a lot more sense, looking from the desired behavior, to define both operators as returning functions which can then be called normally. Defining them as a type of call seems more complicated on the standardization side (when have we introduced a new type of call?), where as leaving them as binary operators that return a function is very simple behavior, but also allows a lot more flexibility. This is especially important for the binding operator, which loses much of its power if you can't assign the results.

@zenparsing
Copy link
Member

@ssube I don't mean that -> would introduce a new kind of call. I mean that it would just be pure sugar:

obj->foo(1, 2, 3);
// Desugars to:
// foo(obj, 1, 2, 3);

Clearly the :: operator would evaluate to a new function, though.

@bergus
Copy link

bergus commented Nov 5, 2015

When the right operand is:
callable, both operators return a function taking ...args and invoking the operand
otherwise, both operators return a function which returns the value of the operand

Please don't do this. Doing completely different things depending on the type of the argument is error-prone. If the property doesn't resolve to a method, just throw a TypeError, like every call on something non-callable does.

It's not that I would not like a shorthand for _=>_.property (which is already pretty short), but please don't mix this with the method binding operator.

@ssube
Copy link
Author

ssube commented Nov 5, 2015

It sounds like we've moved from the original post (which I'll leave for posteriority) to something like:

  • When the left operand is:
    • provided:
      • the selection operator (::) passes it as this
      • the pipeline operator (->) passes it as the first argument
    • otherwise:
      • throws
  • When the right operand is:
    • callable:
      • both operators return a function taking ...args and invoking the operand
    • otherwise:
      • throws

That is, original example 1.

ES6 desugar for instance::method:

return instance.method.bind(instance);

ES6 desugar for instance->method:

return function(...args) {
  return instance.method.apply(this, [instance].concat(args));
}

Does that accurately represent the current suggestions?

Given the discussion, I feel like it's more appropriate to check for bind and apply rather than just typeof instance.method === 'Function', if that's possible (stick with duck typing). It would allow these to interact with functors much better.

@jasmith79
Copy link

Object with an internal [[Call]] (i.e. an actual Function object) property would be the obvious choice... not sure if its the best one. Leaving the door open to objects that implement .bind or .apply is more flexible.

@bergus
Copy link

bergus commented Nov 5, 2015

@ssube OK, I didn't really follow the discussion (just read through everything), thanks for dropping the property access thing.
As I just commented on #19, I don't like passing as the first operand, but lets discuss this over there. (I would like infix :: for extraction and -> for binding as separate operators though).
And I think you want instance -> method to desugar to method.apply…, not instance.method.apply…. Typo?

I feel like it's more appropriate to check for bind and apply, It would allow these to interact with duck-type functors much better.

Hm, interesting idea, but I'm not sure what "functors" you're talking about here. Not these I guess?
Also there's a potential hazard when using the operator on callables that don't inherit from Function.prototype (think console.log in old IE), which I guess is still relvant for transpilation usage.
I would have expected that the "desugaring" is only as a simple equivalence showcase, and that the real spec refers to the builtin bind and apply methods - so more like

// extraction
var method = instance[property]
return %bind(method, instance);

// binding (virtual method)
return %bind(function, instance);

// partial (virtual function)
if (! %isCallable(function)) throw new TypeError();
return function(…arglist) {
    return %apply(function, this, %cons(instance, arglist));
};

Using the builtin bind method would also have the advantage that it throws the error on non-callables for us automaticallly.
Also I would expect that the instance :: function() syntax should be optimisable by the engine into %call(function, instance, …) so that it does not require an actual invocation of bind with an actual creation of a bound function. I think specifiying a check for .bind/.apply methods (why not only apply, btw?) would complicate this optimistion - I could be wrong on that though.

@dead-claudia
Copy link
Contributor

For what it's worth, I've temporarily rescinded the suggestion of the
omitted left operand. Here's the status of this discussion to my
understanding (the proposal doesn't reflect this yet):

Function chaining: obj->func

  • Left operand is required and must be an expression.
  • Right operand is required and must be a call expression.
  • The expression is rewritten internally from host->func(...args) to
    func(host, ...args)
  • It has next lower precedence than property access. As in
    host->ns.func() means `host->(ns.func)()

Method binding:

  • Left operand is required and must be an expression.
  • Right hand is required and must be either a valid identifier, valid
    computed bracketed property access (this::[method], like this[method]),
    or explicitly [new] to bind the [[Construct]] function, like in
    Class::[new].
  • It's effectively equivalent to binding a function to that instance. For
    example, this::method is equivalent to %FunctionBind(this.method, this)
  • It binds at equal precedence to property access. obj.prop::foo is like
    %FunctionBind(obj.prop.foo, obj.prop), and obj::prop.call is like
    %FunctionBind(obj.prop, obj).call.

On Thu, Nov 5, 2015, 16:01 Bergi [email protected] wrote:

@ssube https://github.com/ssube OK, I didn't really follow the
discussion (just read through everything), thanks for dropping the property
access thing.
As I just commented on #19
#19, I don't like
passing as the first operand, but lets discuss this over there. (I would
like infix :: for extraction and -> for binding as separate operators
though).
And I think you want instance -> method to desugar to method.apply…, not
instance.method.apply…. Typo?

I feel like it's more appropriate to check for bind and apply, It would
allow these to interact with duck-type functors much better.

Hm, interesting idea, but I'm not sure what "functors" you're talking
about here. Not these
https://github.com/fantasyland/fantasy-land#functor I guess?
Also there's a potential hazard when using the operator on callables that
don't inherit from Function.prototype (think console.log in old IE),
which I guess is still relvant for transpilation usage.
I would have expected that the "desugaring" is only as a simple
equivalence showcase, and that the real spec refers to the builtin bind
and apply methods - so more like

// extractionvar method = instance[property]return %bind(method, instance);
// binding (virtual method)return %bind(function, instance);
// partial (virtual function)if (! %isCallable(function)) throw new TypeError(…);return function(…arglist) {
return %apply(function, this, %cons(instance, arglist));
};

Using the builtin bind method
http://www.ecma-international.org/ecma-262/6.0/#sec-function.prototype.bind
would also have the advantage that it throws the error on non-callables for
us automaticallly.
Also I would expect that the instance :: function() syntax should be
optimisable by the engine into %call(function, instance, …) so that it
does not require an actual invocation of bind with an actual creation of
a bound function. I think specifiying a check for .bind/.apply methods
(why not only apply, btw?) would complicate this optimistion - I could be
wrong on that though.


Reply to this email directly or view it on GitHub
#26 (comment)
.

@zenparsing
Copy link
Member

@IMPinball Right. That's the basic idea behind the two-operator counter-proposal.

I'm still on the fence about whether this is actually any better than the original proposal. The original proposal was very simple and elegant. In any case, I'll try to get a feel from the committee members on which alternative will have a better chance of advancing later this month.

@dead-claudia
Copy link
Contributor

@zenparsing It's more about the use of this. Whether we should bind the argument to this or not. Which would you prefer?

  • Option 1

    function concat(xss) {
      return [].concat(...xss)
    }
    
    function map(xs, f) {
      return xs.map(f)
    }
    
    function reduce(xs, f, start) {
      return xs.reduce(start)
    }
    
    function tap(xs, f) {
      xs.forEach(f)
      return xs
    }
    
    const foo = {
      index: 0,
      inc() { this.index++ },
    }
    
    list
      ->map(x => [x * 3, x / 3])
      ->tap(foo::inc)
      ->concat()
      ->reduce((a, b) => a + b, 0)
  • Option 2

    function concat() {
      return [].concat(...this)
    }
    
    function map(f) {
      return this.map(f)
    }
    
    function reduce(f, start) {
      return this.reduce(start)
    }
    
    function tap(f) {
      this.forEach(f)
      return this
    }
    
    const foo = {
      index: 0,
      inc() { this.index++ },
    }
    
    list
      ::map(x => [x * 3, x / 3])
      ::tap(::foo.inc)
      ::concat()
      ::reduce((a, b) => a + b, 0)

@bergus
Copy link

bergus commented Nov 7, 2015

I'm much in favour of option 2. Especially because you can do

const {map, reduce} = Array.prototype;

and be done. I would expect this to work (i.e., be useful) on many other classes as well.

@dead-claudia
Copy link
Contributor

@bergus

Edit: I mis-remembered Mori's API...Feel free to s/Mori/Lodash/g and s/m/_/g throughout.*

That is a great bonus, if you're mostly interfacing with native APIs. You could even do the same with hasOwn, hasOwn.call(obj, prop)obj::hasOwn(prop).

The bonus for option 1 is for libraries like Lodash, Underscore, and especially Mori. I do feel it's more ergonomic to use Option 1, since you can also use arrow functions to create the helpers, and they don't bind this. It's still easy to wrap either one, though.

To wrap natives for Option 1:

const wrap = Function.bind.bind(Function.call)
const wrap = f => (inst, ...rest) => Reflect.apply(f, inst, rest)

To wrap third party libraries for Option 2:

const wrap = f => function (...args) { return f(this, ...args) }

If you want an automagical wrapper, you can always use a very simple Proxy:

function use(host, ...methods) {
  const memo = {}
  return new Proxy(host, {
    get(target, prop) {
      if ({}.hasOwnProperty(memo, prop)) return memo[prop]
      return memo[prop] = wrap(host[prop])
    }
  })
}

Example with Option 1 + wrapper:

const m = mori

m.list(2,3)
  ->m.conj(1)
  ->m.equals(m.list(1, 2, 3))

m.vector(1, 2)
  ->m.conj(3)
  ->m.equals(m.vector(1, 2, 3))

m.hashMap("foo", 1)
  ->m.conj(m.vector("bar", 2))
  ->m.equals(m.hashMap("foo", 1, "bar", 2))

m.set(["cat", "bird", "dog"])
  ->m.conj("zebra")
  ->m.equals(m.set("cat", "bird", "dog", "zebra"))

// Using Array methods on utilities
const {map, filter, forEach, slice: toList} = use(Array.prototype)
const tap = (xs, f) => (forEach(xs, f), xs)

document.getElementsByClassName("disabled")
  ->map(x => +x.value)
  ->filter(x => x % 2 === 0)
  ->tap(this::alert)
  ->map(x => x * x)
  ->toList()

// Same example with Option 2 + wrapper:

const m = mori
const {conj, equals} = wrap(m)

m.list(2,3)
  ::conj(1)
  ::equals(list(1, 2, 3))

m.vector(1, 2)
  ::conj(3)
  ::equals(m.vector(1, 2, 3))

m.hashMap("foo", 1)
  ::conj(m.vector("bar", 2))
  ::equals(m.hashMap("foo", 1, "bar", 2))

m.set(["cat", "bird", "dog"])
  ::conj("zebra")
  ::equals(m.set("cat", "bird", "dog", "zebra"))

// Using Array methods on utilities
const {map, filter, forEach, slice: toList} = Array.prototype
function tap(f) { this::forEach(f); return this }

document.getElementsByClassName("disabled")
  ::map(x => +x.value)
  ::filter(x => x % 2 === 0)
  ::tap(::this.alert)
  ::map(x => x * x)
  ::toList()

Also, Lodash, Underscore, and Mori have already implemented helpers that Option 1 basically negates. Lodash has _.flow which is the composition operator in reverse, and Mori has mori.pipeline. Underscore also has function composition. Option 1 integrates better with existing libraries, IMHO.

// Mori
m.equals(
  m.pipeline(
    m.vector(1,2,3),
    m.curry(m.conj, 4),
    m.curry(m.conj, 5)),
  m.vector(1, 2, 3, 4, 5))

// Option 1
m.equals(
  m.vector(1,2,3)
  ->m.conj(4)
  ->m.conj(5),
  m.vector(1, 2, 3, 4, 5))

// Option 2
const conj = wrap(m.conj)

m.equals(
  m.vector(1,2,3)
  ::conj(4)
  ::conj(5),
  m.vector(1, 2, 3, 4, 5))

Well...in terms of simple wrappers, Node-style to Promise callbacks aren't hard to similarly wrap, either. I've used this plenty of times to use ES6 Promises instead of pulling in a new dependency.

// Unbound functions
function pcall(f, ...args) {
  return new Promise((resolve, reject) => f(...args, (err, ...rest) => {
    if (err != null) return reject(err)
    if (rest.length <= 1) return resolve(rest[0])
    return resolve(rest)
  }))
}

// Bound functions
function pbind(f, inst, ...args) {
  return new Promise((resolve, reject) => f.call(inst, ...args, (err, ...rest) => {
    if (err != null) return reject(err)
    if (rest.length <= 1) return resolve(rest[0])
    return resolve(rest)
  }))
}

// General wrapper
const pwrap = (f, inst = undefined) => (...args) => {
  return new Promise((resolve, reject) => f.call(inst, ...args, (err, ...rest) => {
    if (err != null) return reject(err)
    if (rest.length <= 1) return resolve(rest[0])
    return resolve(rest)
  }))
}

@bergus
Copy link

bergus commented Nov 8, 2015

Option 1 integrates better with existing libraries

Yes, I can see that. However I think when we are proposing new syntax for the language, we should do _the right thing_™ (whatever that is) rather than limiting us to the aspect of usefulness for code that was written without such capability. Ideally we want new patterns to emerge that bring the language forward, towards a more consistent/efficient/optimal language.
I don't know whether Option 2 is better than Option 1 (who does?), but if it is we should go for it despite Option 1 being closer to existing patterns.

@gilbert
Copy link

gilbert commented Dec 10, 2015

Hi, I have another clarification question. What would it mean to write ::console.log(4) ? Would that be equivalent to console.log.bind(console, 4) ?

@zenparsing
Copy link
Member

@mindeavor No, it would be roughly console.log.bind(console)(4).

Syntactically, the call parens can't be a part of the unary bind operand. It gets parsed like this:

( :: (console.log) )(4)

@gilbert
Copy link

gilbert commented Dec 10, 2015

I see, thank you. When you say "can't", do you mean "not possible", or "chosen not to"?

@zenparsing
Copy link
Member

"Can't" meaning can't with the proposed grammar.

It's technically possible, of course, but would be at odds with the parsing of the binary form.

obj :: fn () is parsed like ( obj :: fn ) ()

And it would also be surprising in the sense that argument parens "mean": invoke the evaluated thing to the left with an appropriate receiver and these arguments.

dead-claudia pushed a commit to dead-claudia/es-function-bind that referenced this issue Dec 14, 2015
@azz
Copy link

azz commented Jan 4, 2016

A concern I have is that the function bind operator does not exhibit all of the functionality of Function.bind, namely partial application. Since there is an infix (between object and function), and prefix (before function) variant, would it be within the realm of possibility to have a postfix version that permits partial application?

A quickly thrown together example of what I'm thinking:

function clamp(lower, upper, value) {
  if (value < lower) return lower;
  if (value > upper) return upper;
  return value;
}

const zeroToOne = clamp::(0, 1); // Sugar for clamp.bind(this, 0, 1)

zeroToOne(1.1) //=> 1;

[-0.5, 0, 0.5, 1, 1.5].map(zeroToOne); //= [0, 0, 0.5, 1, 1]

Combining with other proposed features.

function createDelta(key, delta) {
  return { ...this, [key]: this[key] + delta };
}

const origin = { x: 0, y: 0 };
const [offsetX, offsetY] = ['x', 'y']
    -> map(key => origin->createDelta::(key)); // createDelta.bind(origin, key)

const point = offsetX(10) -> offsetY(-10);

I know this is a long shot, in fact most likely it is impossible to implement into the grammar, but to me it feels odd that this (::) operator cannot currently bind arguments as well as this.

@domenic
Copy link
Member

domenic commented Jan 4, 2016

Maybe we can rename this proposal "this binding operator" so that people stop trying to add this feature to it.

@zenparsing
Copy link
Member

Yeah, that might help : )

@DerFlatulator Thanks for posting. The idea that this proposal should incorporate a general parameter binding mechanism (and how that might be accomplished) has been explored quite a bit in the various issue threads. Ultimately, though, it's out of scope for this particular proposal.

@azz
Copy link

azz commented Jan 4, 2016

@domenic Perhaps. If both this operator and the pipeline operator both go ahead there will be less and less reason to use Function.bind. The only use case I can think of is partial application.

@zenparsing In looking over the code I just wrote (especially origin->createDelta::(key)), honestly I find createDelta.bind(origin, key) much easier to read. Perhaps there is a place for bind after all.

Again just rampantly speculating, and obviously well out of scope for this proposal, but has a Function.prototype.partial function been considered? (i.e. the same as bind but without changing the this binding.) This would make statements like ::fn.partial(arg) (de-sugaring to fn.bind(this, arg)) possible.

My intuition tells me there's a technical reason why such a function doesn't already exist.

@ariporad
Copy link

ariporad commented Jan 4, 2016

@DerFlatulator: Hmm... That's a really neat idea!

@benjamingr
Copy link

+1000 on Domenic's proposal of calling it "this binding operator".

@dead-claudia
Copy link
Contributor

I also agree on the "this binding operator". This proposal has never done
anything with partial application.

On Mon, Jan 4, 2016, 13:16 Benjamin Gruenbaum [email protected]
wrote:

+1000 on Domenic's proposal of calling it "this binding operator".


Reply to this email directly or view it on GitHub
#26 (comment)
.

@zenparsing
Copy link
Member

@domenic @benjamingr @isiahmeadows Agreed. I've changed the title on the README. I want to preserve the URL, though.

@azz
Copy link

azz commented Jan 5, 2016

@zenparsing Last time I checked, when you change a repository name, old links get automatically redirected to the new URL.

@ariporad
Copy link

ariporad commented Jan 6, 2016

@zenparsing, @DerFlatulator: Yep, it does (just tested it).

@dead-claudia
Copy link
Contributor

I found similar was the case when I semi-recently changed my username.

@azz
Copy link

azz commented Jan 6, 2016

Following up on my post about partials.

I was toying around with the idea of Function.prototype.partial to complement this operator, and I ended up developing a polyfill for it and for Function.prototype.curry. They both seem to well complement the existing Function.prototype functions.

Here's the polyfill and rough proposal: https://github.com/DerFlatulator/es-function-partial-curry

The ability to easily create partials and later bind them to objects is a powerful concept, and works hand in hand with the :: operator.

What is the likelihood of getting something like this to TC39's attention?

[Sorry for further derailing this issue thread, but it didn't feel right to create another issue]

@dead-claudia
Copy link
Contributor

@DerFlatulator

  1. Suggestion with your prolyfill: it'll be a fraction of the size if you use ES5 instead of Babel.
  2. That's been brought up several times in es-discuss. And each time, significant concerns about performance came up. Closures aren't cheap. And a performant curry and partial implementation is not easy. Consider Lodash's combined implementation of _.bind, _.curry, _.partial, _.ary, etc.. It's a complicated mess that relies on bit masks with associated closures. And it's still not super fast, despite being significantly faster than the language equivalent in every engine.

@dead-claudia
Copy link
Contributor

The other reason partials have been met with questions is that there already exists Function.prototype.bind, which is technically partial application.

@azz
Copy link

azz commented Jan 8, 2016

@isiahmeadows Thanks for the comments. I know closures aren't cheap, but sometimes expressibility trumps performance.

I was considering the possibility of implementing partials and currying with only one closure, no matter how deep the currying or .partial().partial()... goes. Instead of wrapping each successive call in a new closure you could push arguments to a special (tree-like?) object at this[Symbol.for("partial")] and the closure would take those to arguments upon [[Call]]. Engines would (potentially) optimize this behaviour (.partial and .curry could well be native code), though that's beyond my knowledge.

It would quite beneficial to obtain language support for partial/currying semantics without prematurely binding this to a function.

If you want to further discuss this perhaps it'd be best to open an issue.

@zenparsing
Copy link
Member

I think this has been fully explored. Thanks!

@dead-claudia
Copy link
Contributor

Note: for future readers, @zenparsing's comment does not imply the operator is itself set in stone. It only implies the options themselves have been fully explored.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests