18:27
<TabAtkins>
Okay, just reminding myself: an "uncurry-this" operator (or unforgeable function) would satisfy ljharb's usecase, right? You'd be able to reliably yank methods off of classes and then later call them with specific objects, just as a normal function taking the object as its first arg. If we can limit it to that one usage, the overlap with pipe disappears, and it instead works nicely with pipe.
18:28
<ljharb>
something that lets me, eg, const { slice } = Array.prototype; slice::(receiver, ...args) or const slice = ::Array.prototype.slice; slice(receiver, ...args) would work
18:28
<TabAtkins>
Yes, exactly.
18:28
<ljharb>
and yes i 100% agree that it works very well in concert with pipe
18:28
<ljharb>
the call form in particular
18:29
<ljharb>
and both would also work well in concert with getIntrinsic
18:51
<TabAtkins>
All right, I've added a nota bene to my summary essay covering this: https://gist.github.com/tabatkins/60d831d3e304e3e7316d473f5c1f269b#nota-bene-reliable-method-calling
18:52
<TabAtkins>
I agree that the call-operator version works better, for several reasons I outline here.
19:06
<jschoi>
If I may try summarizing: between arrayLike |> slice::(##, 1) and arrayLike |> ##::slice(1), you prefer the former, Tab, right?
19:08
<jschoi>
(The former uses the newly proposed “call-on” operator while the latter uses the “bind-this” operator.)
19:15
<TabAtkins>
yes, strongly prefer the former
19:16
<TabAtkins>
It solves the problem just as elegantly, but without the possibility of people writing libraries intentionally aimed at that calling style
19:16
<Ashley Claymore>
I imagine arrayLike |> ##::slice(1) appearing on it's own (not as part of a longer pipe) would be uncommon as one could write arrayLike::slice(1)
19:16
<TabAtkins>
right, exactly
19:17
<jschoi>
Yes, I did arrayLike |> ##::slice(1) only to show parallelism, but it would actually be arrayLike::slice(1).
19:17
<TabAtkins>
also, I think it's better ad-hoc - arrayLike::Array.prototype.slice(1) requires us to be pretty careful with precedence to get right, but Array.prototyype.slice::(arrayLike, 1) is easy
19:18
<TabAtkins>
i think it's pretty easy to explain, too, since it's literally just ".call(), but an operator"
19:18
<jschoi>
I think this is understandable. I can try proposing a rival-rival call-on operator at the next meeting along with Function.pipe.
19:18
<TabAtkins>
Let's get in on that together, and we can rope in Jordan too. ^_^
19:19
<TabAtkins>
i'm thinking over how I might present your diagram for an overview/discussion
19:22
<TabAtkins>
invites sent, lmk if anyone else wants in
19:22
<Ashley Claymore>
i think it's pretty easy to explain, too, since it's literally just ".call(), but an operator"
that seems like a potential weakness? Seems that makes the benefit mostly about protection against Function.prototype.call being patched? Which can be done with Array.prototype.slice |> ReflectApply(##, arrayLike, 1)
19:23
<TabAtkins>
Well it's ".call(), but more convenient".
19:23
<jschoi>
I would argue the primary benefit is that .call is very common and we are shortening a very common function.
19:24
<jschoi>
(I’d also be particularly interested to know how much Richard Gibson feels call-on would overlap with other dataflow proposals.)
19:25
<Ashley Claymore>
call is already quite short and doesn't involve holding shift to type?
19:26
<jschoi>
I guess I do need to work out how things would change with having to use pipe with call-on to get the word-order benefit…
19:26
<TabAtkins>
not if you're worried about patching, which is a big part of this in the first place
19:27
<TabAtkins>
meth |> ReflectApply(##, obj, args) is a lot longer ^_^
19:27
<jschoi>
And also obscures the meaning of the code (important, since this use case actually very commonly occurs).
19:28
<jschoi>
It would probably be obj |> ReflectApply(meth, ##, args), though.
19:29
<jschoi>
Versus obj |> meth::(##, args).
19:29
<TabAtkins>
ah yeah, sure
19:31
<TabAtkins>
I will admit tho, that a major motivation for this particular shape is to solve this use-case without overlapping over dataflow proposals. Between Pipe and PFA, the other bind-this operations can be done reasonably well already; this covers the last significant unhandled use-case (afaict) without stomping on either of those.
19:34
<jschoi>
My argument for bind-this had been that .call’s frequency (as well as, to a lesser extent, .bind’s) is sufficiently high to justify optimizing its word order and brevity with syntax, even in spite of its overlap with the pipe operator, similarly to how Function.pipe is useful in spite of the pipe operator. But if we can optimize .call’s word order and brevity by combining with the pipe instead of overlapping with it, so much the better. The syntax of call-on is simpler, too. And, although call-on does not improve using .bind, .bind’s frequency is not nearly as high as .call’s, so it is less important.
19:36
<TabAtkins>
And when .bind is just used to hard-bind a method to the object it's already sitting on, PFA covers that well on its own. The remaining "hard-bind a method to an unrelated object" is, afaict, a lot less common.
19:36
<Ashley Claymore>
Another potential solution to avoiding Function.prototype.call tampering. Is a build step.
A tool that looks for someMethod.$call(obj, ...args) and transforms it to reflectApply(someMethod, obj, args).
It could install Function.prototype.$call during development to avoid needing to transform during fast dev builds. And only transform for production.
19:36
<ljharb>
TabAtkins: i definitely would prefer something that's ordered like receiver, function, arguments tho
19:37
<ljharb>
build steps don't solve the problem for packages, and it's very dangerous to transpile code you didn't author, so i wouldn't want us to recommend that.
19:37
<jschoi>
The receiver–function–arguments word order is solved (albeit slightly more verbosely) with pipe operator + call-on.
19:37
<TabAtkins>
I understand why that order is appealing, but note that using that order makes it easy to publish modules that are intended to be called with this syntax, promoting ecosystem forking that we don't want.
19:38
<ljharb>
how? fn |> ^::(receiver, ...args) has the same ordering issue as fn::(receiver, ...args)
19:38
<jschoi>
receiver |> fn::(#, ...args) has the desired word order. @ljharb
19:38
<TabAtkins>
Thus the overlap with pipe that makes some committee members uncomfortable, yeah
19:38
<ljharb>
any function that looks at this is intended to be called with this syntax - which includes most builtin methods. no explicit intention beyond that is or should be required.
19:38
<TabAtkins>
Right, publishing a module where free-floating functions are authored to use this is, imo, bad.
19:39
<ljharb>
i'm not sure what you mean, that's already a thing people can (and sometimes do) do
19:39
<TabAtkins>
Who does that today? You'd have to use .call() to invoke the functions.
19:39
<ljharb>
it's not our place to discourage export default function (...args) { this }, that's a normal part of the language
19:39
<ljharb>
yes, that's right, you would
19:39
<ljharb>
and "use .call()" is the thing that needs to be made easier/more robust
19:40
<TabAtkins>
Okay, well that's werid and people can be weird if they want. But that's not something normal libraries do.
19:40
<TabAtkins>
Publishing functions with that signature (a) overlaps with the pipe use-case, and (b) forces an up-front calling-convention decision that is distinct from how every other function in the language is called.
19:41
<jschoi>
topic.fn(arg), topic |> fn(#, arg), topic |> fn::(#, arg).
19:41
<jschoi>
All the same word order.
19:41
<ljharb>
it only forces it if the receiver is required. it's perfectly fine if there's fallback behavior for when this is nullish
19:42
<ljharb>
jschoi: no, in the last two, the function comes first before the topic (because only the word order in that one pipe segment is what i think matters)
19:42
<TabAtkins>
Not as authored, no - that's the point.
19:42
<jschoi>
I’m talking about for each entire expression.
19:42
<TabAtkins>
The code is literally in the same order in all three.
19:42
<ljharb>
the function call only occurs in the one segment
19:43
<ljharb>
a pipeline is not one atomic thing, it's an aggregation of segments
19:43
<TabAtkins>
Yes?
19:43
<jschoi>
Well, it can be viewed either way, right?
19:43
<ljharb>
sure
19:43
<jschoi>
Pipes can be viewed as a rearrangement of word order.
19:43
<ljharb>
but since it can be viewed segment-by-segment, the word order is wrong when viewed that way
19:43
<ljharb>
i'm specifically talking about the ordering of a single function call
19:44
<ljharb>
obj.method(...args) is the order everyone expects
19:44
<ljharb>
fn.call(obj, ...args) is awkward primarily because the order is weird (the robustness angle is separate)
19:44
<ljharb>
which is why obj::fn(...args) is nice
19:45
<ljharb>
because it restores intuitive OOP ordering for a non-OOP usage
19:46
<jschoi>
Well, with functional programming, people do do fn(primaryThingOfInterest) too, but primaryThingOfInterest |> fn(#) is an improvement in word order, too.
19:47
<jschoi>
We could do something like primaryThingOfInterest@@fn() to mean fn(primaryThingOfInterest), which in fact is something that Hax’s extensions can do. The word order is improved in that case also. But |> also takes care of it…if you view a pipe as a single atomic thing. If you don’t, then, well, you need that @@ operator or whatever to improve the word order for non-this-using calls too.
19:48
<ljharb>
to be clear, either way, a syntactic .call does what i want. i just think it's a lost opportunity if we force the ordering to match .call
19:49
<jschoi>
I’m personally fine either way, whether bind-this or call-on. I do think that pipe would allow improving the word order with a call-on operator, but I understand that it might be tough to view a pipe like receiver|>fn::(#) as an atomic thing.
19:50
<ljharb>
it's fine if using pipe offers an improvement, but a proposal for .call needs to be an improvement on its own merits
19:51
<ljharb>
and a proposal that just allows f<?>(o, ...a) vs f.call(o, ...a) isn't likely to be considered worth it by the wider committee
19:51
<jschoi>
That brings up an interesting question, actually.
19:51
<jschoi>
We want proposals to stand on their own, on their own merits.
19:52
<jschoi>
But in this particular case we have had several representatives, like Richard Gibson and Yulia, express concerns about redundancy and overlap between these dataflow proposals. That’s why I created that diagram.
19:52
<jschoi>
These two desires are somewhat in conflict…
19:52
<ljharb>
proposals can stand alone while also interoperating
19:52
<ljharb>
it's a good thing if proposals that hold their own weight, are better when used in concert
19:53
<ljharb>
and any syntactic call will benefit from being used in concert with pipeline. i just doubt a syntactic call will hold its own weight if it persist's .call's broken ordering
19:54
<jschoi>
Yeah. .call is really common, but improving really-common-frequency × brevity+robustness alone (call-on without pipe) might not be compelling enough, compared to improving really-common-frequency × brevity+robustness+word-order (bind-this).
19:55
<TabAtkins>
Yeah, the issue here is that it's not just interoperating (that's what we want) but duplication of significant functionality
19:56
<jschoi>
I think this is a larger question that the plenary might need to discuss on its own: Considering proposals on their own merits—versus avoiding duplication of functionality between proposals—which is the bigger goal?
19:56
<jschoi>
That fundamental question may deserve its own plenary time when we present the diagram of dataflow proposals.
19:57
<jschoi>
After all, this situation is going to happen again in the future someday.
20:02
<ljharb>
i don't see those goals as in conflict here tbh
20:03
<ljharb>
if two proposals stand on their own, then any use cases in one not covered by the other are why the duplication is necessary
20:12
<jschoi>
If I’m understanding correctly, in your opinion, some duplication between two proposals is acceptable, if it occurs due to the proposals having to stand on their own merits.
I do agree: increasing TMTOWTDI for JavaScript is not very desirable but also would not be very terrible.
But I also think that other representatives might not agree. I suppose we will see at plenary.
20:20
<ljharb>
yes, exactly
20:20
<ljharb>
it's great to minimize ways to do things, and i personally prefer one
20:21
<ljharb>
but i don't recall us ever blocking anything because it added a different way to do things - brendan has shut down many potential objections over the years by citing TIMTOWTDI as "the way JS works"
20:32
<Richard Gibson>
I personally find call-on as discussed above (undeniable and concise syntax for invoking a function with specified receiver and arguments as an alternative to the deniable and more verbose fn.call(that, …) and Reflect.apply(fn, that, […])) to be satisfyingly orthogonal to pipeline in particular—they seem to work well both independently and together, with pipeline primarily covering the flow of data between human-relevant (sub)expressions/invocations and call-on putting invocation receiver and arguments on even footing (with an inherently opinionated order, regardless of whether that is receiver…function…args or function…receiver…args).
20:50
<Richard Gibson>
I agree in large part with the latest gist, and really can't thank TabAtkins or jschoi enough for condensing all of this. The questions that remain most interesting to me after digesting those summaries are around PFA, which includes placeholder(s) (potentially interacting with pipeline) and a new invocation-like syntax (potentially interacting with call-on/bind-this/extensions) but provides capabilities that are cumbersome to access and supports patterns that are difficult to accomplish even with the other proposals.
21:34
<TabAtkins>
TIMTOWTDI is a concern that was explicitly raised about pipe and other dataflow proposals, tho. So yes it's an important thing to discuss so we can get an Official Opinion, rather than distinct groups going "oh this isn't big enough to be worth it, we should scope it up" and "oh this is big enough to overlap with the other one, we should scope it down".
22:29
<jschoi>
http://www.xanthir.com/b5Gd0
22:33
<TabAtkins>
(This is just me moving the essay from Gist to my blog)
22:49
<TabAtkins>
All right, started the Call Operator repo https://github.com/tabatkins/proposal-call-operator
22:50
<TabAtkins>
And I realized while writing it that call+PFA reproduces the bind operator, too: meth::~(receiver, arg) == meth.bind(receiver, arg)
23:02
<jschoi>
Feel free to duplicate and modify the text from https://github.com/tc39/proposal-bind-this/blob/main/README.md#bind-and-call-are-very-common into that explainer.
23:03
<jschoi>
I also kind of think we should name it proposal-call-this…or at least something other than proposal-call-operator: https://github.com/tabatkins/proposal-call-operator/issues/1
23:30
<ljharb>
And I realized while writing it that call+PFA reproduces the bind operator, too: meth::~(receiver, arg) == meth.bind(receiver, arg)
if we consider the bind use case important, i do not think it would be prudent to wait for PFA to solve it
23:31
<TabAtkins>
I consider it less important than call, but more importantly I'm just laying out a roadmap of features+intersections that don't have duplications. If it takes a little bit to get there, that's fine to me.
23:49
<Justin Ridgewell>
Tab, I think you're missing the bind-op's ergonomic win in fluent APIs
23:49
<Justin Ridgewell>
Eg, to extend the stdlib (which doesn't exist because of in-fighting)
23:50
<Justin Ridgewell>
Writing array.filter(…)::uniq().find(…) with a pipeline or call-op would suck
23:52
<TabAtkins>
A big part of pipe's argument is that array.filter(...) |> uniq(#).find(...) doesn't suck too badly.
23:53
<TabAtkins>
And it means you don't have an ecosystem split of functions "designed for the operator", which are inconvenient to use in any other way.
23:53
<TabAtkins>
bind-this and F#-pipes both had this exact same issue
23:54
<Justin Ridgewell>
We already have an ecosystem split
23:54
<Justin Ridgewell>
Things that are exist in the stdlib and everything else
23:54
<TabAtkins>
yes, methods vs functions are indeed a split, but one that exists in approximately every language; we might have avoided it with sufficient foresight, but we didn't. That doesn't means introducing a third category is something we should consider acceptable.
23:55
<Justin Ridgewell>
// Bind-op
array
  .filter(...)
  ::uniq()
  .find(...);
  .something();

// Pipeline
array
  .filter(...)
  |> uniq(##)
  |> ##.find(...)
     .something();

// Call-op
array
  .filter(...)
  |> uniq::(##)
  |> ##.find(...)
     .something();
23:55
<Justin Ridgewell>
I don't see why we would ever pursue a call-op if pipeline exists.
23:56
<Justin Ridgewell>
Bind-op actually improves on pipeline for a usecase
23:56
<Justin Ridgewell>
Call-op doesn't.
23:56
<TabAtkins>
Call-this doesn't promote a third ecosystem, which is somewhat the point. ^_^
23:57
<TabAtkins>
It solves one specific case - pulling a method off of a prototype, and using it on a different object.
23:57
<Justin Ridgewell>
That's pipeline and uncurry
23:57
<TabAtkins>
If you tried to publish a library "designed for the call-this operator" it would just be a library of normal functions that take more characters to call.
23:57
<TabAtkins>
Yes, call-this is uncurry, indeed.
23:58
<Justin Ridgewell>
Let me rephrase, it's better as pipeline and uncurry
23:58
<Justin Ridgewell>
It offers no improvement over that.
23:58
<Justin Ridgewell>
Bind-op actually allows for an improvement in fluent APIs.
23:59
<TabAtkins>
Yes, we're shifting the same functionality between different sets of things. Call-this + pipe have the advantage of no overlap, while bind-this + pipe overlap.
23:59
<TabAtkins>
And overlap was an explicit concern of committee members for this entire space at our last meeting.