-
Notifications
You must be signed in to change notification settings - Fork 30
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
Comments
You beat me to it...I was about to file this myself. 😉 Pipeline operator
Binding operator
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 // 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) |
Another interesting thing with this syntax in this gist. |
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? |
My thoughts are more-or-less in agreement with what you've presented, with the following caveat: I don't think introducing 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 We also need to have a story for computed property names 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)(); |
@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 W.r.t. removing @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. |
@ssube Also, I'm not feeling the unary |
@zenparsing You could make an argument for The |
@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. |
@zenparsing Oh, I misunderstood that. It's true that |
All 99% of people care about is binding a method to an object in a scoped way, that can be with partials or with All other use cases like binding to |
@ssube It's synonymous with @benjamingr The unary version should probably be put on hold for now. Is that okay, @zenparsing? |
@IMPinball Yep |
And next question: what should the expected behavior of On Fri, Sep 25, 2015, 10:31 zenparsing notifications@github.com wrote:
|
@IMPinball I think that should probably be a syntax error (at least for now). In other words, obj->foo; // Syntax error obj->foo(); // OK In my mind, it's just a different way of calling a function allowing for pleasant chaining. |
That can work. It ride another train. I'm fine with it (it's not a common On Fri, Sep 25, 2015, 16:00 zenparsing notifications@github.com wrote:
|
@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. |
@ssube I don't mean that obj->foo(1, 2, 3);
// Desugars to:
// foo(obj, 1, 2, 3); Clearly the |
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 It's not that I would not like a shorthand for |
It sounds like we've moved from the original post (which I'll leave for posteriority) to something like:
That is, original example 1. ES6 desugar for
ES6 desugar for
Does that accurately represent the current suggestions? Given the discussion, I feel like it's more appropriate to check for |
Object with an internal |
@ssube OK, I didn't really follow the discussion (just read through everything), thanks for dropping the property access thing.
Hm, interesting idea, but I'm not sure what "functors" you're talking about here. Not these I guess? // 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 |
For what it's worth, I've temporarily rescinded the suggestion of the Function chaining:
Method binding:
On Thu, Nov 5, 2015, 16:01 Bergi notifications@github.com wrote:
|
@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. |
@zenparsing It's more about the use of
|
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. |
Edit: I mis-remembered Mori's API...Feel free to That is a great bonus, if you're mostly interfacing with native APIs. You could even do the same with 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 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 // 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)
}))
} |
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. |
Hi, I have another clarification question. What would it mean to write |
@mindeavor No, it would be roughly Syntactically, the call parens can't be a part of the unary bind operand. It gets parsed like this: ( :: (console.log) )(4) |
I see, thank you. When you say "can't", do you mean "not possible", or "chosen not to"? |
"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.
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. |
Brought up in passing in tc39#26.
A concern I have is that the function bind operator does not exhibit all of the functionality of 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 ( |
Maybe we can rename this proposal "this binding operator" so that people stop trying to add this feature to it. |
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. |
@domenic Perhaps. If both this operator and the pipeline operator both go ahead there will be less and less reason to use @zenparsing In looking over the code I just wrote (especially Again just rampantly speculating, and obviously well out of scope for this proposal, but has a My intuition tells me there's a technical reason why such a function doesn't already exist. |
@DerFlatulator: Hmm... That's a really neat idea! |
+1000 on Domenic's proposal of calling it "this binding operator". |
I also agree on the "this binding operator". This proposal has never done On Mon, Jan 4, 2016, 13:16 Benjamin Gruenbaum notifications@github.com
|
@domenic @benjamingr @isiahmeadows Agreed. I've changed the title on the README. I want to preserve the URL, though. |
@zenparsing Last time I checked, when you change a repository name, old links get automatically redirected to the new URL. |
@zenparsing, @DerFlatulator: Yep, it does (just tested it). |
I found similar was the case when I semi-recently changed my username. |
Following up on my post about partials. I was toying around with the idea of 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 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] |
@DerFlatulator
|
The other reason partials have been met with questions is that there already exists Function.prototype.bind, which is technically partial application. |
@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 It would quite beneficial to obtain language support for partial/currying semantics without prematurely binding If you want to further discuss this perhaps it'd be best to open an issue. |
I think this has been fully explored. Thanks! |
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. |
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:
Using something like the rules:
::
) passes it asthis
~>
) passes it as the first argument::
) returns a function...args
and instantiating an instance of the left operand...args
and invoking the operandThat gives us four distinct (useful) expressions and two with very limited usage:
scope :: property
andscope :: [property]
scope :: new
:: property
and:: [property]
:: new
(provided for completeness, but not very useful)target ~> property
(and potentiallytarget ~> [function]
)target ~> new
(provided for completeness, but not very useful)We can very naively implement them as:
With scope and property:
With scope and new:
Without scope, with property:
Without scope, with new:
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:
and
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.
The text was updated successfully, but these errors were encountered: