18:56
<jschoi>

Ah, yeah, one of Ben Lesh’s threads. I mentioned him earlier as someone who was so vehemently against Hack pipes that he seemed to become disillusioned with TC39 more broadly.

He’s…somewhat of an outlier in the intensity of that reaction. I still don’t really understand why he places so much weight on tacitness of the primary argument, relative to linearization, to the point of wanting to shut the whole thing down. And I still haven’t seen a persuasive articulation from him of why tacitness matters that much more than linearization for fluency. I’m a fan of his work, but he genuinely seems not to see much value in linearization on its own, and I still don’t think I’ve seen a clear explanation from him as to why.

That said, it’s certainly possible that others will also feel emotionally ambivalent or opposed to Hack pipes’ explicitness. And that is a reason for some nervousness.
But it’s not as though we don’t also have reasons to be nervous about encouraging developers to switch to call-this and its heretofore-rare this-based standalone functions en masse, either…
No slam dunks here from the whole community’s point of view.

19:02
<jschoi>
Your “how would you phrase [their problem statements]” question is addressed to ljharb, but, for what it’s worth, when I put call-this’s proposal together, I relied on statistical uses of .call, .bind, and .apply from Gzemnid (see issue #12).
I hadn’t seen anyone make the connection between call-this and Reflect.apply until you did. I would love to hear more about how call-this would help Reflect.apply. Maybe it would be worth adding to the explainer.
19:02
<jschoi>
(And, yes, pivoting call-this into “this-sensitive functions attached to prototypes are poorly tree shakeable, so we should encourage standalone this-sensitive functions” is possible…as long as we accept that that might kill pipe operator in the Committee, due to their overlap in the dataflow use case.)
20:02
<jschoi>
Pipes would incentive plain functions at least a little, if not a lot. The magnitude is uncertain, but I think it certainly would be a positive effect (a little or a lot…for me, personally, a lot).
People like writing and reading x.f0(a).f1(b).f2(c), and they don’t like writing or reading f2(f1(f0(x, a), b), c). So they choose to make f0, f1, and f2 prototype methods and not standalone functions.
The same people might like reading x |> f0(##, a) |> f1(##, b) more than f2(f1(f0(x, a), b), c).
Those people might even like reading it enough to choose to make f0, f1, and f2 plain functions instead of prototype methods, where they wouldn’t have done it had it remained a choice between only x.f0(a).f1(b).f2(c), and they don’t like writing or reading f2(f1(f0(x, a), b), c).
But would the difference be enough to cause a significant increase in plain function usage? Nobody can claim to be confident about the whole community; we can only make our best judgements. I certainly think it would make plain function usage nicer.
Of course, like you said, pipe operators have benefits other than incentivizing DCE-friendly plain functions. But Hack pipes do “nudging the ecosystem towards compatible functions”; the debate shouldn’t be about whether they nudge developers toward plain functions at all, but rather about how much they would.
20:03
<Richard Gibson>

I hadn’t seen anyone make the connection between call-this and Reflect.apply until you have, but I would love to hear more about this

Basically, all of https://github.com/tc39/proposal-call-this/blob/main/README.md#:~:text=There%20are%20a%20variety%20of%20reasons%20why%20developers%20use%20.call and half of https://github.com/tc39/proposal-call-this/blob/main/README.md#call-is-clunky are already addressed by Reflect.apply:

-rec.method(arg0)
+const { apply } = Reflect
+apply(method, rec, [arg0])

It specifically remedies the subject–verb separation of .call, and actually does a better job of protecting against monkey-patching and prototype pollution by being immune from subsequent modification of Function.prototype.call itself... the only complaint it doesn't address is subject–verb–object ordering, which is also already possible to handle with helpers like

-apply(method, rec, [arg0])
+const callThis = (rec, fn, ...args) => apply(fn, rec, args)
+callThis(rec, method, arg0)

that's much better than method.call(rec, arg0) but empirically not by enough for widespread replacement of .call, and neither have been good enough to meaningfully encourage exporting collections of receiver-sensitive functions outside of a containing class, which I think is where a reframed call-this proposal has some room to work.

And regardless, I'm pretty much of the opinion that a data-flow perspective on call-this doesn't even make sense. Even the above callThis helper is intersects with pipeline on the same footing as any other function, and is less like to participate in a pipeline than e.g. Object.fromEntries. And while I don't consider it likely that pipeline would significantly encourage libraries to export functions rather than classes (because, as noted above, calling a method on a class instance gets the same pipeline DX benefits as calling a bare function), I do consider it likely that ergonomic call-this syntax would encourage libraries to export methods in addition to their containing classes for the improved tree-shaking

20:04
<jschoi>
I’m a little confused by this. expr |> ##.method() is almost always worse than expr.method(), except in the rare case you would need to parenthesize expr in (expr).method().
20:21
<jschoi>
Thanks, this is helpful.
20:23
<jschoi>

Yes, Reflect.apply does have several advantages over Function.prototype.call, including improving subject–verb separation.
I don’t think that really negates the “.call this is common and clunky” reasoning in call-this’s explainer, especially if it requires “callThis” userland helper functions in order to get SVO word order. It’s not like Reflector.call isn’t also clunky.

The real debate would probably be over if their use cases are compellingly common enough to warrant syntax—as I have tried to argue to the Committee on behalf of Jordan, with not much success.

20:23
<jschoi>
That SVO word order has empirically not been compelling enough to cause widespread use of Reflect.apply-using callThis helper functions—this point is well taken, although I’m sure other factors are involved in that too.
20:25
<jschoi>
Your point on the dataflow overlap between call-this and pipe operator being maybe overblown is also well taken. This was certainly the conclusion drawn in the first place when pipes reached Stage 1 in 2021: Jordan raised concerns that advancing pipe would kill any future bind operator because of overlap, and others responded that pipes are now orthogonal to any bind operator and would not kill it. This was the overlap-minimizing approach I’ve tried to take with both pipe and call-this.
(I haven’t made it a secret that, if I had to choose one, I would rather have pipe than call-this, but I have tried to make the Committee fairly consider call-this, on its own non-dataflow merits, without considering any overlap with pipe operator.)
20:25
<jschoi>
I am confused about the notion that “calling a method on a class instance gets the same pipeline DX benefits as calling a bare function”, but I’d rather discuss that in another thread in which I’ve already asked about this (https://matrix.to/#/!mjlgwjKxWUpgSgeCQU:matrix.org/$uoU7N_56TqMXsv8rCwzYtUnf25u64_8LKbr-z-Zvg9w?via=matrix.org&via=igalia.com&via=mozilla.org).
20:59
<Evan Winslow>
I can't tell exactly what you have in mind here but I want to clarify that export const {method} = C.prototype is not enough to get tree shaking. Such a method is still attached to the class and therefore still subject to polymorphic/reflective use cases which are what make tree shaking unsafe.
21:07
<Evan Winslow>
Also the data flow aspect of call-this comes from being chainable, which is true of syntax but not of your callThis helper. To support arbitrary functions with call this, it doesn't take long to come up with a helper like function to(fn) { return fn(this); }. Which then let's you do obj..to(Object.entries).filter(fn1).map(fn2)..to(Object.fromEntries);
21:29
<Richard Gibson>

export const {method} = C.prototype is not enough to get tree shaking... [method] is still subject to polymorphic/reflective use cases which are what make tree shaking unsafe.

Even if method is imported but C is not? I can believe that tree-shaking does not currently result in omitting unused parts of C in such cases, but updating to do so seems not just tractable, but exactly the kind of ecosystem change that would be expected as a second-order consequence of call-this.

21:41
<Richard Gibson>

obj..to(Object.entries).filter(fn1).map(fn2)..to(Object.fromEntries);

this is definitely true, but seems irrelevant. It's also already possible to get poor-mans-pipelines using helpers with names like flow or chainable, and I don't think introducing yet another approach via call-this further raises the bar for pipeline to justify its proposed syntax (that need exists regardless, although I guess one could make an argument that ..to(fn) is so much better that it does cover the pipeline use cases 🤷)

21:56
<Evan Winslow>
My point was that the spelling of the operator (appears to me) is a non-factor here. So I moved on to secondary considerations.
22:01
<Richard Gibson>
I think ljharb is saying that the spelling is a factor—and if he's not saying that, I'm certainly willing to, specifically because a misspelling of .. as . is still valid syntax with plausibly undetected behavioral differences, as opposed to alternatives. But regardless, moving on to other considerations seems fine.
22:04
<Evan Winslow>
How does one avoid importing C? At some point your program has to instantiate the thing in order to call any methods on it, right?