tc39 / proposal-function-pipe-flow

A proposal to standardize helper functions for serial function application and function composition.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Function.pipe and flow for JavaScript

ECMAScript Stage-0 Proposal (withdrawn). J. S. Choi, 2021.

⚠️ This proposal is withdrawn. In the plenary on July 21, proposal-function-pipe-flow was formally presented to the Committee, and it was rejected for Stage 1. The Committee generally found its use cases either easily solved by userland functions, such as:

function pipe (input, ...fnArray) {
  return fnArray.reduce((value, fn) => fn(value), input);
}

…or also solved by the pipe operator. Its champion subsequently withdrew it from consideration. (Eventually, after the pipe operator gains users, pain points with the pipe operator may be enough motivation to revive this proposal, but that would not occur for a long time.)

Original proposal

Serial function application and function composition are useful and common in JavaScript. Developers often divide code into many smaller unary callbacks, which are then sequentially called with some initial input—or which are composed into larger callbacks that will sequentially call those functions later.

To do this, they often combine these callbacks with helper functions: pipe (serial function application) and flow and/or compose (function composition). It would be useful to standardize these metafunctions.

The problem space here is the application and composition of serial callbacks. Much of the JavaScript community already has been serially applying or composing callbacks.

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 }))
);

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) => { /* … */ }),
)

If this proposal is approved for Stage 1, then we would explore various directions for serially applying and composing unary callbacks. Additionally, we would assemble as many real-world use cases as possible and shape our design to fulfill them.

(This problem space is already being explored to some extent by the pipe operator’s proposal. However, unlike in this proposal, the pipe operator can “apply” any kind of expression operation, not only unary function calls. The tradeoff is that the pipe operator involves new syntax; additionally, the pipe function is more concise for unary function calls and can handle dynamic arrays of callbacks. In Stage 1, we would continue to examine cross-cutting concerns and overlap with the pipe operator and other “dataflow” proposals.)

Solutions

We could add various combinations of the following static functions:

  • Function.pipe and Function.pipeAsync (for serial application).
  • Function.flow and Function.flowAsync (for LTR serial composition).
  • Function.compose and Function.composeAsync (for RTL serial composition).

(LTR = left to right. RTL = right to left.)

There are eight possible combinations of `pipe`, `flow`, and `compose`.
  • Choice #0: Status quo

  • Choice #1: LTR flow

    Function.flow(f, g, h);
    Function.flowAsync(f, g, h);
  • Choice #2: RTL compose

    Function.compose(h, g, f);
    Function.composeAsync(h, g, f);
  • Choice #3: LTR flow & RTL compose

    Function.flow(f, g, h);
    Function.flowAsync(f, g, h);
    Function.compose(h, g, f);
    Function.composeAsync(h, g, f);
  • Choice #4: Pipe

    Function.pipe(x, f, g, h);
    Function.pipeAsync(x, f, g, h);
  • Choice #5: Pipe & LTR flow

    Function.pipe(x, f, g, h);
    Function.pipeAsync(x, f, g, h);
    Function.flow(f, g, h);
    Function.flowAsync(f, g, h);
  • Choice #6: Pipe & RTL compose

    Function.pipe(x, f, g, h);
    Function.pipeAsync(x, f, g, h);
    Function.compose(h, g, f);
    Function.composeAsync(h, g, f);
  • Choice #7: Pipe, LTR flow & RTL compose

    Function.pipe(x, f, g, h);
    Function.pipeAsync(x, f, g, h);
    Function.flow(f, g, h);
    Function.flowAsync(f, g, h);
    Function.compose(h, g, f);
    Function.composeAsync(h, g, f);

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.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';

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).

(LTR) Function.flow

The Function.flow static method creates a new function by combining several callbacks in left-to-right order.

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.

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 a unary identity function.

Precedents include:

(LTR) Function.flowAsync

The Function.flowAsync static method creates a new function by combining several potentially async callbacks in left-to-right order; the created function will always return a promise.

Function.flowAsync(...fns);

const { flowAsync } = Function;

// async (...args) => await f2(await f1(await f0(...args))).
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.

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.

(RTL) Function.compose

The Function.compose static method creates a new function by combining several callbacks in right-to-left order.

Function.compose(...fns);

const { compose } = Function;

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

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

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

Any function created by Function.compose applies its own arguments to its rightmost callback. Then that result is applied to its next callback.

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

If Function.compose receives no arguments, then, by default, it will return a unary identity function.

(RTL) Function.composeAsync

The Function.composeAsync static method creates a new function by combining several potentially async callbacks in right-to-left order; the created function will always return a promise.

Function.composeAsync(...fns);

const { composeAsync } = Function;

// async (...args) => await f2(await f1(await f0(...args))).
composeAsync(f2, f1, f0);

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

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

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

Any function created by Function.composeAsync applies its own arguments to its rightmost 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 rightmost callback may have any arity, but any subsequent callbacks are expected to be unary.

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

About

A proposal to standardize helper functions for serial function application and function composition.

License:BSD 3-Clause "New" or "Revised" License


Languages

Language:HTML 100.0%