Function helpers for JavaScript

Withdrawn ECMAScript Stage-0 Proposal. J. S. Choi, 2021.

On 2021-10, proposal-function-helpers was presented to the Committee plenary for Stage 1. The Committee rejected the proposal due to its being overly broad and requests that it be split up into multiple proposals. This proposal is therefore withdrawn and split into proposal-function-pipe-flow and proposal-function-un-this.

Original proposal

Several useful, common helper functions are defined, downloaded, and used a lot. We should standardize at least some of them. This proposal is seeking Committee consensus for Stage 1: that standardizing at least some Function helpers is “worth investigating”. It is not seeking to standardize every imaginable helper function: just a selected few frequently used functions. Choosing which functions to standardize would be bikeshedding for Stage 2. Alternatively, the Committee could request that this proposal be split up into multiple proposals.

These convenience functions are simple, and they can be reimplemented easily in userspace. So why standardize them? Because:

  1. These helper functions are commonly used and universally useful. Each function is frequently downloaded from NPM, despite their being easily reimplementable. This is to be expected: after all, every JavaScript developer needs to manipulate callbacks, but they often do not wish to write the utilities themselves.
  2. Standardization would improve developer ergonomics. If we find ourselves needing these functions in a REPL or script, instead of having to download an external package or pasting in a definition into our own code, we can simply destructure the Function object.
  3. Standardization would improve code clarity. There would be one standard name for each of these functions, rather than various names from various libraries that refer to the same thing.

Unlike new syntax, standardized helper functions are relatively lightweight ways to improve the experience of all developers. These helper functions are well-trodden cowpaths, each of which deserves consideration for standardization.

The following functions are only possibilities. Choosing which functions to standardize would be bikeshedding for Stage 2.

Function.flow

The Function.flow static method creates a new function by combining several callbacks.

Function.flow(...fns);

const { flow } = Function;

const f = flow(f0, f1, f2);
f(5, 7); // f2(f1(f0(5, 7))).

const g = flow(g0);
g(5, 7); // g0(5, 7).

const h = flow();
h(5, 7); // 5.

The following real-world examples originally used lodash.flow.

// From gatsby@3.14.3/packages/gatsby-plugin-sharp/src/plugin-options.js:
flow(
  mapUserLinkHeaders(pluginData),
  applySecurityHeaders(pluginOptions),
  applyCachingHeaders(pluginData, pluginOptions),
  mapUserLinkAllPageHeaders(pluginData, pluginOptions),
  applyLinkHeaders(pluginData, pluginOptions),
  applyTransfromHeaders(pluginOptions),
  saveHeaders(pluginData)
)

// From strapi@3.6.8
// packages/strapi-admin/services/permission/permissions-manager/query-builers.js:
const transform = flow(flattenDeep, cleanupUnwantedProperties);

// From semantic-ui-react@v2.0.4/docs/static/utils/getInfoForSeeTags.js:
const getInfoForSeeTags = flow(
  _.get('docblock.tags'),
  _.filter((tag) => tag.title === 'see'),
  _.map((tag) => {
  }),
)

Any function created by Function.flow applies its own arguments to its leftmost callback. Then that result is applied to its next callback. In other words, function composition occurs from left to right.

The leftmost callback may have any arity, but any subsequent callbacks are expected to be unary.

If Function.flow receives no arguments, then, by default, it will return Function.identity (which is defined later in this proposal).

Precedents include:

Function.flowAsync

The Function.flowAsync static method creates a new function by combining several potentially async callbacks; the created function will always return a promise.

Function.flowAsync(...fns);

const { flowAsync } = Function;

// (...args) => Promise.resolve(x).then(f0).then(f1).then(f2).
flowAsync(f0, f1, f2);

const f = flowAsync(f0, f1, f2);
await f(5, 7); // await f2(await f1(await f0(5, 7))).

const g = flowAsync(g0);
await g(5, 7); // await g0(5, 7).

const h = flowAsync();
await h(5, 7); // await 5.

Any function created by Function.flowAsync applies its own arguments to its leftmost callback. Then that result is awaited before being applied to its next callback. In other words, async function composition occurs from left to right.

The leftmost callback may have any arity, but any subsequent callbacks are expected to be unary.

