Closed dead-claudia closed 7 years ago
I'm the champion. : )
There are a couple of reasons that I haven't attempted to advance this proposal with TC39:
First, when I presented this proposal back in March, I received a fair amount of pushback on the following grounds:
this
to support functional programming. ES6 made progress in "taming" the dynamic (and confusing) nature of this
in JS, and the bind proposal would actually make things more confusing.::
operator for "virtual methods" would confuse programmers about property access in general.::
is just kinda weird looking.On balance, TC39 wasn't particularly supportive. We went ahead with getting it into Babel, though, in order to gather some feedback. That feedback has been really useful.
I believe that the next step is to decide whether we would be better off splitting the proposal into separate "method extraction" and "functional pipelining" proposals. I'm cautiously in favor of that.
The second big reason that I haven't advanced this proposal is that I don't think this is the right time for adding syntax to JS, with the big exception of async functions. I think we need to focus on just async functions for ES7/2016/whatever.
Too much syntax too quickly isn't a good thing, especially given the huge amount of syntax added with ES6.
Okay. Understandable.
I'm curious about functional pipelining. How would that work out?
Edit (Aug. 12, 2016): This is merely a strawman proposing a change to the proposal, not the actual proposal itself. Just to clarify.
@zenparsing I do like the idea of separating these two ideas.
First of all, method binding a la Java would look really nice. It could even open up the possibility of a property shorthand.
getBooks().then(JSON.parse).then(books => books.map(book => new Book(book)))
// Simplify constructor call
getBooks().then(JSON.parse).then(books => books.map(new::Book))
// Shorthand binding of book => book.length
books
.then(books => books.filter(compose(::length, atLeast(5))))
.then(books => books.map(new::Book))
Shorthand for book => book[prop]
books.then(books => books.map(::[prop]))
It would be incredibly convenient, and it is also very commonly used. It's also extremely optimizable.
As for a pipelining syntax, I was kinda thinking of something like this:
function *map(f) {
for (const i of this) yield f(i)
}
function* concat() {
for (const i of this) {
if (i[Symbol.iterator]) yield* i
else yield i
}
}
function *flatten() {
yield* this
->map(x => x->flatten())
->concat()
}
And for the dynamic this
problem, it's already come up in TypeScript land because JS already calls methods by delegation (prototypal vs classical), and there's no way to properly type Function.prototype.bind
and friends without it.
class A {
foo() { return "A!" }
bar() { console.log(this.foo()) }
}
class B {
foo() { return "B!" }
}
console.log(new B().bar()) // "B!"
@impinball I think the method being bound should be on the right of the operator, for consistency both internally and with other languages. Type::new or Type::static and instance::method provide a predictable way to reference a call of some sort, even though new is an operator (putting it before the type seems like you're inverting the application). All forms can desugar into simple arrow functions or even just function calls.
@ssube Then how would you do something like Base.new.bind(Base)
? It's not an unheard of name, and it could be the right name for some data types.
@impinball I think the property access should take care of that: instance::[new]. I'm not sure I've seen that in use, but explicitly marking it as the new property should work.
@ssube I like that idea.
On Thu, Sep 17, 2015, 23:57 Sean Sube notifications@github.com wrote:
@impinball https://github.com/impinball I think the property access should take care of that: instance::[new]. I'm not sure I've seen that in use, but explicitly marking it as the new property should work.
— Reply to this email directly or view it on GitHub https://github.com/zenparsing/es-function-bind/issues/24#issuecomment-141336546 .
@shovon A pipelining operator (found in some languages like F# as |>
) would address the "iterlib" use case described in this proposal. It would look like:
function fn(a, b) {
console.log([a, b].join(", "));
}
"hello"->fn("world");
// > "hello, world"
Where the left hand side is used as the first arg in call to the function on the right hand side.
Closing in favor of #26, as the initial questions have been answered.
It seems like this issue isn't really resolved since a bunch of the community want to see it move forward, and it still isn't. Is it possible to reopen this so people have a place to discuss the fact that it's being blocked? (re: https://github.com/zenparsing/es-function-bind/issues/30#issuecomment-173977550) Right now #30 has the most recent discussion but isn't actually about the progress of the proposal
@deontologician Happy to reopen.
I would be willing to present this proposal to the committee again, but would like a co-champion.
Does the co-champion have to be on the committee already?
@deontologician Yes.
Is there a list of people on TC39? I found http://tc39wiki.calculist.org/about/people/ but I'm not sure if it's up to date
I really hope this proposal will be presented again to TC39. It just too useful for things like the 'iterlib'
example, adding custom operators to observables without modifying the prototype (e.g. this ideal world) and so.
I just wrote a couple of helper functions to make selenium tests really easy to write:
// gets the third user, opens menu, clicks delete
await '.users'::eq(2)::get('.menu')::click()::get('.delete')::click();
Every helper function is async
/await
and await
s the previous this
, if it is a Promise. This is super hard to write and use without ::
.
@donaldpipowitch You could just call .then()
and have those functions return callbacks. :P
@ljharb
I would be willing to present this proposal to the committee again, but would like a co-champion.
The first check needs to be unticked in light of the discussion in #35.
The last one needs to be unchecked mainly because of #27 and #36.
@Mouvedia I don't see why the last box had to be unchecked. These challenges have been identified now, haven't they? They don't need to be solved to for advancing to stage 1.
@bergus true, that's one way of going about it.
@Mouvedia To elaborate on that further, it's that those challenges might involve significant changes to the proposal (which are expected of stage 0 and stage 1).
I don't get the point for this proposal
how ::doThing
would be clearer than x=>x.doThing()
? it's slightly shorter, but less readable.
Not completely related but what could be more interesting is a compose operator rather than this
@caub You're wrong in your transform though, at least to my understanding. ::x.doThing
is the same as (...args) => x.doThing(args)
. It allows you to do elm.addEventListener('click', ::this.onClick)
in a class for instance. Another great example is asyncFunc().then(::console.log)
as opposed to what you do today which is asyncFunc().then(result => console.log(result))
.
::console.log
is not necessary on node and recent browsers, (works without .bind(console), try Promise.resolve(1).then(console.log)
)
also it's (...args) => x.doThing(...args)
@Alxandr
While doThing
seems to be an inappropriate identifier example for
this syntax, it has been mentionned by
isiahmeadows previously.
@Alxandr
@caub was referring to my strawman idea, not the proposal's syntax. Sorry for not making that clear enough.
The power of bind operator is unclear until you realize the real use case - method chaining.
How ::doThing would be clearer than x=>x.doThing() It wouldn't, more or less.
But consider the following:
import { pick, take } from "array-utils";
import { filterBy } from "../helpers";
myObj::pick("pets")::filterBy("owner")::take(5)
There are no other ways to do it nicely. The closest option is:
compose(pick("pets"), filterBy("owher"), take(5))(myObj)
And it's ugly, verbose and harder to explain to others. Plus it breaks "object first" semantic.
Syntax matters. The current proposal is so natural to the standard method call, where we have an object and we call a function on it. And, compared to prototype-based approach, it has two major advantages: 1) doesn't pollute the global scope 2) makes it clear when it's an instance function or not. There are different implementations of Extension Methods in different languages, and if we take Kotlin for example, it's totally unclear which function was called, and it's a PITA during the code review process. For instance: https://github.com/JetBrains/kotlin/blob/09e7e0da4dc03afe16acac27d1789f72a911f2a9/libraries/examples/browser-example-with-library/src/main/kotlin/sample/Hello.kt#L15
This code is unreadable until you open IDE because this function is defined as extension method: https://github.com/JetBrains/kotlin/blob/09e7e0da4dc03afe16acac27d1789f72a911f2a9/libraries/examples/kotlin-js-library-example/src/main/kotlin/sample/Main.kt#L11
So... This is all about the syntax, and it must differ.
is myObj::pick("pets")::filterBy("owner")::take(5)
like:
myObj.map(o=>o.pets).filter(o=>o.owner=='owner').slice(0,5)
(or maybe the slice inside the map, I don't understand well filterBy too)
@caub No it's not. Cause you're assuming that myObj
is an Array
(or something else that has .filter
and .map
on it's prototype). The really awesome thing about the bind operator is that it would enable you to do things like this:
document.querySelectorAll('div')
::filter(div => div.offsetHight > 10)
::map(div => div.childNodes[0])
::take(5);
More importantly, you could add "virtual" methods to anything. We're not just talking about arrays here.
fetch('some/url')
::asJson()
::renderAs(MyReactComponent, document.getElementById('root'));
// NodeList.prototype.filter = Array.prototype.filter; // would be cool, but bad to do
const {call: filter} = Array.prototype.filter;
filter(document.querySelectorAll('div'), div => div.offsetHeight > 10)
.map(div => div.firstChild)
.slice(0,5);
const fetchJSON = (url, opts) => fetch(url, opts).then(r=>r.json());
fetchJSON('some/url')
.then(data => ReactDOM.render(React.createElement(MyReactComponent, data), root))
in short, you never need that new operator, the only ossible advantage would be lazyness, static analysis.. but for those simple tasks, not really
@caub you're always making an assumption that target object has this method.
i.e. the result of fetch has .json() method. Okay. Now I want it as protobuf object. Why should I rewrite my chain if I just want to change asJson()
to asProtobuf()
from 3rd party library?
Regarding prototypes - they are global. If one developer in one place with set .prototype.filter
to one method, and another developer to something else - have fun debugging it!
JS world is moving away from the global prototype modifications, and you propose the opposite.
Also, what if querySelectorAll
will return different objects depending on some conditions? So you must know all results returned from it, plus you must assign filter
to every prototype.
Bind operator will take any result, and can do some conditions inside as well, i.e. if list was returned - return filtered list, if it's a single object - check if it matches and return a list with a single item.
By the way, Sergi, that general benefit is known and well discussed. (I also invite you to check out Trine, a utility library built to demonstrate what this can do.)
On Fri, Sep 23, 2016, 03:39 Sergei Egorov notifications@github.com wrote:
@caub https://github.com/caub you're always making an assumption that target object has this method.
i.e. the result of fetch has .json() method. Okay. Now I want it as protobuf object. Why should I rewrite my chain if I just want to change asJson() to asProtobuf() from 3rd party library?
Regarding prototypes - they are global. If one developer in one place with set .prototype.filter to one method, and another developer to something else - have fun debugging it! JS world is moving away from the global prototype modifications, and you propose the opposite.
Also, what if querySelectorAll will return different objects depending on some conditions? So you must know all results returned from it, plus you must assign filter to every prototype.
Bind operator will take any result, and can do some conditions inside as well, i.e. if list was returned - return filtered list, if it's a single object - check if it matches and return a list with a single item.
— You are receiving this because you modified the open/close state. Reply to this email directly, view it on GitHub https://github.com/tc39/proposal-bind-operator/issues/24#issuecomment-249123826, or mute the thread https://github.com/notifications/unsubscribe-auth/AERrBEngDZc73VWwnuc4OU0Ac7aWFX1vks5qs4I7gaJpZM4F-1e1 .
Would it be possible to at least advance the binary form of this operator to stage 1?
jQuery + LoDash as es6 import methods with chaining are a real use case that we'd all really be able to benefit from.
Splitting up the functionality isn't going to work - since use of any form of the operator would need to be consistent with every form of the operator, I think they need to advance together or not at all.
I'm still really super keen to use this syntax... hopefully it can continue to be developed?
I really like the function binding case foo::method(param)
and I think it would be an amazing feature.
If the lambda use-case (i.e. .bar(::method)
) is a source of contention though (to the point of nether getting through), could that not be separated out from the initial proposal?
@MeirionHughes no - see two comments up.
@ljharb I really do not see why this absolutely cannot be split up.
Here's how I see it could be done:
Since both features would need separate implementations, the ::method
syntax can simply be defined as illegal (reserved for future use) in the first proposal, so that a follow-up proposal can be safely made and discussed over the next year or two.
@niieani the semantics of the unary form could absolutely alter the semantics of the binary form - it's entirely possible that advancing just the binary form could forever block shipment of the unary form, whereas coupling both together and deferring only temporarily blocks the binary form.
On July 14 in https://github.com/tc39/proposal-bind-operator/issues/35#issuecomment-232807558, @bterlson said he and @mattpodwysocki would champion this proposal. Has this changed?
@caub You're wrong in your transform though, at least to my understanding. ::x.doThing is the same as (...args) => x.doThing(args). It allows you to do elm.addEventListener('click', ::this.onClick) in a class for instance
You wouldn't want to write that though, as it's then impossible to remove the listener.
Another great example of why we don't need more features leveraging the tricky this
.
I would love |>
though.
People already use Function.prototype.bind
. IMHO, the unary operator
doesn't really add any value because of arrow functions, though. I
seriously hope the binary version makes it, though.
On Mon, Jan 23, 2017, 09:53 AlexGalays notifications@github.com wrote:
"@caub https://github.com/caub You're wrong in your transform though, at least to my understanding. ::x.doThing is the same as (...args) => x.doThing(args). It allows you to do elm.addEventListener('click', ::this.onClick) in a class for instance"
You wouldn't want to write that though, as it's then impossible to remove the listener. Another great example of why we don't need more features leveraging the tricky this. I would love |> though.
— You are receiving this because you modified the open/close state. Reply to this email directly, view it on GitHub https://github.com/tc39/proposal-bind-operator/issues/24#issuecomment-274508273, or mute the thread https://github.com/notifications/unsubscribe-auth/AERrBJ_u1-Yv4-HmgMGBCAFYGLIl-Pocks5rVL8CgaJpZM4F-1e1 .
@isiahmeadows, completely agree!
What can watchers do to move this proposal ahead?
If others are wondering why this proposal is useful, it would add a lot of nice functional syntax making it easier to use functional libraries.
//es6
import { flatten } from 'library';
flatten(array.filter().map())
//es6+addition
import { flatten } from 'library';
array.filter().map()::flatten();
@jackmahoney The pipeline operator (|>
) proposal is made for that use case too. Except it's more functional and explicit as it doesn't bother with the annoying this
.
Binary form also really useful for prollyfills and other experimental features. document::proposedMethodName()
is way better than a broken document.proposedMethodName
The bind syntax would be a stepping stone for JS to eventually have native generic iteration methods. Having to use the Array
iteration methods for other types is currently a significant pain point in the language.
Edit: Keeping this list up to date.
TL;DR: See title.
This already has transpiler support, and has definitely fulfilled at least most of the requirements for Stage 1. These are the ones I can tell are very clearly, unambiguously met (pulled from the process document):
I'm just wondering what's holding this back from Stage 1.