Note
This document was mostly generated by Claude 3.5 based on my wishes
Here's a draft TC39 proposal for "Trait Call Expression" based on your requirements:
Gal Schlezinger
The Trait Call Expression proposal aims to provide a concise syntax for invoking methods on objects without modifying their prototypes. It is inspired by the Stage 0 Bind Operator proposal but focuses solely on the function invocation use case.
This proposal addresses several common pain points in JavaScript development:
- Enabling better tree-shaking and bundling optimization by keeping functions separate from object prototypes.
- Allowing developers to extend object behavior without modifying prototypes, which can lead to conflicts and unexpected side effects.
- Facilitating left-to-right function composition, which can improve code readability.
We propose introducing a new operator ::
for trait call expressions. This operator would allow developers to invoke a function with a specific this
context without using the .call()
method explicitly.
object::function(arg1, arg2, ...)
This syntax is equivalent to:
function.call(object, arg1, arg2, ...)
import { calculateArea } from './geometry';
const circle = { radius: 5 };
const area = circle::calculateArea();
// Equivalent to:
// const area = calculateArea.call(circle);
- The left-hand side of
::
is evaluated and becomes thethis
context for the function call. - The right-hand side of
::
must be a function reference. - Any arguments provided in parentheses after the function reference are passed to the function as-is.
The ::
operator would have higher precedence than function calls but lower precedence than member expressions. This leads to the following behavior:
a.b::c(d) // Equivalent to c.call(a.b, d)
a::b(c).d // Equivalent to b.call(a, c).d
- The right-hand side of
::
must be a direct function reference. It cannot be an arbitrary expression. - The trait call expression cannot be used with arrow functions or other functions that don't allow
this
binding.
- Ensuring that the new syntax doesn't conflict with existing or proposed JavaScript features.
- Educating developers on the differences between this syntax and the full bind operator proposal.
- Implementing the feature efficiently in JavaScript engines.
The current proposed syntax doesn't directly address functions that are properties of other objects. We have a few options to consider:
- Allow a MemberExpression to be an invoked function binding:
This should desugar just like:
const value = Option.some(1)::Option.map(v => v + 1)
const value = Option.some(1)::(Option.map)(v => v + 1)
- Create a new syntax for this specific case that is similar to Rust:
const value = Option.some(1)::Option::map(v => v + 1) const value = Option.some(1)::<Option>::map(v => v + 1)
- Require destructuring or local binding:
This maintains the simplicity of the original proposal but requires an extra step and forces you to have unique names for bindings.
const { map } = Option; const value = Option.some(1)::map(v => v + 1)
Each of these options has pros and cons in terms of readability, consistency, and complexity of the proposal.
A polyfill for this syntax could be implemented using a Babel plugin or similar tools. Native implementation would require changes to JavaScript engines.
The Trait Call Expression proposal differs from the Pipe Operator proposal in several key aspects:
- It's simpler and more focused, addressing only method invocation with
this
binding. - The syntax is familiar to developers from other languages (e.g., Rust, Ruby).
- It doesn't introduce new conceptual overhead, closely mirroring
Function.prototype.call()
. - It allows for extending object behavior without modifying prototypes.
- The syntax is less intimidating and works naturally with
this
-dependent methods.
While both proposals support left-to-right composition, the Trait Call Expression is specifically tailored for method-like invocations, complementing rather than competing with the Pipe Operator.
In Effect.ts, the common way of interacting with data types is through .pipe
:
Effect.succeed(1)
.pipe(Effect.map(x => x + 1)) // Effect(2)
Trait Call Expression would allow to write this code as Effect.succeed(1)::map(x => x + 1)
. However, instead of having a curried function, map
would receive a value like
function map<T, R>(this: Effect<T>, fn: (t: T) => R): Effect<R>;
This proposal draft outlines the core idea, motivation, and some initial design considerations for the Trait Call Expression. It would need further refinement and discussion within the TC39 committee to progress through the stages of the TC39 process.
How would this look?
Like this?
Also how would this feature be taught and promoted? Who should implement this? When? With what mental model?
Loving this proposal btw, this is much less controversial than pipe imho