If Function.flowAsync receives no arguments, then, by default, it will return Promise.resolve.

The name “flow” comes from lodash.flow. (The name compose would be confusing with other languages’ RTL function composition.)

Function.pipe

The Function.pipe static method applies a sequence of callbacks to a given input value, returning the final callback’s result.

Function.pipe(input, ...fns);

const { pipe } = Function;

// f2(f1(f0(5))).
pipe(5, f0, f1, f2);

// 5.
pipe(5);

// undefined.
pipe();

The following real-world examples originally used fp-ts’s pipe function.

// From @gripeless/pico@1.0.1/source/inline.ts:
return pipe(
  download(absoluteURL),
  mapRej(downloadErrorToDetailedError),
  chainFluture(responseToBlob),
  chainFluture(blobToDataURL),
  mapFluture(dataURL => `url(${dataURL})`)
)

// From StoplightIO Prism v4.5.0 packages/http/src/validator/validators/body.ts:
return pipe(
  specs,
  A.findFirst(spec => !!typeIs(mediaType, [spec.mediaType])),
  O.alt(() => A.head(specs)),
  O.map(content => ({ mediaType, content }))
);

The first callback is applied to input, then the second callback is applied to the first callback’s result, and so forth. In other words, function piping occurs from left to right.

Each callback is expected to be a unary function.

If Function.pipe receives only one argument, then it will return input by default.
If Function.pipe receives no arguments, then it will return undefined.

Precedents include:

  • fp-ts: import { pipe } from 'fp-ts/function';

What happened to the F# pipe operator?

F#, Haskell, and other languages that are based on auto-curried unary functions have a tacit-unary-function-application operator. The pipe champion group has presented F# pipes for Stage 2 twice to TC39, being unsuccessful both times due to pushback from multiple other TC39 representatives’ memory performance concerns, syntax concerns about await, and concerns about encouraging ecosystem bifurcation/forking. (For more information, see the pipe proposal’s HISTORY.md.)

Given this reality, TC39 is much more likely to pass a Function.pipe helper function than a similar syntactic operator.

Standardizing a helper function does not preclude standardizing an equivalent operator later. For example, TC39 standardized binary ** even when Math.pow existed.

In the future, we might try to propose a F# pipe operator, but we would like to try proposing Function.pipe first, in an effort to bring its benefits to the wider JavaScript community as soon as possible.

Function.pipeAsync

The Function.pipeAsync static method applies a sequence of potentially async callbacks to a given input value, returning a promise. The promise will resolve to the final callback’s result.

Function.pipeAsync(input, ...fns);

const { pipeAsync } = Function;

// Promise.resolve(5).then(f0).then(f1).then(f2).
pipeAsync(5, f0, f1, f2);

// Promise.resolve(5).
pipeAsync(5);

// Promise.resolve(undefined).
pipeAsync();

The input is first awaited. Then the first callback is applied to input and then awaited, then the second callback is applied to the first callback’s result then awaited, and so forth. In other words, function piping occurs from left to right.

Each callback is expected to be a unary function.

If any callback returns a promise that then rejects with an error, then the promise returned by Function.pipeAsync will reject with the same error.

If Function.pipeAsync receives only one argument, then it will return Promise.resolve(input) by default.
If Function.pipeAsync receives no arguments, then it will return Promise.resolve(undefined).

Function.constant

The Function.constant static method creates a new function from a constant value. The new function will always return that value, no matter what arguments it is given.

Function.constant(value);

const { constant } = Function;

const f = constant(5);
f(11, 0, 3); // 5.

const g = constant();
g(11, 0, 3); // undefined.

The following real-world examples originally used lodash.constant.

// From cypress@8.6.0/packages/net-stubbing/lib/server/util.ts:
setDefaultHeader('access-control-expose-headers', constant('*'))

// From cypress@8.6.0/packages/driver/src/cypress/utils.ts:
return [fn, constant(type)]

// From Odoo v15.0 addons/pad/static/src/js/pad.js:
url.toJSON = constant(this.url);

// From ng-table@3.0.1/test/specs/settings.spec.ts:
const newSettings: = {
  filterOptions: _.mapValues(allSettings.filterOptions, constant(undefined)),
  dataOptions: _.mapValues(allSettings.dataOptions, constant(undefined)),
  groupOptions: _.mapValues(allSettings.groupOptions, constant(undefined))
};

