Skip to content

Instantly share code, notes, and snippets.

@Schniz
Last active August 19, 2024 14:07
Show Gist options
  • Save Schniz/d7789a8c8962b87664c3e9745be3b985 to your computer and use it in GitHub Desktop.
Save Schniz/d7789a8c8962b87664c3e9745be3b985 to your computer and use it in GitHub Desktop.
Trait Call Expression Proposal

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:

Trait Call Expression

Stage 0

Authors

Gal Schlezinger

Motivation

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:

  1. Enabling better tree-shaking and bundling optimization by keeping functions separate from object prototypes.
  2. Allowing developers to extend object behavior without modifying prototypes, which can lead to conflicts and unexpected side effects.
  3. Facilitating left-to-right function composition, which can improve code readability.

Proposed Solution

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.

Syntax

object::function(arg1, arg2, ...)

This syntax is equivalent to:

function.call(object, arg1, arg2, ...)

Examples

import { calculateArea } from './geometry';

const circle = { radius: 5 };
const area = circle::calculateArea();

// Equivalent to:
// const area = calculateArea.call(circle);

Detailed Design

Semantics

  1. The left-hand side of :: is evaluated and becomes the this context for the function call.
  2. The right-hand side of :: must be a function reference.
  3. Any arguments provided in parentheses after the function reference are passed to the function as-is.

Precedence

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

Limitations

  1. The right-hand side of :: must be a direct function reference. It cannot be an arbitrary expression.
  2. The trait call expression cannot be used with arrow functions or other functions that don't allow this binding.

Potential Issues and Challenges

  1. Ensuring that the new syntax doesn't conflict with existing or proposed JavaScript features.
  2. Educating developers on the differences between this syntax and the full bind operator proposal.
  3. Implementing the feature efficiently in JavaScript engines.

Open Questions

Handling Nested Functions

The current proposed syntax doesn't directly address functions that are properties of other objects. We have a few options to consider:

  1. Allow a MemberExpression to be an invoked function binding:
    const value = Option.some(1)::Option.map(v => v + 1)
    This should desugar just like:
    const value = Option.some(1)::(Option.map)(v => v + 1)
  2. 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)
  3. Require destructuring or local binding:
    const { map } = Option;
    const value = Option.some(1)::map(v => v + 1)
    This maintains the simplicity of the original proposal but requires an extra step and forces you to have unique names for bindings.

Each of these options has pros and cons in terms of readability, consistency, and complexity of the proposal.

Implementation

A polyfill for this syntax could be implemented using a Babel plugin or similar tools. Native implementation would require changes to JavaScript engines.

References

  1. Bind Operator Proposal
  2. Function.prototype.call()

Appendix

Differences from Pipe Operator Proposal

The Trait Call Expression proposal differs from the Pipe Operator proposal in several key aspects:

  1. It's simpler and more focused, addressing only method invocation with this binding.
  2. The syntax is familiar to developers from other languages (e.g., Rust, Ruby).
  3. It doesn't introduce new conceptual overhead, closely mirroring Function.prototype.call().
  4. It allows for extending object behavior without modifying prototypes.
  5. 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.

Examples

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.

@datner
Copy link

datner commented Aug 19, 2024

How would this look?

Effect.succeed(Option.some(1)).pipe(
  Effect.map(
    Option.map(
      increment
    )
  )
)

Like this?

Effect.succeed(Option.some(1))
  ::Effect.map(
    Option.map( // how do I bind the nested `this` option?
      increment // or is Number::increment preferred? Why?
    )
  )
)

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment