Open dead-claudia opened 7 years ago
@lukescott I strongly disagree with both the idea and syntax, especially when considering the current method call syntax of object.method()
:
object.(func)
does almost look like a typo.object.(foo).(bar)
looks seriously ugly for chaining.In addition, all languages that have similar function call and value syntaxes (s-expression-based languages and most primarily functional languages) conflate function and method application while still keeping the context either adjoined or to the right, leaving no ambiguity:
(method context arg1 arg2)
(send context method arg1 arg2)
func context arg1 arg2
Mod.func context arg1 arg2
context#method arg1 arg2
Let's not confuse people with an ugly syntax.
@isiahmeadows
How do you address that with object::foo()::bar()
it looks like foo
and bar
and members of object
? Other languages that use ::
it would be.
Also, how do you prevent object::foo::bar
? Do you throw an error? If it's an error, shouldn't it be more obvious? For example, object::foo(
is an error, but object::foo
doesn't look like one.
The syntax should be intuitive enough to solve those problems.
@lukescott
How do you address that with
object::foo()::bar()
it looks likefoo
andbar
and members ofobject
? Other languages that use::
it would be.
By explaining object::foo()
as a syntactic form rather than object::foo
as binding this
to object
in foo
, you avoid the issue altogether. If we eventually accept @robotlolita's suggestion, it'll emphasize the intent as a function call-only form even more.
Here's some existing precedent for that:
super(...args)
and the proposed import(name)
are themselves syntactic forms that just happen to look like function calls. You can't do let f = super; f(...args)
or let f = import; f(name)
, for example.foo:bar()
as sugar for foo.bar(foo)
for a long time now, but it is also purely a syntactic form. You can't do local f = foo:bar; f()
, for example.So yes, it's possible to avoid confusion by merely explaining it as syntactic sugar, which is what Lua's community already does for object:foo()
.
@isiahmeadows sorry, I was just replying to @lukescott's comment. I should've been clearer.
(There's a separate proposal for that, though I'm unsure if championed by anyone on TC39, https://github.com/mindeavor/es-explicit-this)
@robotlolita No problem
@isiahmeadows Just saw this post (I was taking a break for a while).
I think you're right.
I'll have to go through the old issues to remind myself of why memoization was rejected.
The prefix form is one of the primary draws; as part of discussions on the pipeline operator, the committee agreed that even if only one of pipeline and bind landed, we’d still need a method extraction operator.
In other words, if the prefix form is dropped, I’m not sure what this operator adds over the pipeline operator.
I'm with @ljharb: given they're now pretty separate, we could go and drop the infix chaining variant instead.
Although...then there's the upcoming partial application strawman floating about (there's a link in the proposals repo, but I don't remember it off-hand), which could make the bind proposal even less relevant.
My thought now, though: if we drop the current infix variant, we could make the binding infix instead. So where currently, ::this.foo
binds this.foo
to this
, we could instead use this::foo
. Note that the current infix binding method is pretty useless, since this::foo
and foo.bind(this)
take roughly the same number of characters. Just an alternative idea in light of the pipeline operator proposal's success.
So where currently, ::this.foo binds this.foo to this, we could instead use this::foo
Yes, this is what I was getting at above - method-binding. It seems pretty intuitive that way given the semantics of ::
in other languages. ::this.foo
is just really weird to look at.
@fatcerberus I'm aware, it just wasn't at the time initially proposed very tractible*, and would've required redesigning the syntax (which was obviously not something anyone was eager to do). And I agree, it's pretty weird and odd to look at.
* It was probably about a year ago when the this::foo
for this.foo.bind(this)
suggestion was first presented. It's not an original idea of mine.
I think what confuses me most about the prefix syntax is that ::foo.bar
looks like a unary operation on foo.bar
but in reality is a binary operation over foo
and bar
. How does that even work out for precedence? I guess you would parse it as a ternary operator with no first operand, which is strange to say the least.
If we can clear up some of the issues with the pipeline operator (multiple arguments and integration with method calls) I would completely be in favor of changing x::y
to do method extraction.
I think what confuses me most about the prefix syntax is that ::foo.bar looks like a unary operation on foo.bar but in reality is a binary operation over foo and bar. How does that even work out for precedence? I guess you would parse it as a ternary operator with no first operand, which is strange to say the least. @fatcerberus
A similar thing could be said about the delete
keyword. delete obj.foo
operates on obj using 'foo' as an argument. This is achieved because obj.foo
returns a reference that has two internal slots: the base (obj
) and the referenced name (foo
).
That way ::foo.bar
is a unary operation on the reference returned by foo.bar
.
@acutmore Yeah, I always found delete
’s semantics odd for that exact reason. Looking at a delete
expression I don’t intuitively expect it to work and always second-guess it.
in my opinion
obj::func
equivalent to func.bind(obj)
obj::func(bar)
equivalent to func.bind(obj, bar)
function pipelining should be written as obj::func(bar)(foo)
and equivalent to func.bind(obj, bar)(foo)
also equivalent to func.call(obj, bar, foo)
。
make obj::func(bar, foo)
equivalent to func.call(obj, foo)
, apply
will fill unhappy
@leemotive So it's just syntax sugar, what's the benefit of using it instead of func.bind(...)
?
Anyway, this proposal seems already dead? What's the future plan, @zenparsing ?
@hax syntax is robust against delete Function.prototype.bind
and similar, which is very critical.
@ljharb Most things in JS can be delete
, eg. delete Object.is
. Will committee consider introduce a is b
operator?
@hax i would love to see that happen, yes. However, the slippery slope argument doesn't invalidate this specific syntactic improvement.
the slippery slope argument
@ljharb Sorry, it's not my intention. I gave this example because past TC39 decisions make me a impression that TC39 would very unlikely to introduce any pure syntax sugar which do not have other significant benefit. And I remember Object.is
vs a is b
operator has been discussed before, so I have a impression that immune from delete Object.is
was not a significant benefit.
I'm wondering if the taste of TC39 has changed now?
@hax one of the interesting properties of syntactic sugar in a dynamic language is that it can open up compiler improvements. So not only can the code be clearer to the reader but also it gives the compiler more information to work with.
For example adding await
to javascript could be seen as just sugar over using .then
but it allowed v8 to produce much clearer call stacks: https://mathiasbynens.be/notes/async-stack-traces
I would imagine this bind syntax might also open the door for some optimisations
@acutmore await
is not a pure sugar as my understanding. For example, it's very troublesome to convert await
to .then
if there is flow control like throw/return/break/continue...
If we plan to repurpose a::b
to method extraction as https://github.com/tc39/proposal-bind-operator/issues/44#issuecomment-359215966 , there is also another problem: we need to support both member access (a.b
) and computed member access (a[b]
).
In the original proposal, we use ::a.b
and ::a[b]
which is consistent, but if change to a::b
and a::[b]
, we will face the same problem like currently controversial syntax issue of optional chaining operators.
For the consistency, we'd better drop ::
syntax. I think we could use a.☐b
and a.[☐b]
which ☐ is a possible char. For example, use &
, it will be a.&b
a[&b]
. (Groovy use a.&b
for similar purpose.)
@hax
In the original proposal, we use ::a.b and ::a[b] which is consistent, but if change to a::b and a::[b], we will face the same problem like currently controversial syntax issue of optional chaining operators.
Great observation! And I agree completely. I would still like to see if we can provide some kind of syntactic support for method extraction, but it will likely need to be a unary operator. We probably don't want to have a sigil inside of the brackets, though.
Maybe just a prefix &
, somehow?
let a = &obj.x;
let b = &obj['x'];
let c = &obj->x; // ;)
I could get behind unary &
. Double-colon ::
as a unary operator puts me off big time since I don't think there's any other mainstream language where the operator works that way. By contrast &
is the addressof operator in at least C/C++ so I think &obj.x
would be pretty nice.
@zenparsing Prefix &
will introduce new ASI problem 😢 Though it seems we will unlikely meet it in current code.
a // <- missing ;
&obj.x // useless now...
But maybe future:
a // <- missing ;
&obj.x |> accept_a_func
@hax Yes, I thought about the ASI issue, but (as with prefix +
and -
) it may be acceptable depending on usage patterns. Semicommon-light linters would have to be upgraded to warn on &
on a newline without a preceding ;
.
@zenparsing Yes, it just add another new token to -+/([
list 😅
Another reason I prefer a.&b
to &a.b
is: when people program, it seems many would realize they need extract the method after they entered a.
so it's a little pain to backwards the cursor.
This is my subjective feeling, but groovy's a.&b
design may reflect my guess 🧐
I even consider postfix operator may be better though there is not much choice of token...
Since the pipeline operator/proposal handles the chaining use-case, what's wrong with obj->x
? It closely resembles the fat arrow =>
syntax, so it could easily be explained that "arrows mean bind".
@lukescott obj->x
seems good, but it also face the inconsistent problem of obj->[x]
, see https://github.com/tc39/proposal-bind-operator/issues/44#issuecomment-372593028
To me, the most important use case of the function pipelining a::b()
are extension methods. In languages like C#, you can define extension methods that look as if they were part of the object itself. This would pair nicely with ES6 modules:
import { distinct, join, sort } from 'linq';
let a = [ 8, 42, 5, 9, 2, 5 ];
a::distinct()
::sort( x => -x )
::join(", ")
|> console.log;
The reason why I prefer the ::
syntax over the ->
operator is that it is similar to the .
operator used to access properties – a::sort()
is almost like a.sort()
. However I agree that the proposal is getting very close to the pipeline proposal and the syntax should probably reflect the similarity.
Therefore I propose changing the operator for “this pipelining” from ::
to .>
, as it's similar to both the pipeline operator |>
and the member access operator .
. Then we should also merge it with the proposal-pipeline-operator and leave this repository for the unary operator only.
On tc39/proposal-pipeline-operator#110 I've done a summary of the similarities and differences between the Bind Operator and the Pipeline Proposal and I found out that the part promoted by @isiahmeadows is really the only part we need. The rest can be quite easily done with the Smart Pipelines. Be sure to check the issue out, as this is IMHO the most probable way of getting the “function pipelining”, as Isiah calls it, to the specs!
IMO, pipeline is different and can't replace bind operator. Check my comment at https://github.com/tc39/proposal-pipeline-operator/issues/101#issuecomment-374513960 .
This proposal maybe refocus to cover #5 mainly, as I think that use case can't be covered by the pipeline operator.
TL; DR: Use bind operator on destructive functions assignment.
function foo({ ::doA, ::doC }) {
doA();
doC();
}
const bar = {
a: 1,
c: 2,
doA() {
console.log(this.a);
},
doC {
console.log(this.c);
}
}
foo(bar);
// Output: (With bind operator)
// 1
// 2
// Output: (Without bind operator)
// undefined
// undefined
I think this would fit nicely between functional programming, and the oop spirit of Javascript.
This currently could be done with something like this
function foo(bar) {
const { doA, doC } = bar;
doA.bind(bar);
doC.bind(bar);
doA();
doC();
}
But it's not practical, as I would need n + 1
lines for every function I would like to do this. This could even be used with renamed assignment
function foo({ ::doA: baz }) {
baz
}
In #5 was a discussion about where the binding should belong, and I think the binding should belong to the destructive property (doA
), as baz
is just a variable assignment.
Don't know what you think about this, or if you just want to let this die completely.
Edit: Here's specifically what I'm proposing:
object::func(foo, bar, baz)
, equivalent tofunc.call(object, foo, bar, baz)
object::func
, equivalent tofunc.bind(object)
::object.func
, equivalent toobject.func.bind(object)
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: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 (usingthis
/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: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.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:
Method chaining will avoid ugly nested calls like this:
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.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
, andforEach
, 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.