// From Elastic Kibana v7.15.1
// src/plugins/vis_types/vislib/public/fixtures/mock_data/histogram/_slices.js.
{
  name: 0,
  size: 378611,
  aggConfig: {
    type: 'histogram',
    schema: 'segment',
    fieldFormatter: constant(String),
    params: {
      interval: 1000,
      extended_bounds: { /* … */ },
    },
  },
  /* … */
},

// From Yhat Rodeo v2.5.2 src/node/services/files.test.js:
fs.lstat.onCall(0).yields(null, {isDirectory: constant(true)});

Precedents include:

Function.identity

The Function.identity static method always returns its first argument.

Function.identity(value);

const { identity } = Function;

identity(5); // 5.
identity(); // undefined.

The following real-world examples originally used lodash.identity.

// From cypress@8.6.0/packages/driver/src/cypress/runner.ts:
// “Iterates over a suite's tests (including nested suites)
// and will return as soon as the callback is true”.
const findTestInSuite = (suite, fn = identity) => {
  for (const test of suite.tests) {
    if (fn(test)) {
      return test
    } /* … */
  }
}

// From gatsby@3.14.3/packages/gatsby-plugin-sharp/src/plugin-options.js:
// “Get all non falsey values”.
return _.pickBy(options, identity)

// From gatsby@3.14.3/packages/gatsby-plugin-gatsby-cloud/src/constants.js:
export const DEFAULT_OPTIONS = {
  // “Optional transform for manipulating headers for sorting, etc”.
  transformHeaders: identity,
  /* … */
}

// From ghost@4.19.0/core/frontend/helpers/img_url.js:
// “only make paths relative if we didn't get a request for an absolute url”.
const maybeEnsureRelativePath = !absoluteUrlRequested ? ensureRelativePath : _.identity;

// From Meteor v2.5.0 tools/cordova/builder.js:
const boilerplate = new Boilerplate(CORDOVA_ARCH, manifest, {
  urlMapper: identity,
  /* … */
});

Precedents include:

Function.noop

The Function.noop static method always returns undefined. Function.noop is equivalent to () => {}. It is also equivalent to constant().

This function is already available and frequently used from both jQuery and Lodash, generally to fill a required callback argument or to disable a callback property.

const { noop } = Function;
[ 0, 1 ].map(noop)
// [ undefined, undefined ].

The following real-world examples originally used jQuery’s $.noop or lodash.noop.

// From Wordpress v5.1.11:
{ /* … */
  defaultExpandedArguments: {
    duration: 'fast',
    completeCallback: noop }
  /* … */ }

// From three@0.133.1/test/benchmark/benchmark.js:
SuiteUI.prototype.run = function() {
  this.runButton.click = noop;
  this.runButton.innerText = "Running..."
  this.suite.run({ async: true });
}

// From typeahead.js@0.11.1/src/typeahead/dataset.js:
this.cancel = function cancel() {
  canceled = true;
  that.cancel = noop;
  that.async &&
    that.trigger('asyncCanceled', query);
};

// From typeahead.js@0.11.1/src/bloodhound/bloodhound.js:
// “if max size is less than 0, provide a noop cache”.
sync = sync || noop;
async = async || noop;
sync(this.remote ? local.slice() : local);

// From typeahead.js@0.11.1/src/bloodhound/lru_cache.js:
// “if max size is less than 0, provide a noop cache”.
if (this.maxSize <= 0) {
    this.set = this.get = $.noop;
}

// From verdaccio@5.1.6/packages/middleware/src/middleware.ts:
errorReportingMiddleware(req, res, noop);

// From Odoo v15.0 addons/bus/static/src/js/services/bus_service.js:
Promise.resolve(this._audio.play()).catch(noop);

// From ClickHouse v21.10.2.15-stable website/js/docsearch.js:
if (this.$hint.length === 0) {
  this.setHint = this.getHint = this.clearHint = this.clearHintIfInvalid = noop;
}

Precedents include:

Function.prototype.once

The Function.prototype.once method creates a new function that calls the original function at most once, no matter how much the new function is called.

fn.once();

const fn = console.log.once();
fn(5); // Prints 5.
fn(5); // Does not print anything.
fn(5); // Does not print anything.

