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

Proposal iteration plan: remove auto adaption of util functions #12

Open
hax opened this issue Jun 1, 2022 · 0 comments
Open

Proposal iteration plan: remove auto adaption of util functions #12

hax opened this issue Jun 1, 2022 · 0 comments

Comments

@hax
Copy link
Member

hax commented Jun 1, 2022

Current design and the pitfalls

value::X:foo(...args) currently support both constructors and namespace objects by default.

For constructors, it works as X.prototype.foo.call(value, ...args). For namespace objects, it works as X.foo.call(X, value, ...args).

Such design is try to reuse the ecosystem of util-function style libraries like underscore/lodash, make them just work without any modification, but have some issues:

  1. Not easy to explain and cause confusion.
  2. Though using first arg as receiver matches the most real-world util-function usage, there are some cases should use ...args as receiver (eg. max(...args) -> args::max()), and there are rare cases (lodash/fp) use last arg as receiver, so still need manually customization.
  3. Constructors could also have static methods, normally they are util functions (so the constructor also behave as namespace objects), theoretically there could be prototype methods and static methods with same name, which take precedence? Actually classical (not es module version) underscore/lodash already have such conflict (prototype methods for oop style or seq methods), to make it work as expect (most people only use util-function style), we need to make static methods take precedence, but it may not fit for other cases.
  4. Finally, if the author customize the adaptation (via Symbol.extension hook) later, bring potential breaking change. This is especially bad for built-in/platform APIs because they do not have versioning.
  5. To make module namespace work, import ::{foo} from "m" does not desugar to import {foo} from "m"; const ::foo = foo, but desugar to import * as m from "m"; const ::{foo} from m. This works but too subtle.
  6. Some authors want full control of how their library could be used and it's annoying to get the bug report because of possible misuse of their libraries as extensions.

As the feedbacks and the analysis, adapting the util-function style to method style is good, but adapt them automatically is not good. Even without such auto adaption, we already have symbol-based customization, if people want to use exist libraries as extensions, they always can adapt them manually, so removing auto adaptation won't lose power too much. The problem is how to make the customization easy, and optimize for common cases.


Rough Intro of the new design

X can't be used as extension if X[Symbol.extension] is falsy, aka. value::X:foo always throw TypeError, const ::{foo} from X also throw.

Constructors still can be used as extensions (from prototype methods) by default, via built-in Function.prototype[Symbol.extension]. Authors could customize the behavior of their classes if they want, or even use C[Symbol.extension] = null to nullify it.

To use namespace object as extensions, you need to specify it explicitly: X[Symbol.extension] = {}. And each functions on X need to mark itself could be used as extension methods/accessors, or use X[Symbol.extension] = { config: ... } to provide these settings. (see below for details)

This proposal only specify Module Namespace objects have built-in [Symbol.extension] property to allow them be used as extension (from exported functions). Follow-on proposals and web APIs could add [Symbol.extension] property to other built-ins or platform APIs.


By default functions can't be used as extension methods directly. This avoid the misuse of non-method functions, and avoid the confusion of free methods which some delegates (include myself) worry about.

Previously, the proposal already rule out arrow functions, bound functions and constructors to avoid misuse, but can't avoid free methods totally.

const ::getX = function getX(o) { return o.x } // previously allowed, now throw TypeError

In the new design, we unify the form of extension method and accessor to {invoke, get, set} descriptor:

const ::getX = { invoke() { return this.x } }
const ::x = { get() { return this.x } }
let o = {x: 42}
o::getX() // 42
o::x // 42

To simplify the adaptation of functions to extension methods, we introduce a new well-known symbol Symbol.invokeStyle to allow authors define if/how a function could be invoked as an extension method.

function getX(o) { return o.x }
// if invoked as method, the receiver would be pass to getX as the first argument
getX[Symbol.invokeStyle] = {receiver: "first"}
// other possible values are {receiver: "last"}, {receiver: "spread"}
// {receiver: "first"} is the default value, so could just write
// getX[Symbol.invokeStyle] = {}

const ::getX = getX
let o = {x: 42}
o::getX() // 42

Could also specify it to be used as getter:

function getX(o) { return o.x }
getX[Symbol.invokeStyle] = {kind: "get"} // receiver default to "first"

const ::x = getX
let o = {x: 42}
o::x // 42

It would be simple if we have function decorators in the future:

@Extension.get
function getX(o) { return o.x }

Another exception will be functions with this parameter if this parameter proposal could advance, because this parameter already explicitly denote it's a method.

const ::getX = function (this) { return this.x } // ok

Consider lodash.isEqual as example:

import {isEqual} from "lodash"
let v = {x: 1}
isEqual(v, {x: 1}) // true

To use it as extension method, it's trivial to convert isEqual to a extension method:

import {isEqual} from "lodash"
const ::isEqual = { invoke(that) { return isEqual(this, that) } }
let v = {x: 1}
v::isEqual({x: 1}) // true

A much better solution is allowing the maintainers of lodash to define the adaptation:

// lodash source
function isEqual() { ... }
isEqual[Symbol.invokeStyle] = {}

// usage
import ::{isEqual} from "lodash"
let v = {x: 1}
v::isEqual({x: 1}) // true

// or
import * as lodash from "lodash"
let v = {x: 1}
v::lodash:isEqual({x: 1}) // true

Note, if isEqual do not have [Symbol.invokeStyle] (or falsy), import ::{isEqual} and v::lodash:isEqual() would throw TypeError. This protect the users from misuse as early as possible.


To enable normal objects which is a collection of some util functions to be used as extensions like module namespace object:

const util = {
  first(arrLike) { return arrLike[0] },
  last(arrLike) { return arrLike[arrLike.length - 1] },
  isEqual(a, b) { ... },
  ...
}
util.first[Symbol.invokeStyle] = {kind: "get"}
util.last[Symbol.invokeStyle] = {kind: "get"}
util.isEqual[Symbol.invokeStyle] = {}
util[Symbol.extension] = {}

const ::{first, last, isEqual} from util
let a = [1,2,3]
a::first // 1
a::last // 3
a::isEqual([1,2,3]) // true

Add [Symbol.invokeStyle] to each util functions is wordy, so we allow [Symbol.extension] to denote the mapping directly:

const util = {
  first(arrLike) { return arrLike[0] },
  last(arrLike) { return arrLike[arrLike.length - 1] },
  isEqual(a, b) { ... },
  ...
  [Symbol.extension]: {
    config: {
      first: {kind: "get"},
      last: {kind: "get"},
      "*": {receiver: "first"},
    }
  }
}

Summary

With all these modification, we solve the related issues and still keep the goal of this proposal, allow current libraries in the ecosystem could be upgraded to support extensions easily, allow users import same API in the style as they need (extensions or util-functions style) without manually conversion. Note it's important to avoid two set of APIs of same features (lodash/fp is an example of such failure), which call-this proposal also noticed but can't solve well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant