2026-03-12 [09:58:10.0185] We had a brief conversation at plenary during Evan Winslow’s timely presentation about prioritizing bad dead code / good tree shakeability as a language-level design problem. [09:58:17.0872] Chengzhong Wu brought up extension syntax for `this`-based standalone functions. [09:58:31.0406] I also brought up pipe operator vs. tree-shakeable methods. [09:58:46.0552] Evan Winslow said that he would like to talk with me more about how he sees pipe, etc. fitting in with his vision of improving dead code at the system level, so I’m inviting him to do so here. [09:59:02.0053] (hax (HE Shi-Jun) also responded at this time that F#-style pipe operator wouldn’t help with fluent chaining, which I didn’t quite understand—he might be referring to how F# pipes aren’t tacit. But, even if not tacit, at least F# improves the linearity by creating linear chains, which is probably the most important part of fluent chaining?) [09:59:35.0496] Steve Hicks also suggested that the pipe operator just overcome its bikeshedding and just pick an option to help the dead-code problem. And I am all for just ending the bikeshedding, picking something, and presenting it. (I think maybe `##` has the most support?) [09:59:45.0763] F# pipes aren't tacit? Or do you mean hack pipes? [09:59:48.0402] In addition to the bikeshedding, maybe the biggest barriers of for pipe operator was the idea that simple convenience syntax proposals didn’t carry their implementation weight (e.g., “pipe should belong in the future JSSugar layer”). But, if we’re making dead-code elimination a language priority, maybe this proposal could overcome that barrier. [09:59:53.0165] Whoops, yes. [10:00:01.0109] * (hax (HE Shi-Jun) also responded at this time that Hack-style pipe operator wouldn’t help with fluent chaining, which I didn’t quite understand—he might be referring to how Hack pipes aren’t tacit. But, even if not tacit, at least F# improves the linearity by creating linear chains, which is probably the most important part of fluent chaining?) [10:00:03.0406] It’s been a while… [10:00:09.0983] * It’s been a while…I fixed it. [10:56:27.0200] I interpreted hax' comment as "pipes aren't close enough to methods to move the needle. They are more attractive to existing function users, not existing class users". I would share that concern. [10:56:44.0128] * I interpreted hax' comment as "pipes aren't close enough to methods to move the needle. They are more attractive to existing function users, not existing class users". I would share that concern... [11:10:48.0227] fwiw I'm not sure we have the concept of "language priorities". The committee is diverse and represent different stakeholders. [11:11:10.0337] i.e. I don't think this is any different to agreeing in Stage 1 that the problem is something we want to address [11:25:47.0132] Ashley Claymore: sounds like cross-cutting concerns that might affect many proposals are just up to individual members to advocate for? Is there a canonical list of goals/considerations that delegates typically bring up? [11:26:16.0363] that is not a list I have seen. Though one does live rent free in my head :D [11:29:02.0452] > "cross-cutting concerns that might affect many proposals are just up to individual members to advocate for?" That matches my feeling. A champion group advocates for something and the committee agrees and doesn't object [11:29:17.0540] and that goes into the notes for that meeting [11:45:41.0047] * (hax (HE Shi-Jun) also responded at this time that Hack-style pipe operator wouldn’t help with fluent chaining, which I didn’t quite understand—he might be referring to how Hack pipes aren’t tacit. But, even if not tacit, Hack pipes still improve the linearity by creating linear chains, which is probably the most important part of fluent chaining?) [11:47:02.0052] > <@ewinslow:matrix.org> Ashley Claymore: sounds like cross-cutting concerns that might affect many proposals are just up to individual members to advocate for? Is there a canonical list of goals/considerations that delegates typically bring up? There was https://github.com/codehag/documenting-invariants, but that’s been dormant for a long time. [12:03:28.0350] yep. though invariants are only the strictest form of 'priority' [12:03:36.0224] these are the things that won't change [12:03:41.0822] not what proposals we want to see [12:04:07.0155] * not what problems we want to see solutions for [12:04:12.0531] * not which problems we want to see solutions for [12:05:13.0682] She also was going for “Design Principles” too—there’s a section in the explainer for those—but, yeah. [12:07:36.0579] * She also was going for “Design Principles” too—there’s a section in the explainer for those—but, yeah, you’re right. [12:23:00.0816] Regarding how pipes "are more attractive to existing function users, not existing class users", it's true that pipes address a different style of dataflow. This is something that’s come up a lot: TC39 has been considering three* styles of “dataflow” and procedural programming. We have many JS APIs that use classes or prototype methods. And we have many JS APIs that use “static”/standalone functions. (And there are also standalone `this`-using functions, which are currently relatively rare but which the call-this or extensions proposal would encourage. Jordan H. is passionate about this style.) JavaScript has long let that three-way style divergence out of the bag. So a perennial question we have is to what extent we should add syntax that encourages one style over others (There’s Only One [encouraged] Way to Do It, or TOOWTTI)—or add syntaxes that improve two or all three styles, perhaps with some overlap (There Is More Than One Way to Do It). If we want to improve tree-shaking with syntax, but we have a limited budget of new syntaxes, then we still face this choice between TOOWTTI and TIMTOWDI (and, if the latter, *which* ways?). But the fact remains that all approaches are already used in the wild. With that said, we’ve already seen at least one big API (Firebase) that switched from prototype methods to standalone functions—explicitly due to tree shakability. So there may already be an ongoing trend there migrating from one approach to another. [12:23:25.0155] * Actually, there is even a fourth style, tacit/point-free functional programming, which I happen to be a fan of. Tacit FP is what F# pipes, partial function application syntax, and Function.pipe would have encouraged. But several engine implementors have pushed strongly back on it due to concerns about hot-path function allocation, GC, and optimization complexity. This is an example of TC39 (or at least several very important members of it) actually *discouraging* one of the styles. We still have three styles left to consider. And the pipe operator works with higher-order functions, if you’re willing to attach an explicit call `(##)` to your HOF… [12:24:50.0424] Anyways, back in 2022, we had a series of articles about these different styles (list of links in ). No strong consensus was reached from these 2022 discussions other than: * “Some overlap is okay, but too much is bad; we have to decide this on a case-by-case basis.” * Mark Miller not wanting multiple new dataflow syntaxes. * Jordan H. reaffirming that he will block pipe if call-this does not also advance (not sure how he feels now). * And extensions and call-this being mutually exclusive. (I know that Hax continues to champion extensions, but I’m not sure if extensions have garnered further support since 2022. I’m also not sure what would happen to Jordan’s conditional blocking of pipelines if I try to present call-this but fail to advance it again.) [12:25:22.0139] oh noes, what happened to jschoi.org? [12:25:47.0577] Syntaxes for writing tree-shakeable methods would kind of fall in this overlap too, since it’s essentially an enhancement of the `.` member access syntax. [12:25:59.0869] I continue to hope that syntaxes enhancing all styles can be considered for implementation, in the name of encouraging tree-shakable code. For example, I think that syntaxes for writing tree-shakeable methods can easily coexist with a pipe operator; they address two of the three different styles. [12:26:39.0758] I used a nice Node-based host called Glitch.com, which got acquihired by Fastly and then killed. I’ve been too busy to update it due to research; I really should, though… [12:40:10.0672] Also Evan Winslow, I literally just saw your Discourse post from January on the "reshaping extensions" thread: https://es.discourse.group/t/reshaping-the-extensions-proposal/2439/24. Big foreshadowing to your talk today… [12:44:44.0581] ^_^ yup. I basically fleshed out that post and turned it into this talk [12:47:58.0392] Oh, dang, and there was even also relevant discussion on . You’re pretty plugged into the related proposals. Sorry for not responding—research has been hectic. [12:48:06.0552] * Oh, dang, and there was even also relevant discussion on https://github.com/tc39/proposal-call-this/issues/10#issuecomment-3765894544. You’re pretty plugged into the related proposals. Sorry for not responding—work has been hectic. [12:51:11.0267] No worries. We all have busy lives! I did want to respect all the discussion history that has already happened so I tried hard to read up on everything I possibly could. [12:54:12.0006] https://github.com/tc39/proposal-call-this/issues/12#issuecomment-3765070082 > For my use case here, I don't see pipelines helping due to the loose precedence and plethora of sigils. I hope folks can be open to the idea that @js-choi has advanced that they can complement each other, rather than ruling each other out, even if there is some overlap for some use cases. Looks like you said much the same thing as this back in January. I agree it would be nice to have them all. It’s a matter of pushback from engines due to limited implementation budget and Waldemar’s and Mark Miller’s (reasonable) concerns about stewarding the unified language and pushing against complexity creep. [13:29:26.0230] ah yes, rip glitch 2026-03-15 [08:32:07.0608] Two other proposals (protocols and structs) currently provide for a new way to define methods. We could simplify both of these considerably I think by just dropping the methods from them and deferring to call-this. [08:33:07.0735] Regarding the conflict between call-this and extensions, I believe this can be resolved by removing one of the syntactic options from call this: [08:34:34.0218] * Regarding the conflict between call-this and extensions, I believe this can be resolved by removing one of the syntactic options from call this: `o..ns.m()`. If we instead require this to be `o..(ns.m)()`, then there is no conflict. [08:36:41.0638] I mean I was wondering if we could even get away with only allowing `o..m()` and not having a place for arbitrary expressions there at all. [08:45:07.0050] Then there's also the apparently steady stream of proposals for adding methods to built-in types. Having "extension methods" (via call-this) provides a compelling way to try things out without standard monkey-patching risks. [08:47:04.0889] * I was wondering if we could get away with only allowing `o..m()` and not having a place for arbitrary expressions there at all. "Just use a temp variable" and all that. [08:47:32.0792] * I was wondering if we could get away with only allowing `o..m()` and not having a place for arbitrary expressions there at all. "Just use a temporary const" and all that. [10:37:44.0457] > <@jschoi:matrix.org> * Actually, there is even a fourth style, tacit/point-free functional programming, which I happen to be a fan of. Tacit FP is what F# pipes, partial function application syntax, and Function.pipe would have encouraged. But several engine implementors have pushed strongly back on it due to concerns about hot-path function allocation, GC, and optimization complexity. This is an example of TC39 (or at least several very important members of it) actually *discouraging* one of the styles. We still have three styles left to consider. And the pipe operator works with higher-order functions, if you’re willing to attach an explicit call `(##)` to your HOF… IMO, the likeliest case for hot path function allocation would have been `ar |> f(?)`, which could be optimized away *in the spec* and not just in implementations. Even then, pipes like this are more commonly *outside* of loops. Instead, it's the `f` that would contain the loop (i.e., `map`/`filter`/etc.). [10:40:13.0424] Optimizing it away in spec could have been detecting ``PipelineExpression : LeftHandSideExpression `|>` PartialApplicationExpression`` in an SDO and transforming it to an immediate evaluation rather than a function allocation. [10:44:28.0819] That would cause issues with existing libraries preferring a mechanism for gradual adoption of shared structs in their codebase, as well as Shu's oroginal hopes for structs to be *better* classes due to their fixed layout and initialization order guarantees. Call-this would be a major impediment to adoption, IMO. [11:31:34.0960] > <@ewinslow:matrix.org> Regarding the conflict between call-this and extensions, I believe this can be resolved by removing one of the syntactic options from call this: `o..ns.m()`. If we instead require this to be `o..(ns.m)()`, then there is no conflict. I’m open to requiring parentheses around property access chains in call-this’s RHS, in the spirit of compatibility with the extensions proposal. And it can always be added back in extensions continues to fail to progress. I would also be interested to know how Jordan feels about it for his use cases. [11:31:43.0035] > <@ewinslow:matrix.org> I was wondering if we could get away with only allowing `o..m()` and not having a place for arbitrary expressions there at all. "Just use a temporary const" and all that. Is there a particular reason not to allow parenthesized arbitrary expressions in the RHS? [11:33:57.0787] More examples: * RxJS went from Methods to HOF. * Angular team tells me they write their API in a weird way in order to be tree-shakable, though they don't like that they have to do that. * Zod library is method-based, and Valibot was created as a tree-shakable alternative. * `_.chain` from Lodash is method-based, but they of course export a whole separate library of tree-shakable functions also for people who want that. * Colors (by Lea Verou) has an OO-methods-based API but it's huge so they export a separate set of functions that are more tree-shakable. So yes, I see a strong and consistent trend of people having method-based APIs then needing to back out and switch to function-based APIs for tree-shaking / performance. [11:35:36.0486] And they all, to a fault, do it sacrificing ergonomics and going to functions, which are much more reliably tree-shakable, not by giving "hints" to bundlers to be more aggressive. [11:35:53.0705] * And they all, to a fault, do it by sacrificing ergonomics and going to functions, which are much more reliably tree-shakable, not by giving "hints" to bundlers to be more aggressive. [11:38:31.0057] And given all the articles online about "barrel files considered harmful" it is apparent to me that many, many folks are quite willing to change their code in order to get **reliable** tree-shaking. [11:38:43.0046] * And given all the articles online about "barrel files considered harmful" it is apparent to me that many, many folks are quite willing to change their code in order to get **more reliable** tree-shaking. [11:39:56.0647] A commitment to aggressively simplifying the proposal in every conceivable way :) [11:43:28.0163] The way Jordan explained his use-case to me, it was all about defensiveness against mutated prototypes. So something like `myArr..Array.prototype.slice(...)` doesn't accomplish that anyways. Nor does `myArr..(Array.prototype.slice)(...)`. You need a top-level `const {slice} = Array.prototype;` and then later on invoke with `myArr..slice(...)`. [11:45:19.0577] So maybe that is another reason to not have the `(expr)` option at all. But yes, would be great to hear from him directly. [11:50:13.0531] Hmm, that would of course be disappointing. I guess I was hoping it would be the inverse -- `structs` lacking prototype methods would be one more motivation to switch existing libraries to tree-shakable methods. So you are just all around getting really really big performance benefits out of making the switch to better tree-shaking. [11:52:14.0619] * Hmm, that would of course be disappointing. I guess I was hoping it would be the inverse -- `structs` lacking prototype methods would be one more motivation to switch existing libraries to tree-shakable methods. So you are just all around unlocking really really big performance benefits out of making the switch to better tree-shaking. [12:03:12.0756] * So maybe that is another reason to not have the parenthesized `obj..(ns.m)()` option at all. But yes, would be great to hear from him directly. [12:03:30.0489] * So maybe that is another reason to not have the parenthesized `o..(ns.m)()` option at all. But yes, would be great to hear from him directly. [12:09:13.0724] One reason in favor of leaving the parenthesized RHS syntax is actually bundlers: To faithfully represent ESM semantics, they tend to do property access on the module object. So you end up seeing a lot of `(0, ns.fn)(...args)` in the bundled code to simulate calling a named import without accidentally setting `this` to the module object `ns` [12:09:55.0969] * One reason in favor of leaving the parenthesized RHS syntax is actually bundlers: To faithfully represent ESM semantics, they tend to lazily do property access on the module object. So you end up seeing a lot of `(0, ns.fn)(...args)` in the bundled code to simulate calling a named import without accidentally setting `this` to the module object `ns` [12:22:05.0361] One of the requirements we have for structs is the ability to maintain private state, which can only be accessed by methods and functions declared on the `struct` itself, just as on classes. Struct methods currently have some unique differences to class methods which are intended to be improvements over normal class methods, however. For one, struct instance methods are intended to have a fixed `this` type that is enforced at runtime so as to reduce polymorphism. In addition, shared structs are currently specified to only be legal at the top level of a module, so no `shared struct` expressions or `shared struct` declarations inside functions or blocks. This means that `shared struct` methods are guaranteed to be placed on a declaration at the top level, which will help improve static analysis of these methods as a knock-on effect. [12:30:01.0715] Awhile back I suggested we consider introducing a `Symbol.geti`/`Symbol.seti` protocol in conjunction with slice/index-from-end notation, which could be defined on an object intended to be used as a key. I wonder if something like that could be leveraged to allow you to just use a function as a key: `myArr[Array.prototype.slice](...)` [12:36:49.0997] Ah, interesting. Can you say more? Which object would have the `Symbol.geti` and what would be the implementation? [12:37:10.0348] * Ah, interesting. Can you say more? Which object would have the `Symbol.geti`? `myArr` or `slice`? and what would be the implementation? [12:37:21.0902] The idea was that you could have a `Slice` class and an `Index` class that supported these symbols, and that you could use `x:y` literal syntax to produce a `Slice` and `^x` syntax to produce an `Index` computed from the end of an array, such that the following are equivalent: ```js myArr[0:1]; myArr[new Slice(0, 1)]; new Slice(0, 1)[Symbol.geti](myArr); myArr[^1]; myArr[new Index(1, true /*fromEnd*/)]; new Index(1, true)[Symbol.geti](myArr); ``` A `Symbol.geti` for a function could just be a call to `bind`: ```js myArr[Array.prototype.slice](1, 2); Array.prototype.slice[Symbol.geti](myArr)(1, 2); Array.prototype.slice.bind(myArr)(1, 2); ``` Or a possible `Symbol.calli` could intercept invocation: ```js myArr[Array.prototype.slice](1, 2); Array.prototype.slice[Symbol.calli](myArr, 1, 2); ``` [12:38:20.0507] `slice` would, or rather, any function would. The symbol just avoids depending on `bind` or `call` directly to avoid the issue with `then` we see with `Promise`. [12:39:42.0292] The downside is that putting it on all functions would possibly change existing behavior, in which case some other token would be necessary to opt in. i.e., `myArr[::Array.prototype.slice](1, 2)`, or similar. [12:40:43.0438] either way, you need to use something like `()` or `[]` to bracket the expression. This just sticks to using `[]` since that's familiar to JS users. [12:40:43.0863] Urf, right, because `myArr[sliceFn]` already means `myArr[String(sliceFn)]`? [12:40:50.0679] Yeah. [12:42:54.0164] But this could be as simple as a wrapper function. i.e., if we had `Symbol.geti` already, someone could just write: ```js const wrap = f => ({ [Symbol.geti]: obj => f.bind(obj) }); myArr[wrap(Array.prototype.slice)](1, 2); ``` [12:44:54.0650] The intermediate object and the call to `.bind` are obvious downsides, however. [12:46:02.0593] I wonder how often people actually try to use a function as a key in an object. [12:49:18.0064] I had not seen or considered this idea of making `o[fn]()` work before. Although it technically has an existing meaning, maybe there's a chance no one uses it like that... I will write it down as an alternative to consider. [12:50:55.0380] Oh I guess another potential concern here is "this would slow down all existing `[]` access, since they would all have to first check for `Symbol.calli/geti/seti` now. [12:51:08.0176] * Oh I guess another potential concern here is "this would slow down all existing `[]` access," since they would all have to first check for `Symbol.calli/geti/seti` now. [13:06:17.0466] Ah interesting! I was just musing with Ashley Claymore plenary about a potential new syntax for declaring extension methods that would grant access to private state, but in a tree-shakable way. And I also imagined enforcing the `this` type. I wasn't sure if that would be too much of a performance penalty (runtime checks being expensive) or a performance win (megamorphic functions trigger their own special kind of slow). Let me find it... [13:06:40.0239] I recall that they were concerned about observably different stack traces making optimizations like that complex or not possible. You might remember what happened there better than me. [13:08:54.0758] I don't think it would, actually. We would only resolve `geti` on objects, so it wouldn't effect string, symbol, and number, which are the lion's share of uses. Anything else already requires a user-code carve out for `toString` [13:09:07.0480] ``` [13:09:41.0879] * class C { #x = Math.random(); #y = Math.random(); } extends C { length() { return Math.sqrt(this.#x**2 + this.#y**2); } } console.log(new C()..length()) [13:10:01.0927] * ``` class C { #x = Math.random(); #y = Math.random(); } extends C { length() { return Math.sqrt(this.#x**2 + this.#y**2); } } console.log(new C()..length()) ``` [13:10:08.0364] * ```ts class C { #x = Math.random(); #y = Math.random(); } extends C { length() { return Math.sqrt(this.#x**2 + this.#y**2); } } console.log(new C()..length()) ``` [13:12:10.0876] You don't want just anyone to be able to reopen your class and access your private state. It defeats the purpose of strong privacy. [13:12:34.0158] Correct. So this would be scope-limited [13:12:59.0783] I considered `WeakMap` with `geti`/`seti`, but that wouldn't work for structs. [13:13:10.0655] E.g. ```ts import {C} from '...'; extends C { length() { this.#x } // lol no, syntax error, go away } ``` [13:16:03.0057] I'm curious how you envisioned enforcing the `this` type at runtime without too much performance penalty? [13:16:39.0001] Is it just a basic `instanceof` check or something fancier? [13:16:59.0358] If you're in the same scope, you don't need those if we already have `static {}`: ``` let getX; class C { x = ...; static { getX = c => c.#x; } } C.prototype.length = function () { return getX(this); }; ``` [13:17:34.0763] Shared structs have a fixed shape. If the `this` does have the same fixed shape it throws. [13:20:07.0859] Interesting. So the browser has maybe some cache of the shape for the struct, which cannot be changed. Technically, two structs with the same shape could share each other's methods, but then why would you do that because your struct already has the needed method? [13:20:14.0571] * So the browser has maybe some cache of the shape for the struct, which cannot be changed. Technically, two structs with the same shape could share each other's methods, but then why would you do that because your struct already has the needed method? [13:22:53.0219] Yea I do see how it's already possible but this seems like exactly the kind of case where people will go, "nah, I'll just do the convenient non-tree-shakable thing instead" [13:23:01.0550] * Yea I do see how it's already possible but this seems like exactly the kind of case where people will go, "nah, I'll just do the much more convenient non-tree-shakable thing instead" [13:25:05.0580] > <@ewinslow:matrix.org> So the browser has maybe some cache of the shape for the struct, which cannot be changed. Technically, two structs with the same shape could share each other's methods, but then why would you do that because your struct already has the needed method? There is a type identity tied to the shape, which is used to handle prototypes across threads. You also don't want a third party to emulate your shape to access your private state. [13:27:05.0650] The way it is set up, the only way to access shared state across threads in a shared struct is using a function/method of the struct loaded from the same file in two agents [14:25:42.0051] > <@rbuckton:matrix.org> If you're in the same scope, you don't need those if we already have `static {}`: > > > ``` > let getX; > class C { > x = ...; > static { getX = c => c.#x; } > } > > C.prototype.length = function () { return getX(this); }; > ``` > The idea is to be able to move "methods" out of the declaration so they are exported and imported separately making tree shaking almost trivial. https://gist.github.com/acutmore/e08fad716e0369dfa0341bde8b8e17d9 2026-03-16 [17:46:52.0723] Did "pipe should belong in the future jssugar layer" block advancement to stage 3? [17:49:04.0473] For me, if people are willing to write tools to transpile it, that is a huge win. E.g. if TS supports it by default. Because that means people can start learning about it and writing code to take advantage of it. [17:51:00.0181] For call-this, native browser support would probably bring some marginal performance benefit. For pipeline, I can't imagine there'd be any difference. [02:00:46.0073] One small issue with `obj[fn]' is that sites polyfilling the feature would need to transform almost all dynamic index syntax as they won't know which arguments use the new feature. [02:01:36.0015] Personally I feel like it would be better to have new syntax rather than overload the existing space. [09:47:23.0431] There are and will be some JS features that cannot be polyfilled or downleveled, and I think that's OK. 2026-03-17 [16:56:58.0213] > <@ewinslow:matrix.org> Did "pipe should belong in the future jssugar layer" block advancement to stage 3? JSSugar did not block the pipe operator from Stage 3 because the pipe operator has never been formally proposed for Stage 3. Every plenary timeslot since it advanced to Stage 2 in 2021 has been devoted to bikeshedding about its syntax or its overlap with the other dataflow proposals . JSSugar was formally proposed in October 2024 . My understanding is that nothing concrete has happened for JSSugar since then, except for vague rumblings from sympathetic engine implementers. Someone correct me if I’m wrong. 2026-03-18 [17:00:13.0086] When I wrote https://github.com/tc39/proposal-pipeline-operator/issues/232#issuecomment-2784879225, I had no idea what was going to happen with pipe operator and JSSugar. It seemed like an existential and rapidly evolving issue for not only pipe operator but every convenience syntax proposal. Seemingly not much has happened since then—I’m still not sure what’s going on with JSSugar. [09:24:57.0201] I agree in general, but I'm not convinved that this features meets that bar. This is a feature that would squarely fit in the "JS Sugar" bucket, if we'd followed up with that split - it's entirely a developer ergonomics feature, so it seems like the wrong trade-off to make it unpolyfillable and/or to have an expensive transpilation that has to touch tons of existing code. [11:42:45.0004] While I agree that Hack pipelines are generally "JS Sugar", I'm not yet convinced that something like call-this should be. [11:46:43.0354] The fact that Hack-style pipelines are essentially just `,` makes their desugaring trivial. I don't agree that something that is "entirely a developer ergonomics feature" means it must be polyfillable. Operator overloading is purely a developer ergonomics feature, and that cannot be polyfilled cleanly. [11:49:34.0142] I'm also not bullish on `o..(x)()` syntax due to possible confusion over null chaining with `?`/`?.[`/`?.(`, which may make that syntax an uphill battle. 2026-03-19 [20:32:29.0128] I think Evan is leaning into this specifically because it works well with optional chaining syntax: `o?..method()` could short circuit if `o` is nullish. [20:45:11.0728] Whoa never even proposed for stage 3, I didn't realize. [20:52:53.0188] Yes. Also starts with a dot, huge Autocomplete win. People have said "dot chaining just feels so familiar". Easy to type and delete to get back and forth from normal methods which I think mitigates the "which one was it?" concern somewhat. I dunno it's my favorite right now and hasn't gotten much consideration historically so I figure I might as well try it out a bit while we're talking about it. [20:58:59.0840] All spellings have received one complaint or another. I think the main barrier to overcome for that proposal is this-hesitancy, not spelling. I will accept any spelling that can get through committee if we can figure out how to overcome the this-phobia first. [21:19:45.0841] Another bit of feedback that was mentioned during plenary was "will new syntax actually be attractive enough to induce people to switch"? Given that it's a key premise I think it is entirely fair question and I want to do some due diligence to validate that assumption. I'm planning to reach out to a couple of the libraries I mentioned in my talk and get a pulse from their maintainers and engaged community members on call this and hack pipeline. [21:20:36.0126] If anyone has recommendations on a better way to go about this, do share 🙂 [21:48:02.0391] 'obj..fn()` is most usually complained about in terms of conflicting with the existing semantics for `1..toString()` [21:49:50.0513] personally I'm fine with the similarity as I dont think it will come up in practice. theres no much benefit to calling a custom method on a number literal [21:50:13.0503] * personally I'm fine with the similarity as I dont think it will come up in practice. theres not much benefit to calling a custom method on a number literal  [21:58:31.0954] I think that would be some fantastic community outreach. [22:00:59.0193] If some of the largest web libraries say they have a preference for pipe style and that they would offer a more tree shakable API if pipeline existed that would be very compelling motivation [22:04:47.0381] example of the "current" (3yrs ago) push back from consumers when a library (Firebase) moved to functions: https://www.reddit.com/r/Firebase/s/j9rJJa0YsB > Things just take longer to program. They are backwards. firestore.collection('users').doc(userId) is a million times better and more logical than doc(collection(getFirestore(), 'users'), userId) This is not even mentioning that you need to KNOW the names of these functions in order to import them. I don't know what the Firebase team was thinking here. Surely there must have been some better solution that attempting to turn the entire mindset of programming upside down. [23:08:55.0562] structs seem completely orthogonal to me [23:09:09.0060] `..(` should never be a viable option for anything imo, it's a bug farm [02:04:53.0918] > <@ljharb:matrix.org> `..(` should never be a viable option for anything imo, it's a bug farm I'm blanking. What's the potential bug with the open paren? [07:35:03.0782] > <@ewinslow:matrix.org> Another bit of feedback that was mentioned during plenary was "will new syntax actually be attractive enough to induce people to switch"? Given that it's a key premise I think it is entirely fair question and I want to do some due diligence to validate that assumption. I'm planning to reach out to a couple of the libraries I mentioned in my talk and get a pulse from their maintainers and engaged community members on call this and hack pipeline. I agree that community outreach here would be useful. The pipe operator README already has a lot of examples of turning deeply nested expressions into linear ones, so that might be helpful for outreach. The big argument is that making an expression a little longer with an explicit `##`is worth it if it means you no longer have to read back and forth from left to right or keep track of matching parentheses. In other words, for dataflow fluency, linearity matters more than tacitness. Call-this and this-based standalone functions are also a tree-shakeable alternative in a lot of cases, and they preserve that tacit style. But they’re less flexible, since they do not help linearize ordinary functions like Array.from (very common), array or object literals (very common), spread syntax, await, arithmetic, and so on. Still, some API designers and developers may prefer call-this to pipe operator, in worlds where we must choose one between them. I’d be interested to hear what they think. [07:36:54.0340] > <@aclaymore:matrix.org> example of the "current" (3yrs ago) push back from consumers when a library (Firebase) moved to functions: > https://www.reddit.com/r/Firebase/s/j9rJJa0YsB > > > > Things just take longer to program. They are backwards. > > firestore.collection('users').doc(userId) > > is a million times better and more logical than > > doc(collection(getFirestore(), 'users'), userId) > > This is not even mentioning that you need to KNOW the names of these functions in order to import them. > > I don't know what the Firebase team was thinking here. Surely there must have been some better solution that attempting to turn the entire mindset of programming upside down. > > > > > Maybe we should add some Firebase examples to the pipe readme… I do think the “you need to KNOW the names of these functions in order to import them” remark kind of funny. Is that not also true for ordinary methods? [08:00:35.0188] I do know that at least one prominent API designer, Ben Lesh of RxJS, went in that direction too: he redesigned RxJS around standalone ordinary functions. Back then, he was very enthusiastic about F# pipes for tacit functional programming and reacted very negatively to Hack pipes trading tacitness for explicitness. When engine implementers pushed back on F# pipes, he became broadly disillusioned with TC39, with strong language. He has even said he would prefer that nothing be added to the language rather than Hack pipes. Even though I like tacit FP, I still do not fully understand why he feels that strongly about needing tacitness, rather than reading linearity. Anyways, you might also encounter his opinions or similar opinions from other people in the community. It would still be good input. [09:05:21.0973] Autocomplete helps with method names a lot. You can get a list of methods without knowing any names at all. Just `.` and you're on your way. The dot is powerful... [09:10:12.0172] I should also say that "Works with lots of existing expressions and functions" is not a motivation for me, because those are already tree shakable. The impact of our effort here on tree shaking depends specifically on convincing would be method users to do something more tree shakable instead. [09:21:46.0351] * I do know that at least one prominent API designer, Ben Lesh of RxJS, went in that direction too: he redesigned RxJS around standalone ordinary functions. Back then, he was very enthusiastic about F# pipes for tacit functional programming and reacted very negatively to Hack pipes trading tacitness for explicitness. When engine implementers pushed back on F# pipes, he became broadly disillusioned with TC39, with strong language. He has even said he would prefer that nothing be added to the language rather than Hack pipes. Even though I like Ben Lesh’s work, and I like tacit FP, I still do not fully understand why he feels that strongly about needing tacitness, rather than reading linearity. Anyways, you might also encounter his opinions or similar opinions from other people in the community. It would still be good input. [09:29:02.0578] Ordinary functions are already tree-shakeable, but they are not ergonomic because ordinary function calls are not linear. They are prefix forms, so they often lead to deeply nested expressions, lots of parenthesis tracking, and a back-and-forth reading pattern. And from what you found, the main complaint about Firebase’s move to ordinary functions for tree-shakeability was that they were harder to read, right? A perceived loss of fluency in reading dataflow expressions—probably due to loss of linearity, and perhaps loss of tacitness. [09:32:32.0386] Ok so we should connect those dots more explicitly: [09:33:56.0832] * Ok so we should connect those dots more explicitly: "makes the existing corpus of standalone functions more attractive to method users" or something like that. [09:34:56.0793] Yes, that’s right. The link between the pipe operator and tree-shakeability is that pipes could make tree-shakeable code more appealing by making ordinary-function APIs easier to read and use. The idea is to answer complaints that `doc(collection(getFirestore(), 'users'), userId)` feels “a million times [worse] and [less] logical” than `firestore.collection('users').doc(userId)` by making the former writable as `getFirestore() |> collection(##, 'users') |> doc(##, userId)`. So the real question is whether gaining linearity, even without tacitness, improves fluency enough that developers would be more willing to adopt tree-shakeable ordinary-function APIs. [09:35:48.0902] …While at the same time also pursuing call-this in parallel, and trying to figure out how much “overlap” the engine implementors are willing to accommodate. There’s already more than one way to do it in JavaScript… [09:36:24.0145] * …While at the same time also pursuing call-this in parallel, and trying to figure out how much “overlap” the engine implementors (and stewardship-minded folk like Mark Miller) are willing to accommodate. There’s already more than one way to do it in JavaScript… [09:42:06.0821] I’d like to give call-this another good shot, too, in the name of “this is a common pattern” and “it might encourage people to write more tree-shakeable code and decrease web dead code”. And because Jordan is very concerned that pipe operator would kill call-this.But, even though I am a co-champion of both pipe operator and call-this, if I had to choose one, I would personally rather live in a world with pipe operator than with call-this. [09:42:20.0673] * I’d like to give call-this another good shot, too, in the name of “this is a common pattern” and “it might encourage people to write more tree-shakeable code and decrease web dead code”. And also because Jordan is concerned that pipe operator would kill call-this. But, even though I am a co-champion of both pipe operator and call-this, if I had to choose one, I would personally rather live in a world with pipe operator than with call-this. [09:42:31.0624] * I’d like to give call-this another good shot, first, in the name of “this is a common pattern” and “it might encourage people to write more tree-shakeable code and decrease web dead code”. And also because Jordan is concerned that pipe operator would kill call-this. But, even though I am a co-champion of both pipe operator and call-this, if I had to choose one, I would personally rather live in a world with pipe operator than with call-this. [09:42:44.0063] * I’d like to first give call-this another good shot, in the name of “this is a common pattern” and “it might encourage people to write more tree-shakeable code and decrease web dead code”. And also because Jordan is concerned that pipe operator would kill call-this. But, even though I am a co-champion of both pipe operator and call-this, if I had to choose one, I would personally rather live in a world with pipe operator than with call-this. [10:03:12.0123] * I’d like to first give call-this another good shot, in the name of “this is a common pattern” and “it might encourage people to write more tree-shakeable code and decrease web dead code”. And also because Jordan is concerned that pipe operator would kill call-this. But, if I had to choose one, I would personally rather live in a world with pipe operator than with call-this. [10:27:00.0216] > theres not much benefit to calling a custom method on a number literal 1 ..seconds() // Temporal duration, woot! [10:27:09.0505] * > theres not much benefit to calling a custom method on a number literal ```js 1 ..seconds() // Temporal duration, woot! ``` [10:29:04.0279] * > theres not much benefit to calling a custom method on a number literal ```js 10 ..seconds() // Temporal duration, woot! ``` [10:38:08.0698] * > theres not much benefit to calling a custom method on a number literal I use number-literal extension methods in Kotlin most often for creating durations. ```js 10 ..seconds() // Temporal duration, woot! ``` So... I suspect this could definitely become a thing in JS too, although it requires a new helper method. If the autocomplete gets as good as I hope it can, that won't be very much hindrance at all. [10:57:16.0914] So far this pattern does seem to be exceedingly rare in practice. I can only found low-thousands of potential matches on GitHub and many of them appear to be people just figuring out what that even does (e.g. `console.log(1..toFixed(0))`) or built code rather than source code. Compare to high 10s of millions for `this` and arrow functions, each. I probably would want my auto-formatter to wrap it in parens or something instead: e.g `(1).toFixed()` but I just so rarely ever see methods called on number literals in the first place. [11:46:47.0000] > <@ewinslow:matrix.org> > theres not much benefit to calling a custom method on a number literal > > I use number-literal extension methods in Kotlin most often for creating durations. > > ```js > 10 ..seconds() // Temporal duration, woot! > ``` > > So... I suspect this could definitely become a thing in JS too, although it requires a new helper method. If the autocomplete gets as good as I hope it can, that won't be very much hindrance at all. Sure. Though `seconds(10)` is not that bad. chaining is great as it avoids deeply nesting. But a number literal can only be at the start. [11:46:55.0423] My assumption is currently that pipeline will be attractive to X% and call-this will be attractive to X+Y%. I would like to get a better feel for the magnitudes of X and Y. X is small + Y is small = do neither. Neither will make a meaningful difference. X is large + Y is small = Just do pipeline. It's more generally applicable and call-this provides only marginal benefit to end users. X is small + Y is large = do call-this. Pipeline won't move the needle but call-this will. X is large + Y is large = do call-this, then maybe pipeline if it somehow interests a different set of method-users. [11:47:38.0139] * My assumption is currently that pipeline will be attractive to X% and call-this will be attractive to X+Y%. I would like to get a better feel for the magnitudes of X and Y. X is small + Y is small = do neither. Neither will make a meaningful difference. X is large + Y is small = Just do pipeline. It's more generally applicable and call-this provides only marginal additional benefit to end users. X is small + Y is large = do call-this. Pipeline won't move the needle but call-this will. X is large + Y is large = do call-this, then maybe pipeline if it interests a different set of method-users. [11:48:41.0679] Probably % of users is not quite right either, since not all JS authors affect web traffic equally... [11:48:57.0468] * Probably % of users is not quite right either, since not all JS authors affect web traffic equally... I dunno this is hard hahaha [11:48:57.0832] RxJs is also an example of the exact code engines we're worried about. Every RxJs operator allocates the functor with the bound arguments for the pipe to then pass in the observable [11:51:18.0705] Pipe can't be proposed for stage 3 (or 2.7). The design is still in draft. [11:52:03.0439] We need to have a theoretically finished spec with no expected changes sans test/implementation feedback [13:01:16.0908] * So far this pattern does seem to be exceedingly rare in practice. I can only find low-thousands of potential matches on GitHub and many of them appear to be people just figuring out what that even does (e.g. `console.log(1..toFixed(0))`) or built code rather than source code. Compare to high 10s of millions for `this` and arrow functions, each. I probably would want my auto-formatter to wrap it in parens or something instead: e.g `(1).toFixed()` but I just so rarely ever see methods called on number literals in the first place. [14:16:53.0274] If structs are like classes and define methods, then they're the same [14:17:14.0720] Dynamic lookup of methods defeats static DCE, which is what we're hoping to target [14:37:30.0464] Note that this approach doesn't solve the "have to know the name to import it" argument. Having a known position (either first arg, or `this` binding) would. [14:39:12.0944] F# also doesn't solve this, because you have to complete the closure call to know what function is being invoked by the pipeline [14:40:24.0805] I'd really like to pursue Elixir style (to support known arg position, auto imports, tacitness, and fluency), with PFA layered on (to support any arg position) [15:25:28.0699] Okay thank you for saying so. So far every time I brought up elixir I just get baffled looks so I hadn't been planning on pursuing it further. But I'm open to it if you think it's worth making the case. It certainly a side steps the this-hesitancy issue nicely 2026-03-20 [19:03:08.0518] > <@ewinslow:matrix.org> My assumption is currently that pipeline will be attractive to X% and call-this will be attractive to X+Y%. I would like to get a better feel for the magnitudes of X and Y. > > X is small + Y is small = do neither. Neither will make a meaningful difference. > X is large + Y is small = Just do pipeline. It's more generally applicable and call-this provides only marginal additional benefit to end users. > X is small + Y is large = do call-this. Pipeline won't move the needle but call-this will. > X is large + Y is large = do call-this, then maybe pipeline if it interests a different set of method-users. It‘s probably more like a Venn diagram with some overlap: X%+Y%+Z%, where Y% is the overlap between pipe operator and call-this. A lot of people have expressed interest in the pipe operator but not call-this over the years, even disregarding code-shaking as a motivation. I don’t quite think pipe-attracted people are a subset of call-this-attracted people. With that said, it’s true that we have not, to my knowledge, pitted them head-to-head in front of the community. Well, actually, I *think* prior State of JS surveys have maybe asked developers about both proposals when assessing their interest in various TC39 proposals. I’d need to double-check the prior years’ results…but I’m not really looking forward to doing so. It’s a good resource, but I find the State of JS websites difficult to navigate… [19:16:54.0863] > <@jridgewell:matrix.org> I'd really like to pursue Elixir style (to support known arg position, auto imports, tacitness, and fluency), with PFA layered on (to support any arg position) The point that Elixir pipes (but not F# pipes or Hack pipes) would support autocompletion on the first argument is intriguing. I wonder if autocompletion would have changed Daniel Ehrenberg’s decision to use F# pipes instead of Elixir pipes, back in 2017 (nearly a decade ago…). Though I don’t know if I really want to wade through that whole pipe-style bikeshedding ordeal again. Weren’t quite a few people against promoting the zeroth argument to be special, even with PFA syntax? And then there were the engine concerns with PFA syntax and optimizability of observable stack traces, etc. [19:17:15.0366] Well, I suppose call-this could also serve that role of providing tree-shakeability, fluency, *and* autocompletion. And that Reddit comment about Firebase’s change makes it clear that at least some developers feel strongly about autocompletion too. This makes me more torn about that statement I made earlier about rather having Hack pipes than call-this. I suppose that, even in a world where call-this makes `this`-based standalone functions more common, the pipe operation could still be useful…because non-`this`-based standalone functions like Array.from are not ever going away. But the engine implementers are likely not to agree. [19:20:50.0679] * Well, I suppose call-this could also serve that role of providing tree-shakeability, fluency, and autocompletion. And that Reddit comment about Firebase’s change makes it clear that at least some developers feel strongly about autocompletion too. This makes me more torn about that statement I made earlier about rather having Hack pipes than call-this. I suppose that, even in a world where call-this makes this-based standalone functions more common, the pipe operation could still be useful…because non-this-based standalone functions like Array.from, Object.keys, etc. are not ever going away. But the engine implementers are likely not to agree. [19:22:51.0906] I think we dismissed Elixir without really discussing its merits originally. There are definitely delegates that dislike the special first arg behavior, but there are also those that dislike `this` binding behavior too. Re PFA and optimizing, I don't think this will be a concern the way it is with F#. Using PFA directly within a pipeline is optimizable, but we've only talked about PFA with F# and F# promotes any type of closure-returning function call. [19:23:27.0035] You could abuse Elixir to generate F# closure pipelines, but it actively resists it. [19:25:25.0465] * You could abuse Elixir to generate F# closure pipelines, but it actively resists it. (You would have to put trailing `foo(arg)()` to generate an F# single param style call) [19:26:26.0861] From NPM: ``` // Status quo: const json = await npmFetch.json(npa(pkgs[0]).escapedName, opts); // With Hack pipes: const json = pkgs[0] |> npa(##).escapedName |> await npmFetch.json(##, opts); // With Elixir pipes, assuming it allows a `|> await memberExpression()` form: const json = (pkgs[0] |> npa()).escapedName |> await npmFetch.json(opts); // With call-this: // Can’t change it. [19:26:53.0440] * Using the pipe readme’s example from `node/deps/npm/lib/unpublish.js`: // Status quo: const json = await npmFetch.json(npa(pkgs[0]).escapedName, opts); // With Hack pipes: const json = pkgs[0] |> npa(##).escapedName |> await npmFetch.json(##, opts); // With Elixir pipes, assuming it allows a `|> await memberExpression()` form: const json = (pkgs[0] |> npa()).escapedName |> await npmFetch.json(opts); // With call-this: // Can’t change it. [19:26:54.0500] But, I'd settle for any of Hack/Elixir/Call pipelines. I would like multiple, but I don't know if we'll get that. [19:27:06.0702] * Using the pipe readme’s example from `node/deps/npm/lib/unpublish.js`: ```js
// Status quo:
const json = await npmFetch.json(npa(pkgs[0]).escapedName, opts);
// With Hack pipes:
const json = pkgs[0] |> npa(##).escapedName |> await npmFetch.json(##, opts);
// With Elixir pipes, assuming it allows a |> await memberExpression() form:
const json = (pkgs[0] |> npa()).escapedName |> await npmFetch.json(opts);
// With call-this:
// Can’t change it. ``` [19:29:57.0680] …I’m trying this again; Element’s message editing is so unreliable. From inside NPM’s code: ```js // Status quo: const json = await npmFetch.json(npa(pkgs[0]).escapedName, opts); // With Hack pipes const json = pkgs[0] |> npa(##).escapedName |> await npmFetch.json(##, opts); // With Elixir pipes, assuming that it supports a `|> await memberExpression(arg0)` form: const json = (pkgs[0] |> npa()).escapedName |> await npmFetch.json(opts); ``` [19:31:41.0357] * …I’m trying this again; Element’s message editing is so unreliable. From inside NPM’s code: ```js // Status quo: const json = await npmFetch.json( npa(pkgs[0]).escapedName, opts, ); // With Hack pipes const json = pkgs[0] |> npa(##).escapedName |> await npmFetch.json(##, opts); // With Elixir pipes, // assuming that it supports a `|> await memberExpression(arg0)` form: const json = (pkgs[0] |> npa()).escapedName |> await npmFetch.json(opts); ``` [19:37:11.0253] Nit: we could support tacit `pkgs[0] |> npa`. But yes, running arbitrary expressions is a little more difficult, and likely pushes some to do `getEsapedName` helpers or similar. And `await`/`yield` could be handled as either special behavior inline, or a postfix `|> await |>` so we don't redefine the expression inline. [19:37:22.0163] * Nit: we could support tacit `pkgs[0] |> npa`. But yes, running arbitrary expressions is a little more difficult, and likely pushes some to do `getEsapedName` helpers or similar. And `await`/`yield` could be handled as either special behavior inline, or a postfix `|> await |> …` so we don't redefine the expression inline. [19:39:18.0516] Double checking [HISTORY.md](https://github.com/tc39/proposal-pipeline-operator/blob/main/HISTORY.md#20172018), it looks like Dan ran into automatic semicolon insertion problems with `|> await`, which is why he tried to defer it to a future proposal. [19:40:02.0875] …Times like this is why I’m glad I wrote this document back then… [19:40:24.0950] Very good job! This is sooo long ago I can barely remember. [19:40:45.0753] Of course ASI is biting us 🙄 [19:42:51.0079] > <@jridgewell:matrix.org> F# also doesn't solve this, because you have to complete the closure call to know what function is being invoked by the pipeline What do you mean by "complete the closure call" in this case? I'd venture to guess that with F#-style and PFA, the majority of use cases would have been `|> f(?, a, b)` anyways since *most* functional libraries for JS are first-arg (aside from Ramda). [19:45:49.0260] That was from me trying Rx pipelines. You'd do `x |> foo(a, b)`, which means you wrote a ton of code before we could figure out what you're doing. Even with `x |> foo(?)`, you still need to write `foo(` before we can figure out where you're placing the arg and how filter the possible set of functions. [19:46:17.0042] With Elixir style, `x |> f` is enough to start filtering for functions starting with `f` that receive `x` type [19:46:24.0147] From an autocomplete perspective, a language like TS could prioritize completions for F#-style based the topic type first by tacit functions, then by functions that could accept the topic in any position (and possibly complete with `f(_, ?)` for non-first-arg cases with the cursor at the `_`) [19:53:00.0957] Brian Terlson and I were discussing pipelines the day before a plenary back in 2017/2018, I think? One of our concerns at the time was that either Elixir style pipes primarily favored first-arg based libraries like lodash/underscore at the expense of last-arg based libraries like Ramda, which would essentially "choose a winner" in that space, which would likely result in significant community backlash. My solution to the issue was F#-style pipes with PFA as it applied equally to both camps. [19:53:12.0232] * Brian Terlson and I were discussing pipelines the day before a plenary back in 2017/2018, I think? One of our concerns at the time was that Elixir style pipes primarily favored first-arg based libraries like lodash/underscore at the expense of last-arg based libraries like Ramda, which would essentially "choose a winner" in that space, which would likely result in significant community backlash. My solution to the issue was F#-style pipes with PFA as it applied equally to both camps. [19:54:49.0235] There's also the potential for confusion by not supplying a first-arg when pipeline is involved, i.e.: ```js map(ar, x => x + 1) // vs ar |> map(x => x +1) ``` as these two calls to the same function could potentially live side-by-side in the same file. [19:56:35.0997] The upside of PFA is that it kept that intuition as to argument placement. The argument didn't just disappear because I'm using `|>`: ```js const mapped = map(ar, x => x + 1); // vs const mapper = map(?, x => x + 1); const mapped = mapper(ar); // vs const mapped = ar |> map(?, x => x + 1); ``` All three styles could be used interchangeably in the same file without confusing users as to argument order. [19:58:24.0537] Ramda essentially avoids this because Ramda functions curry, so they set the expectation that partial application results in a bound function: ```js add(1, 2); // 3 add(1)(2); // 3 add()(1)(2); // 3 ``` Each of these produces the same result, so it builds an intuition for the developer as to how arguments are applied. [19:59:10.0613] Elixir style pipes don't provide any such intuition. There's nothing special about the function your calling (i.e., no currying), only the `|>` syntax, so it doesn't carry over to any other aspect of the language. [19:59:33.0355] It's just one corner-case of the language that does this unique thing, so its far less intuitive. [19:59:43.0745] In my experience, first arg is the most common, and the way I personally write my libraries. But with PFA, Elixir handles every case as well as Hack or F#. [20:00:38.0651] This is good context for HISTORY.md. I should add it to the file. [20:00:43.0946] Half of the battle with Hack is the topic token. Why didn't we just ban PFA inside pipeline and use the same `?` token? [20:00:53.0293] Yes, all the pipe styles can (mostly) accomplish the same things. My point is that Elixir pipes are unintuitive. [20:01:50.0100] I mean, I would call F#'s auto call style unintuitive, too. It's just which one you get used to using. [20:01:52.0027] I've never really been a fan of Hack style. IMO, while I think PFA has some value on its own, its case is severely weakened by lack of support in pipelines. [20:02:00.0410] Auto-call? [20:02:10.0978] you mean `x |> F` is `F(x)`? [20:02:21.0459] `x |> foo(a, b)` does `foo(a, b)(x)` [20:02:22.0923] That's what the `|>` *means* in F# [20:02:40.0467] It's the same intuition behind `@` in decorators [20:02:51.0598] And this is what it means in Elixir. Both of them are foreign to JS, neither is intuitive. [20:03:55.0964] You just learn the syntax and then it becomes intuitive after using it enough. I think we can get the community to adopt either style. [20:04:19.0809] I still believe F#-style has a very low learning curve. F#-style could be explained without magical argument placement, even without PFA, and PFA could be explained without F#, but they work best together. [20:04:34.0193] The benefit to Elixir is that it won't suffer from the performance regressions promoted by closure allocs. [20:04:56.0320] In F#-style `|>` is just function application. Call the thing on the right with the argument on the left. F# even has multiple-argument styles for `|>` [20:05:39.0341] F#-style doesn't need closure allocs when used with PFA. Those can be statically replaced with an immediate call, and could even be specified as such. [20:06:10.0035] `x |> f(?)` is easily evaluated without a closure [20:06:28.0841] Yes, in the end, that was the part that was important. The engines just did not want to encourage more hot function allocation. They want people to name their functions or to use callbacks that are very simple to optimize. With Elixir or Hack pipes, you cannot do `x |> f(y)` to mean `f(y)(x)`, and, for the engines, this is an advantage. They might have been fine with it if F# pipes restricted the RHS side to a member expression? [20:06:39.0782] And so is `x |> f` [20:07:15.0705] * Yes, in the end, that was the part that was important. The engines just did not want to encourage more hot function allocation. They want people to name their functions or to use callbacks that are very simple to optimize. With Elixir or Hack pipes, you cannot do `x |> f(y)` to mean `f(y)(x)`, and, for the engines, this is an advantage. They might have been fine with it if F# pipes restricted the RHS side to a member expression like ` |> f` or `|> NS.f`? [20:07:17.0544] You only really need to produce a closure for `f(?)` on its own [20:08:05.0019] It’s true that autocompletion engines could use a zeroth-argument-is-probably-the-principle-type heuristic on F# pipes, switching to the next argument’s type. With F# pipes and PFA syntax, as soon as I type `x |>`, my editor’s autocompletion engine could assume that I’m going to type `x |> f(?` or something similar, and it could show completions for standalone functions that expect `x`’s type in its zeroth place. And, if I then type `x |> f(0,` instead, then it can switch to the next argument’s type. Just as a heuristic. …And this would be true even for Hack pipes, too, wouldn’t it? If I type `x |>`, and we were using Hack pipes, the engine could *also* use the same heuristic. It could show functions that expect `x`’s in their zeroth place. So, actually, Justin, I might have changed my mind again thanks to Ron, haha. I think Hack pipes probably are fine for autocompletion. In any case…it’s this part that’s important. The engines just did not want to encourage more hot function allocation. They want named functions. With Elixir or Hack pipes, you cannot do `x |> f(y)` to mean `f(y)(x)`, and this is an advantage for the engines. [20:08:26.0547] * It’s true that autocompletion engines could use a zeroth-argument-is-probably-the-principle-type heuristic on F# pipes, switching to the next argument’s type. With F# pipes and PFA syntax, as soon as I type `x |>`, my editor’s autocompletion engine could assume that I’m going to type `x |> f(?` or something similar, and it could show completions for standalone functions that expect `x`’s type in its zeroth place. And, if I then type `x |> f(0,` instead, then it can switch to the next argument’s type. Just as a heuristic. …And this would be true even for Hack pipes, too, wouldn’t it? If I type `x |>`, and we were using Hack pipes, the engine could _also_ use the same heuristic. It could show functions that expect `x`’s in their zeroth place. So, actually, Justin, I might have changed my mind again thanks to Ron, haha. I think Hack pipes probably are fine for autocompletion. [20:09:18.0283] I don't think this is the style that F#'s syntax promotes, though. You _can_ do this, but there's the subtle push towards `f` without explicitly placing the arg. [20:09:49.0147] * It’s true that autocompletion engines could use a zeroth-argument-is-probably-the-principle-type heuristic on F# pipes, switching to the next argument’s type. With F# pipes and PFA syntax, as soon as I type `x |>`, my editor’s autocompletion engine could assume that I’m going to type `x |> f(?` or something similar, and it could show completions for standalone functions that expect `x`’s type at its zeroth parameter. And, if I then type `x |> f(0,` instead, then it can switch to the next argument’s type. Just as a heuristic. …And this would be true even for Hack pipes, too, wouldn’t it? If I type `x |>`, and we were using Hack pipes, the engine could _also_ use the same heuristic. It could show functions that expect `x`’s type at their zeroth parameters. So, actually, Justin, I might have changed my mind again thanks to Ron, haha. I think Hack pipes probably are fine for autocompletion. [20:10:42.0771] I think the engines concerns about `f(?)` may have been a bit over cautious. You're less likely to see `x |> f(?)` in a tight loop, as it's usually the `f` in this scenario that has the loop (i.e., `map`, `filter`, `reduce`, etc.). You'd be producing no more closures than you normally would with the arrow in `map(x, a => ...)` [20:12:11.0358] It's definitely RX.js that scared them. I agree that PFA inline a pipeline wouldn't be an issue. [20:12:16.0752] F#+PFA definitely promotes `x |> f(?)` as the majority case. `x |> f` is far less frequent, and introduces no closure on its own. The other cases for PFA on its own are no worse than `=>` today. [20:13:47.0919] If Rx.js had PFA, it would arguably be no worse than `=>` since Rx.js consumers were going to use `=>` anyways. All PFA does is reduce the second-guessing about closed over variables. In a way, it *reduces* closures since it evaluates the arguments immediately and only holds the values (like `.bind`). [20:14:34.0983] I don't agree. If I'm writing `x |> foo(?, a, b, c)`, do I really want to place the arg? If I just wrote `x |> foo(a, b, c)` and returned a closure, it shaves the chars… I think that's the subtle push that doesn't happen with other styles. [20:19:38.0268] * It’s true that autocompletion engines could use a zeroth-argument-is-probably-the-principle-type heuristic on F# pipes, switching to the next argument’s type. With F# pipes and PFA syntax, as soon as I type `x |>`, my editor’s autocompletion engine could assume that I’m going to type `x |> f(?` or something similar, and it could show completions for standalone functions that expect `x`’s type at their zeroth parameters (followed by functions that expected `x`’s type at their first parameters, then second parameters, etc.). Just as a heuristic. …And this would be true even for Hack pipes, too, wouldn’t it? If I type `x |>`, and we were using Hack pipes, the engine could _also_ use the same heuristic. It could show functions that expect `x`’s type at their zeroth parameters (and then first, second, etc.). So, actually, Justin, I might have changed my mind again thanks to Ron, haha. I think Hack pipes probably are fine for autocompletion. [20:27:08.0692] Sure, if you are the author of `foo` and don't mind the overhead of returning a closure, then maybe `x |> foo(a, b, c)` seems like a win. Performance wise, it isn't. PFA gives you the *option* to produce a function and allows you to use *existing* functions that were not designed for currying. F#-style support for tacit functions promotes *reuse* rather than one-off closures: ```js // F#+PFA const add = (x, y) => x + y; add(1, 2); // no closure ar |> add(?, 1); // no need for closure const addOne = add(?, 1); ar1 |> addOne; ar2 |> addOne; // reused // vs F#-style with only function-returning functions const add = x => y => x + y; add(1)(2); // produces short-lived closure ar |> add(1); // produces short-lived closure const addOne = add(1); ar1 |> addOne; ar2 |> addOne; // reused ``` PFA is good for all three cases. Function returning is only good for one case. [20:33:44.0641] > don't mind the overhead of returning a closure, then maybe x |> foo(a, b, c) seems like a win. Performance wise, it isn't. We know this, but are library authors? I have done horribly inefficient things in the name of clean looking code when I was a novice. [20:35:05.0634] But I agree with you about PFA. I want it in the language, and I hope it's enough to overcome the subtle push. [20:35:27.0721] * > don't mind the overhead of returning a closure, then maybe x |> foo(a, b, c) seems like a win. Performance wise, it isn't. We know this, but will library authors? I have done horribly inefficient things in the name of clean looking code when I was a novice. [20:37:40.0817] Like, look at Chai.js style syntax chaining 🙈 [20:38:12.0579] I'm personally not sold on Hack-style's premise that people want to use `|>` to operate purely on the topic, i.e. `x |> # + #`. Function pipeline allows you to perform higher-order operations. We can already do `x + x`, and you can emulate Hack-style pipes with `,`, so it doesn't really add anything to the language, IMO. [20:40:29.0805] ```js // Hack-style x = ar |> f(#) |> # + #; // Plain-old JS var _; x = _ = ar, _ = f(_), _ = _ + _; ``` [20:41:18.0233] or if you want it to look all sigil-y: ```js x = _= ar, _= f(_), _= _ + _; ``` [20:41:35.0978] TypeScript doesn't love this [20:41:46.0664] Hack-style pipes is the `,_=` operator. [20:41:48.0112] and it would be rejected in a PR [20:42:00.0754] Perhaps, but that's the downleveling. [20:42:36.0013] Right, it's purely syntatic [20:43:24.0885] an engine would likely need zero new bytecode for it [20:44:11.0432] TypeScript handles this fine: [20:44:33.0293] * TypeScript handles this fine [20:44:34.0759] It's always the same type there [20:44:57.0614] what if it changes to different object types through the pipe [20:46:27.0759] Ok, TypeScript has a slight bug here, but works fine if you use parens: [20:47:18.0010] well, not a bug. You need the parens to keep the precedence correct. [20:47:33.0349] * ~~Ok, TypeScript has a slight bug here, but~~ works fine if you use parens: [20:47:41.0420] * ~Ok, TypeScript has a slight bug here, but~ works fine if you use parens: [20:47:53.0723] Mildly agree. I think it's convenient that Hack allows this, but I think our first priority should be solving method DCE. These kinds of expressions would be mostly simple to extract into helpers, or break apart into multiple pipes (we don't always need super long pipe flows) [20:47:54.0852] hmm. strikethrough doesn't work in Element. [20:48:04.0876] * works fine if you use parens: [20:48:13.0379] * (You need the parens to keep the precedence correct). [20:51:01.0395] IMO the only thing Hack-style pipes do better than F# is `await` and `yield`, though you can still address those with parens: ```js // Hack-style a |> await f(#) |> yield g(#); // F#-style yield (await a |> f(?)) |> g(?); ``` Though there was a proposal to address `await` and `yield` as a special operation: ```js a |> f(?) |> await |> g(?) |> yield; ``` [20:53:23.0160] I don't know how often that would be an issue in practice, though. [20:55:35.0940] I just mentioned that above, apparently there are ASI issues: [20:55:53.0832] * > Though there was a proposal to address await and yield as a special operation: I just mentioned that above, apparently there are ASI issues: [20:55:56.0208] it's bad practice to `yield` deep within an expression, it makes it hard to reason over where you're function might suspend. `await` is the same. It's usually better to break up `await`s into multiple statements, otherwise you're back to parenthesizing like `(await f()).foo()` [20:59:01.0169] I wouldn't call it an "ASI Issue", per se. Anyone who chooses to rely on ASI to avoid semicolons already needs to pepper them throughout their code. ASI is only really an issue if you're trying to introduce new syntax that could be interpreted as existing syntax due to ASI. That doesn't apply to `|>` because its mere existence in the language doesn't cause unrelated code to break. [21:05:12.0539] And as far as Hack-styles benefit for `x |> # + #`, that can be achieved in F# using an arrow, and we could theoretically inline arrows statically within the spec as well: ```js x |> (_ => _ + _) ``` The only thing that would care that it's not actually a function would be a stack trace, but I'm ok with that. [21:05:44.0726] Still no `yield` or `await`, but I contend that's just not as critical here. [21:06:05.0999] * And as far as Hack-style's benefit for `x |> # + #`, that can be achieved in F# using an arrow, and we could theoretically inline arrows statically within the spec as well: ```js x |> (_ => _ + _) ``` The only thing that would care that it's not actually a function would be a stack trace, but I'm ok with that. [21:10:19.0733] F#+PFA only really introduces two distinct concepts (`|>` and `?`/`...`) but gives us so many things. For Hack-style we've suggested multiple additional sigils like `|+>` to provide something like PFA, which is starting to feel like a sigil soup. Too many tokens to remember what does what. It's like trying to remember keybinds in Blender. There's 1000s of them. [21:12:31.0558] I think the same could be said for Elixir. But the additional sigils are still likely going to be suggested regardless of which we choose, eg to support short-circuiting. [21:13:10.0223] * I think the same could be said for Elixir. But the additional sigils are still likely going to be suggested regardless of which we choose, eg to support nullish short-circuiting. [21:13:18.0377] short-circuiting? [21:14:23.0407] Ah. I think that's less of an issue for `|>` than for function calls in general. Aside from `?` and `if` there's no good way to make a call conditional an an input argument being nullish [21:14:47.0526] `x |> foo ?|> bar` could stop the pipeline if the return value from `foo` is nullish. [21:15:11.0661] In F# style, you could just fall back on an arrow: ```js x |> (_ => _ == null ? null : f(_) |> ...) ``` [21:15:33.0664] One of Evan's arguments is that pipeline needs to be as ergonomic as method chaining. `x.foo()?.bar()` is very ergonomic to write. [21:16:14.0691] If we want to encourage people to stop writing class methods, the alternative needs to feel at least as good. [21:16:50.0343] I'm not sure `?|>` is the sigil though. It looks awkward. [21:17:06.0853] Maybe just `?>` [21:17:38.0230] Yah, it all starts to look like soup eventually. Evan was proposing `..` as the pipeline operator, so it'd be `x?..foo()` [21:17:59.0027] That doesn't feel too bad. [21:18:11.0534] * Yah, it all starts to look like soup eventually. Evan was pushing `..` as the pipeline operator, so it'd be `x?..foo()` [21:18:42.0319] * Yah, it all starts to look like soup eventually. Evan was pushing `..` as the pipeline operator, so it'd be `x?..foo()` (specifically with call-this semantics) [21:18:50.0192] `?.[` and `?.(` are already bad, if necessary. For pipelines, I'd rather have something recognizably pipe-like. [21:19:19.0994] I could get behind `?>` [21:20:01.0299] And, IMO, `?..` should just synonymous with `?.`, given that `?.[` is the nullish `[` and `?.(` is the nullish `(`, then `?..` *should* be the nullish `.` [21:20:12.0595] * And, IMO, `?..` should just be synonymous with `?.`, given that `?.[` is the nullish `[` and `?.(` is the nullish `(`, then `?..` _should_ be the nullish `.` [21:20:15.0631] * IMO, `?..` should just be synonymous with `?.`, given that `?.[` is the nullish `[` and `?.(` is the nullish `(`, then `?..` _should_ be the nullish `.` [21:20:44.0611] Not that I want that syntax in the language, just that it essentially holds that space logically. [21:22:18.0326] We should stay away from `..`. We already have `.` and `...` meaning two wholly different things, we don't need `..` meaning something else, especially since some languages use `..` in the same way we use `...` [21:22:54.0252] i.e., C# supports `..` in collection initializers: ```cs int[] x = [1, 2, ..y]; ``` [21:23:53.0865] And other languages use `..` for ranges, i.e. `a..b`. [21:25:59.0197] If we hadn't needed to use `?.[` and `?.(` for chaining, I would have suggested `x.(f)()` or `x.[f]()` for call-this. That said, I still think `::` is perhaps a good choice for that. [21:29:51.0862] Though I had thought about using `::` for a syntactic event mechanism. It was used for events in a different way in early versions of IE/JScript: ```js function window::onload(e) { } ``` [21:35:41.0787] That reminds me too much of class syntax in C++ [21:40:06.0006] My thought was to use `event` to describe named events in a separate namespace (similar to `#`) and `::` to access them, e.g.: ```js class Button { event click; event dblclick; raiseClick(e) { this::click(e); // can only invoke 'click' using :: when lexically inside class } } const b = new Button(); b::click += e => { }; b::dblclick += e => { }; ``` [21:41:21.0564] and the whole syntax would leverage a symbol-based protocol that could be adapted to both NodeJS EventEmitter and DOM EventTarget (and any other event system in userland) [21:43:05.0758] If that were to ever make it to an actual proposal, I'm sure there are other symbols that could be used. `::` here is mostly JScript nostalgia. [21:43:27.0802] `::` for bind-this/call-this is also perfectly reasonable. [23:35:37.0224] > <@aclaymore:matrix.org> I'm blanking. What's the potential bug with the open paren? the bug is “i meant to type . and typed ..” or the reverse. Two syntaxes with quite different resolution semantics shouldn’t be that similar [01:00:04.0566] I like '::` for call-this. I don't think we need syntax for events in JS. [10:27:07.0852] I kind of feel like we’ve gone in circles over this. I already did a somewhat thorough review of what big open-source JS projects actually do for the current pipe proposal readme. Almost nobody actually uses `_ = …, _ = …`. What people *do* use is deeply nested parenthesized expressions, mostly for function calls and `await` expressions, but also sometimes for object literals, array literals, and template literals. We can say things, “They should just break up all of those up into statements with different variables,” or, “They can already reassign to a dummy variable,” but the fact remains that almost everyone *doesn’t* do this. They deeply nest expressions in consolidated statements as if they were method chains, except with lots of recursive parentheses. Many of the real-world examples I found are still in the readme. I do agree with Justin that the most important feature of Hack pipes is that, like other pipe styles and like call-this, it encourages DCE-friendly API use. I agree that it’s a mere secondary convenience that Hack pipes *do* accommodate mixing different styles like function calls, member access, method calls, object/array/template literals, and the like in a unified postfix style. Nevertheless, I do not agree that “people can already just use `_ = …, _ = …`; therefore Hack pipes are not beneficial”. The fact is that, even if developers already can use `,`, they simply they do not. They want a fluent syntax. [10:28:43.0313] * I kind of feel like we’ve gone in circles over this. I already did a somewhat thorough review of what big open-source JS projects actually do for the current pipe proposal readme. Almost nobody actually uses `_ = …, _ = …`. What people _do_ use is deeply nested parenthesized expressions, mostly for function calls and `await` expressions, but also sometimes for object literals, array literals, and template literals. We can say things, “They should just break up all of those up into statements with different variables,” or, “They can already reassign to a dummy variable,” but the fact remains that almost everyone _doesn’t_ do this. They deeply nest expressions in consolidated statements as if they were method chains, except with lots of recursive parentheses. Many of the real-world examples I found are still in the readme. I do agree with Justin that the most important feature of Hack pipes is that, like other pipe styles and like call-this, it encourages DCE-friendly API use. I agree that it’s a mere secondary convenience that Hack pipes _do_ accommodate mixing different styles like function calls, member access, method calls, object/array/template literals, and the like in a unified postfix style. Nevertheless, I do not agree that “people can already just use `_ = …, _ = …`; therefore Hack pipes are not beneficial”. The fact is that, even if developers already can use `,`, and even if they can already break every `await ` into a separate statement, they simply they do not. They continue to create poorly readable, deeply nested expressions everywhere. They want a fluent syntax that accommodates these somehow, whether it be Hack pipes or something else. [10:29:31.0684] * I kind of feel like we’ve gone in circles over this over the years. I already did a somewhat thorough review of what big open-source JS projects actually do for the current pipe proposal readme. Almost nobody actually uses `_ = …, _ = …`. What people _do_ use is deeply nested parenthesized expressions, mostly for function calls and `await` expressions, but also sometimes for object literals, array literals, and template literals. We can say things, “They should just break up all of those up into statements with different variables,” or, “They can already reassign to a dummy variable,” but the fact remains that almost everyone _doesn’t_ do this. They deeply nest expressions in consolidated statements as if they were method chains, except with lots of recursive parentheses. Many of the real-world examples I found are still in the readme. I do agree with Justin that the most important feature of Hack pipes is that, like other pipe styles and like call-this, it encourages DCE-friendly API use. I agree that it’s a mere secondary convenience that Hack pipes _do_ accommodate mixing different styles like function calls, member access, method calls, object/array/template literals, and the like in a unified postfix style. Nevertheless, I do not agree that “people can already just use `_ = …, _ = …`; therefore Hack pipes are not beneficial”. The fact is that, even if developers already can use `,`, and even if they can already break every `await ` into a separate statement, they simply they do not. They continue to create poorly readable, deeply nested expressions everywhere. They want a fluent syntax that accommodates these somehow, whether it be Hack pipes or something else. [10:33:28.0971] > Almost nobody actually uses `_ = ...` I don't expect they do. I do expect there are many instances of ```js const a = f1(x, ...) const b = f2(a, ...) const c = f3(b, ...) ``` in the wild, which is essentially the same thing. My point with `_ = ...` is that it readily reproduces Hack-style behavior with no new syntax. It greatly sours the value of Hack-style for me, as I'd initially hoped for a simple syntax to give me more power over function application (tacit or otherwise), which is a far more novel capability. [10:33:38.0045] * > Almost nobody actually uses `_ = ...` I don't expect they do. I do expect there are many instances of ```js const a = f1(x, ...) const b = f2(a, ...) const c = f3(b, ...) ``` in the wild, which is essentially the same thing. My point with `_ = ...` is that it readily reproduces Hack-style behavior with no new syntax. It greatly sours the value of Hack-style for me, as I'd initially hoped for a simple syntax to give me more power over function application (tacit or otherwise), which is a far more novel capability. [10:33:56.0081] * > Almost nobody actually uses `_ = ...` I don't expect they do. I do expect there are many instances of ```js const a = f1(x, ...) const b = f2(a, ...) const c = f3(b, ...) ``` in the wild, which is essentially the same thing. My point with `_ = ..., ` is that it readily reproduces Hack-style behavior with no new syntax. It greatly sours the value of Hack-style for me, as I'd initially hoped for a simple syntax to give me more power over function application (tacit or otherwise), which is a far more novel capability. [11:16:02.0042] I like that you wrote Elixir-favoring functions there 😉 [11:16:48.0164] First-arg favoring, maybe. My examples aren't intended to speak to a preference. [11:16:57.0247] * First-arg favoring, maybe, but examples aren't intended to speak to a preference. [11:18:28.0873] > They want a fluent syntax that accommodates these somehow, whether it be Hack pipes or something else Agree with this. I like that Hack feels like regular code sequences, and can break them up to be much more readable. I think people would use this more personally like, but the end result will still be readable code. [11:18:40.0692] Same same. [11:19:22.0730] * > They want a fluent syntax that accommodates these somehow, whether it be Hack pipes or something else Agree with this. I like that Hack feels like regular code sequences, and can break them up to be much more readable. I think people would use this more than I would personally like, but the end result will still be readable code. [11:21:39.0332] One place where F#-style aligns with JS is Stage 3 Decorators. In F# style, `x |> F` invokes `F` with `x`, and `x |> F(a)` invokes the result of `F(a)` with `x`. This is the same with decorators/decorator factories: `@F` invokes `F` with the decorator context, and `@F(a)` invokes the result of `F(a)` with the decorator context. [11:23:52.0685] And while we didn't propose the syntax, F#'s reverse pipe `<|` follows the same order as decorators: `@F @G` is essentially `F <| G <| x` [11:34:02.0841] Given my hatred of decorators, I don't think trying pipeline to the (reversed) execution model of decorators is a win. [11:35:02.0289] Especially, why did decorators need to be tied to closure buildup? [12:19:07.0848] It's less the reversed execution model and more the existing mental model related to decorator vs factory. [12:19:52.0608] > <@jridgewell:matrix.org> Especially, why did decorators need to be tied to closure buildup? Prior art from python and also that these only run during class declaration init 2026-03-21 [18:09:15.0482] To be clear, when I said X% I meant "X% of people who would otherwise be using class methods" not "X% of all JS developers". I have a hard time imagining that there is a large contingent of method-chainers that would prefer hack pipes over call-this, but I suppose it's technically possible. [18:42:01.0019] > the most important feature of Hack pipes is that, like other pipe styles and like call-this, it encourages DCE-friendly API use. 👏👏👏👏 I really strongly believe we need to stay focused on this particular point. It benefits basically all of humanity, and the poorest the most. [18:46:55.0499] Can I persuade anyone to update the call-this and pipeline repos to explicitly mention DCE as THE primary motivation, and others as secondary motivations? [19:13:53.0364] FWIW, I don't think this is a great argument since all other syntaxes face similar objections. E.g. other delegates have said things like: "I never want a beginner to wonder "which was it again? `.` or `~>`?" So I have been going forward with the assumption that some people are going to get mixed up sometimes and the important goal is "easy to fix" not "impossible to make a mistake". 1. Syntax constraints / autocomplete / linters / TS will all help prevent actual bugs sneaking in; 2. starting with a `.` makes it easy to discover both member methods and tree-shakable methods, and 3. if you mistype it manually, it's the easiest to recover from. One keypress to flip to the other option. 4. **Any** of these spellings will be pretty easy to learn for people who find themselves stuck with what we decide. They will just learn it and move on, or the AI will write the code for them and it won't matter anyways. We are never going to come up with something immediately intuitive to everyone. [19:14:05.0655] That said, your and Ron's objections are noted and I am happy to compromise on whatever syntax would get through committee. I continue to believe that call-this' main benefit is DCE, not spelling, and the main hindrance is the `this` bit, not the spelling. [19:17:14.0712] * FWIW, I don't think this is a great argument since all other syntaxes face similar objections. E.g. other delegates have said things like: "I never want a beginner to wonder "which was it again? `.` or `~>`?" So I have been going forward with the assumption that some people are going to get mixed up sometimes and the important goal is "easy to fix" not "impossible to make a mistake". 1. Syntax constraints / autocomplete / linters / TS will all help prevent actual bugs sneaking in; 2. starting with a `.` makes it easy to discover both member methods and tree-shakable methods, and 3. if you mistype it manually, it's the easiest to recover from. One keypress to flip to the other option. 4. IMO **any** of `::`, `..`, `~>`, etc will be fine to learn for people who find themselves stuck with what we decide. They will just learn it and move on... or the AI will write the code for them and it won't matter anyways. We are never going to come up with something immediately intuitive to everyone... [19:22:42.0052] * FWIW, I don't think this is a great argument since all other syntaxes face similar objections. E.g. other delegates have said things like: "I never want a beginner to wonder "which was it again? `.` or `~>`?" So I have been going forward with the assumption that some people are going to get mixed up sometimes and the important goal is "easy to fix" not "impossible to make a mistake". 1. Syntax constraints / autocomplete / linters / TS will all help prevent actual bugs sneaking in; 2. starting with a `.` makes it easy to discover both member methods and tree-shakable methods, and 3. if you mistype it manually, it's the easiest to recover from. One keypress to flip to the other option. 4. I was hoping using another character familiar to method-chainers `.` would make the switch feel "simple" or at least "not as scary". 5. IMO **any** of `::`, `..`, `~>`, etc will be fine to learn for people who find themselves stuck with what we decide. They will just learn it and move on... or the AI will write the code for them and it won't matter anyways. We are never going to come up with something immediately intuitive to everyone... [19:24:08.0762] * FWIW, I don't think this is a great argument since all other syntaxes face similar objections. E.g. other delegates have said things like: "I never want a beginner to wonder "which was it again? `.` or `~>`?" So I have been going forward with the assumption that some people are going to get mixed up sometimes and the important goal is "easy to fix" not "impossible to make a mistake". 1. Syntax constraints / autocomplete / linters / TS will all help prevent actual bugs sneaking in; 2. starting with a `.` makes it easy to discover both member methods and tree-shakable methods, and 3. if you mistype it manually, it's the easiest to recover from. One keypress to flip to the other option. 4. I was hoping using another character familiar to method-chainers `.` would make the switch feel "simple" or at least "not as scary". To invoke a method, use `.`; to invoke an extension method, use `..` (extend the dot). Easy. 5. IMO **any** of `::`, `..`, `~>`, etc will be fine to learn for people who find themselves stuck with what we decide. They will just learn it and move on... or the AI will write the code for them and it won't matter anyways. We are never going to come up with something immediately intuitive to everyone... [19:33:16.0795] * FWIW, I don't think this is a great argument since all other syntaxes also face similar "I got them mixed up" objections. E.g. other delegates have said things like: "I never want a beginner to wonder "which was it again? `.` or `~>`?" So I have been going forward with the assumption that some people are going to get mixed up sometimes and the important goal is "easy to fix" not "impossible to make a mistake". 1. Syntax constraints / autocomplete / linters / TS will all help prevent actual bugs sneaking in; 2. starting with a `.` makes it easy to discover both member methods and tree-shakable methods, and 3. if you mistype it manually, it's the easiest to recover from. One keypress to flip to the other option. 4. I was hoping using another character familiar to method-chainers `.` would make the switch feel "simple" or at least "not as scary". To invoke a method, use `.`; to invoke an extension method, use `..` (extend the dot). Easy. 5. IMO **any** of `::`, `..`, `~>`, etc will be fine to learn for people who find themselves stuck with what we decide. They will just learn it and move on... or the AI will write the code for them and it won't matter anyways. We are never going to come up with something immediately intuitive to everyone... [19:33:37.0804] * FWIW, I don't think this is a great argument since all other syntaxes also face similar "I got them mixed up" objections. E.g. other delegates have said things like: "I never want a beginner to wonder "which was it again? `.` or `~>`?" So I have been going forward with the assumption that some people are going to get mixed up sometimes and the important goal is "easy to fix" not "hard to make a mistake". 1. Syntax constraints / autocomplete / linters / TS will all help prevent actual bugs sneaking in; 2. starting with a `.` makes it easy to discover both member methods and tree-shakable methods, and 3. if you mistype it manually, it's the easiest to recover from. One keypress to flip to the other option. 4. I was hoping using another character familiar to method-chainers `.` would make the switch feel "simple" or at least "not as scary". To invoke a method, use `.`; to invoke an extension method, use `..` (extend the dot). Easy. 5. IMO **any** of `::`, `..`, `~>`, etc will be fine to learn for people who find themselves stuck with what we decide. They will just learn it and move on... or the AI will write the code for them and it won't matter anyways. We are never going to come up with something immediately intuitive to everyone... [19:34:41.0100] * FWIW, I don't think this is a great argument since all other syntaxes also face similar "I got them mixed up" objections. E.g. other delegates have said things like: "I never want a beginner to wonder "which was it again? `.` or `~>`?" So I have been going forward with the assumption that some people are going to get mixed up sometimes and the important goal is "easy to fix" not "hard to make a mistake". 1. Syntax constraints / autocomplete / linters / TS will all help prevent actual bugs sneaking in; 2. starting with a `.` makes it easy to discover both member methods and tree-shakable methods; I never have to wonder where to start since it's the same character either way 3. if you mistype it, it's the easiest to recover from. One keypress to flip to the other option. 4. I was hoping using another character familiar to method-chainers `.` would make the switch feel "simple" or at least "not as scary". To invoke a method, use `.`; to invoke an extension method, use `..` (extend the dot). Easy. 5. IMO **any** of `::`, `..`, `~>`, etc will be fine to learn for people who find themselves stuck with what we decide. They will just learn it and move on... or the AI will write the code for them and it won't matter anyways. We are never going to come up with something immediately intuitive to everyone... [19:51:07.0972] Another aspect of this discussion that I think merits consideration, given a goal of **persuading method users to switch to something more tree-shakable**: precedence. Methods are tight, which means there are several cases where migrating from normal method calls to a "loose" pipeline is... awkward. As a simple example, take any awaited method call: ```js // Class methods await this.deletePost(id); // Call-this await this..deletePost(id); // Easy peasy // Hack pipe await this |> deletePost(##, id); // Oops! this |> await deletePost(##, id); // No more bug, but this is pretty different from method chaining at this point... await (this |> deletePost(##, id)); // No more bug, but oof, we're back to paren-balancing... ``` Has this issue been discussed yet? [21:38:28.0657] Another example is arithmetic between the results of two method calls, based on real example for our codebase: ```js // Class methods this.fn1() + this.fn2(arg1, arg2); // Call this this..fn1() + this..fn2(arg1, arg2); // Hack (this |> fn1(##)) + (this |> fn2(##, arg1, arg2)); // Plain functions fn1(this) + fn2(this, arg1, arg2); ``` [21:40:20.0762] Boolean expression, also based on a real example: ```js // Class methods foo && !bar.isBar() // Call-this foo && !bar..isBar() // Hack foo && !(bar |> isBar(##)) // Plain functions foo && !isBar(bar) ``` [21:41:06.0879] * Boolean expression, also based on a real example: ```js // Class methods if (foo && !bar.isBar()) { ... } // Call-this if (foo && !bar..isBar()) { ... } // Hack if (foo && !(bar |> isBar(##))) { ... } // Plain functions if (foo && !isBar(bar)) { ... } ``` [21:48:37.0845] * Another aspect of this discussion that I think merits consideration, given a goal of **persuading method users to switch to something more tree-shakable**: precedence. Methods are tight, which means there are several cases where migrating from normal method calls to a "loose" pipeline is... awkward. As a simple example, take any awaited method call: ```js // Class methods await this.deletePost(id); // Call-this / tight-elixir await this..deletePost(id); // Easy peasy // Hack await this |> deletePost(##, id); // Oops! this |> await deletePost(##, id); // No more bug, but this is pretty different from method chaining at this point... await (this |> deletePost(##, id)); // No more bug, but oof, we're back to paren-balancing... ``` Has this issue been discussed yet? [21:48:52.0643] * Another example is arithmetic between the results of two method calls, based on real example for our codebase: ```js // Class methods this.fn1() + this.fn2(arg1, arg2); // Call-this / tight-elixir this..fn1() + this..fn2(arg1, arg2); // Hack (this |> fn1(##)) + (this |> fn2(##, arg1, arg2)); // Plain functions fn1(this) + fn2(this, arg1, arg2); ``` [21:49:10.0724] * Boolean expression, also based on a real example: ```js // Class methods if (foo && !bar.isBar()) { ... } // Call-this / tight-elixir if (foo && !bar..isBar()) { ... } // Hack if (foo && !(bar |> isBar(##))) { ... } // Plain functions if (foo && !isBar(bar)) { ... } ``` [03:21:55.0342] re: "going in circles". [03:23:58.0900] this is how I felt with the R&T proposal.The University Of Bergen h [03:25:24.0000] * this is how I felt with the R&T proposal and created https://acutmore.github.io/record-tuple-laboratory/ to effectively show there is no design that will make everyone happy. The University Of Bergen wrote a paper on this https://dl.acm.org/doi/10.1145/3732771.3742715