const initialize = createApplication.once();
initialize();
initialize();
// createApplication is invoked only once.

The following real-world example originally used lodash.once.

// From Meteor v2.2.1:
// “Are we running Meteor from a git checkout?”
export const inCheckout = (function () {
  try { /* … */ } catch (e) { console.log(e); }
  return false;
}).once();

// From cypress@8.6.0:
cy.on('command:retry', _.after(2, (() => {
  button.remove() /* … */
}).once()))

// From Jitsi Meet v6482:
this._hangup = (() => {
 sendAnalytics(createToolbarEvent('hangup'));
 /* … */
}).once()

Precedents include:

Function.prototype.debounce

The Function.prototype.debounce method creates a new function that calls the original function at most once, no matter how much the new function is called.

fn.debounce(numOfMilliseconds);

Numerous graphical applications use debounce. In this example, logging happens on keyup events from inputEl, but only after the user has stopped typing for at least 250 ms:

inputEl.addEventListener('keyup',
  console.log.debounce(250));

This method may come with options that could be bikeshedded in Stage 1.

Precedents include:

Function.prototype.throttle

The Function.prototype.throttle method creates a new function that, when called, calls the original function—but only at most once within a given length of time.

fn.throttle(numOfMilliseconds);

Numerous graphical applications use throttle. In this example, logging happens on window scroll, but no more than once every 250 ms:

inputEl.addEventListener('keyup',
  console.log.throttle(250));

This method may come with options that could be bikeshedded in Stage 1.

Precedents include:

Function.prototype.aside

The Function.prototype.aside method creates a new unary function that applies some callback to its argument before returning the original argument.

fn.aside();

const { aside } = Function;

console.log.aside(5); // Prints 5 before returning 5.

arr.map(console.log.aside).map(f);
// Prints each item from `arr` before passing them to `f`.

const data = await Promise.resolve('intro.txt')
  .then(Deno.open)
  .then(Deno.readAll)
  .then(console.log.aside())
  .then(data => new TextDecoder('utf-8').decode(data));

The following real-world example originally used lodash.aside and lodash/fp’s pipe.

// From IBM/report-toolkit v0.6.1 packages/common/src/config.js:
export function filterEnabledRules(config) {
  return pipe(
    config,
    _.getOr({}, 'rules'),
    _.toPairs,
    _.reduce(
      (enabledRules, [ruleName, ruleConfig]) =>
        (_.isObject(ruleConfig) && _.get('enabled', ruleConfig)) ||
        (_.isBoolean(ruleConfig) && ruleConfig)
          ? [ruleName, ...enabledRules]
          : enabledRules,
      []
    ),
    (ruleIds => {
      debug('found %d enabled rule(s)', ruleIds.length);
    }).aside();
}

Precedents include:

  • lodash: _.tap
  • Ramda: import { tap } from 'ramda/src/tap';

Function.prototype.unThis

The Function.prototype.unThis method creates a new function that calls the original function, supplying its first argument as the original function’s this receiver, and supplying the rest of its arguments as the original function’s ordinary arguments.

This is useful for converting this-based functions into non-this-based functions.

fn.unThis();

const $slice = Array.prototype.slice.unThis();
$slice([ 0, 1, 2 ], 1); // [ 1, 2 ].

This is not a substitute for a bind-this syntax, which allows developers to change the receiver of functions without creating a wrapper function.

fn.unThis() is equivalent to
Function.prototype.call.bind(fn) and to
Function.prototype.bind.bind(Function.prototype.call)(fn).

Therefore, fn.unThis()(thisArg, ...restArgs) is equivalent to fn.call(thisArg, ...restArgs).

The following real-world example originally used call-bind or a manually created similar function.

// From chrome-devtools-frontend@1.0.934332
// node_modules/array-includes/test/implementation.js.
runTests(implementation.unThis(), t);

// From string.prototype.trimstart@1.0.4/index.js:
var bound = getPolyfill().unThis();

// From andreasgal/dom.js (84b7ab6) src/snapshot.js.
const /* … */
  join = A.join || Array.prototype.join.unThis(),
  map = A.map || Array.prototype.map.unThis(),
  push = A.push || Array.prototype.push.unThis(),
  /* … */;

Precedents include: