Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use case: Individually importable, tree-shakable methods #6

Closed
zakhenry opened this issue Sep 27, 2021 · 5 comments
Closed

Use case: Individually importable, tree-shakable methods #6

zakhenry opened this issue Sep 27, 2021 · 5 comments
Labels
documentation Improvements or additions to documentation help wanted Extra attention is needed question Further information is requested

Comments

@zakhenry
Copy link

Please correct me if I am wrong, but my understanding of this proposal has me thinking that the real power of it is the extension function capability, as it would allow for libraries like rxjs and other functional libraries to provide free functions (that are tree shakeable) that operate on this. Additionally, it works well as a local builder pattern for a foreign object.

RxJS example:

Before

const searchResults$ = fromEvent(document.querySelector('input'), 'input').pipe(
  map(event => event.target.value),
  filter(searchText => searchText.length > 2),
  debounce(300),
  distinctUntilChanged(),
  switchMap(searchText => queryApi(searchText).pipe(retry(3))),
  share(),
)

After

const searchResults$ = fromEvent(document.querySelector('input'), 'input')
  -> map(event => event.target.value)
  -> filter(searchText => searchText.length > 2)
  -> debounce(300)
  -> distinctUntilChanged()
  -> switchMap(searchText => queryApi(searchText).pipe(retry(3)))
  -> share()

where a function like map has the signature

function map<T, R>(this: Observable<T>, project: (value: T, index: number) => R): Observable<R> {
  // ...
}

Builder example:

// imagine this class is external library code
class UrlBuilder {

   constructor(url) {
     this.url = new URL(url)
   }

   build(): string {
     return this.url.toString()
   }
   
	setHost(host: string): this {
       this.url.host = host;
       return this;
    }

}

// and our own local code provides extensions to it
function toUrlBuilder(this: string): UrlBuilder {
   return new UrlBuilder(this);
}

function addQueryParam(this: UrlBuilder, name: string, value: string): UrlBuilder {
   this.url.searchParams.append(name, value);
   return this
}

then we can use the proposed bind syntax to augment the existing library with our new function in a fluent chaining way

const url: string = "https://github.com/js-choi/proposal-bind-operator" -> toUrlBuilder() // local extension 
.setHost("google.com") // external library member
->addQueryParam("s", "TC39 bind proposal") // local extension
.build() // external library member

I feel like this addresses a number of the concerned use cases around the Hack style pipeline operator

@js-choi js-choi added the question Further information is requested label Sep 28, 2021
@js-choi
Copy link
Collaborator

js-choi commented Sep 28, 2021

It’s true: this proposal would allow developers to use individually importable (tree-shakable) “generic” methods more ergonomically and fluently.

Generic methods are already an existing and common pattern in JavaScript, of course. The pattern is even built into the language itself: Array.from is a generic factory method that can be transplanted into other array-like classes.

The explainer currently focuses on the security use case because that use case’s benefits might be the clearest to TC39. But I have been planning on adding a section to the explainer about generic methods for a while.


With regards to Hack pipes, there is a lot of history behind its relationship with bind operators.

During 2015–2017, people debated a lot about how the old proposed bind operator :: could serve as a “pipe” operator in JavaScript (e.g., a::f(b, c)::g(d, e) would be a “pipeline”, equivalent to g.call(f.call(a, b, c), d, e)).

However, parts of TC39 objected to promoting this as the primary way to create pipelines, saying that using this is strange outside of methods, and that the bind operator as a pipe operator would promote lots of small inline this-based functions (like F# pipes, which have had their own setbacks, but without even arrow functions). This bind-as-pipe debate, both online in the :: repository and also offline, was bogged down in circles, and it was a major reason why that proposal stagnated.

So I’ve wanted to try to avoid that old debate. We have a clear-cut security use case for a bind operator, and we’ll focus TC39 on that security use case. Meanwhile, we’ll present the bind operator as independent of the pipe operator, whose fundamental function (as the pipe champion group is presenting it) is flattening and linearizing deeply nested expressions.


But we definitely can and should present individually importable generic methods as another use case: a pattern that is already common in JavaScript. I would welcome any existing examples from high-impact open-source libraries. (My understanding is that @benlesh had been originally planning for RxJS to use this pattern while waiting for the bind operator that did not come. I would welcome his expertise and insight here, too.)

@js-choi js-choi changed the title Builder pattern / pipe use-case Use case: Individually importable generic methods Sep 28, 2021
@js-choi js-choi added documentation Improvements or additions to documentation help wanted Extra attention is needed labels Sep 28, 2021
@benlesh
Copy link

benlesh commented Sep 28, 2021

The original catalyst for RxJS moving to someObservable.pipe(map, filter, mergeMap) etc was because we needed a way to provide our "operators" in a left-to-right chainable way that was tree-shakable. Originally we had everything implemented as methods that could be patched onto the prototype by importing modules. This worked okay but it came with a LOT of problems. This was mostly done with the hopes that :: would be standardized (You can actually see it in the README of the 5.x branch). But when we realized that was never going to happen, we moved to functional pipes.

@js-choi
Copy link
Collaborator

js-choi commented Sep 29, 2021

@benlesh: Thanks for your insight! Was that RxJS v5 system (its individually importable this-based functions) initially inspired by another existing library, or is this system unique as far as you know?

I’m trying to assess how generally common this pattern already is, given that this proposal would greatly lubricate it.

@benlesh
Copy link

benlesh commented Sep 29, 2021

I'm not sure how often this technique is used, considering its drawbacks. However it was important enough that TypeScript added a feature around it to support modules augmenting interfaces in maybe TS 2.1 or so (I don't remember the exact version).

@js-choi js-choi changed the title Use case: Individually importable generic methods Use case: Individually importable methods Sep 30, 2021
@js-choi js-choi changed the title Use case: Individually importable methods Use case: Individually importable, tree-shakable methods Sep 30, 2021
js-choi added a commit that referenced this issue Oct 6, 2021
@js-choi
Copy link
Collaborator

js-choi commented Oct 6, 2021

The explainer now deemphasizes the security use case.

Based on feedback from some TC39 representatives (see #8, some discussion on Matrix, and pushback that the similar Extensions proposal has gotten), I’ve become reluctant to emphasize individually importable, tree-shakable quasi-extension methods. I’m afraid that emphasizing them as a use case would make several representatives colder toward this proposal.

Instead, the explainer now simply emphasizes the high frequency and the high clunkiness of .bind, .call, and .apply. These do include some examples of tree-shakable, individually importable quasi-extension methods, such as:

// graceful-fs-4.1.15.tgz/polyfills.js
return fs$read.call(fs, fd, buffer, offset, length, position, callback)
return fs->fs$read(fd, buffer, offset, length, position, callback)

Hopefully, this makes sense and, hopefully, the explainer is more compelling than it had been before.

js-choi added a commit that referenced this issue Oct 6, 2021
js-choi added a commit that referenced this issue Oct 6, 2021
js-choi added a commit that referenced this issue Oct 6, 2021
@js-choi js-choi closed this as completed Oct 6, 2021
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Nov 12, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
documentation Improvements or additions to documentation help wanted Extra attention is needed question Further information is requested
Projects
None yet
Development

No branches or pull requests

3 participants