Name | Status | Features | Purpose |
---|---|---|---|
Core Proposal | Stage 0 | Infix pipelines … |> … Lexical topic # |
Unary function/expression application |
Additional Feature BC | None | Bare constructor calls … |> new … |
Tacit application of constructors |
Additional Feature BA | None | Bare awaited calls … |> await … |
Tacit application of async functions |
Additional Feature BP | None | Block pipeline steps … |> {…} |
Application of statement blocks |
Additional Feature PF | None | Pipeline functions +> |
Partial function/expression application Function/expression composition Method extraction |
Additional Feature TS | None | Pipeline try statements |
Tacit application to caught errors |
Additional Feature NP | None | N-ary pipelines (…, …) |> … Lexical topics ## , ### , and ... |
N-ary function/expression application |
ECMAScript No-Stage Proposal. Living Document. J. S. Choi, 2018-12.
This document is not yet intended to be officially proposed to TC39 yet; it merely shows a possible extension of the Core Proposal in the event that the Core Proposal is accepted.
This additional feature – Pipeline Functions – would dramatically increase the usefulness of
pipelines. It introduces just one additional operator that solves:
tacit unary functional composition,
tacit unary functional partial application,
and many kinds of tacit method extraction,
…all at the same time.
And with Additional Feature NP, this additional feature would also solve
tacit N-ary functional partial application
and N-ary functional composition.
The new operator is a prefix operator +> …
, which creates pipeline
functions, which are just arrow functions. +> …
interprets its inner
expression as a pipeline but wraps it in an arrow function that applies its
pipeline steps to its arguments.
A pipe function takes no a parameter list; no such list is needed. Just like with regular pipelines, a pipeline function may be in bare style or topic style.
If the pipeline function starts with bare style (like +> f |> # + 1
), then
the function is variadic and applies all its arguments to the function
reference to which the bare-style pipeline step evaluates (that is,
(...$) => f(...$) + 1
), where $
is a hygienically unique
variable. (This is forward compatible with Additional
Feature NP.)
If the pipeline function starts with topic style (like +> # + 1 |> # + 1
),
then the function is unary and applies its first argument (that is,
$ => # + 1
, where $
is a hygienically unique variable).
As an aside, topic style can also handle multiple parameters with Additional
Feature NP, such that +> # + ##
would be a binary arrow function equivalent
to ($, $$) => $ + $$
, and +> [...].length
would be a variadic arrow function
equivalent to (...$rest) => [...$rest].length
– where $
, $$
, and $rest
are all hygienically unique variables.
In general, Additional Feature NP would explain “+>
Pipeline” as equivalent
to “(...$rest) => ...$rest |>
Pipeline”.
+>
was chosen because of its similarity both to |>
and to =>
. The precise
appearance of the pipeline-function operator does not have to be +>
. It could
also be ~>
, ->
, =|
, =|>
or something else to be decided after future
bikeshedding discussion.
Additional Feature PF is formally specified in in the draft specification.
With smart pipelines | Status quo |
---|---|
array.map($ => $ |> #); array.map($ => $); These functions are the same. They both pipe a unary parameter into a topic-style pipeline whose only step evaluates simply to the topic, unmodified. |
array.map($ => $); In other words, they are both identity functions. |
array.map($ => $ |> # + 2); array.map(+> # + 2); These functions are also the same with each other. They both pipe a unary parameter into a topic-style pipeline whose only step is the topic plus two. |
array.map($ => $ + 2); |
array.map(+> f |> # + 2); This pipeline function starts in bare mode. This means it is a variadic function.
(As an aside, with Additional Feature NP, this would also be expressible as:
|
array.map((...$) => f(...$)); |
Pipelines may be chained within a pipeline function. array.map(+> f |> g |> h |> # * 2); The prefix pipeline-function operator |
array.map((...$) => h(g(f(...$))) * 2); |
+> x + 2;
// 🚫 Syntax Error:
// Pipeline step `+> x + 2`
// binds topic but contains
// no topic reference. This is an early error, as usual. The topic is not used anywhere
in the pipeline function’s only step – just like with | |
=> # + 2;
// 🚫 Syntax Error:
// Unexpected token `=>`. If the pipeline-function operator | |
() => # + 2;
// 🚫 Syntax Error:
// Lexical context `() => # + 2`
// contains a topic reference
// but has no topic binding. But even if that typo also includes a parameter head for the arrow function
| |
Terse composition of unary functions is a goal of smart pipelines. It is equivalent to piping a value through several function calls, within a unary function, starting with the outer function’s tacit unary parameter. array.map(+> f |> g |> h(2, #) |> # + 2); There are several existing proposals for unary functional composition, which Additional Feature PF would all subsume. And with Additional Feature NP, even n-ary functional composition would be supported, which no current proposal yet addresses. |
array.map((...$) => h(2, g(f(...$))) + 2); |
const doubleThenSquareThenHalfAsync =
async $ => $
|> double |> await squareAsync |> half; const doubleThenSquareThenHalfAsync =
async +> double |> await squareAsync |> half; When compared to the proposal for syntactic functional composition by
TheNavigateur, this syntax does not need
to give implicit special treatment to async functions. There is instead an async
version of the pipe-function operator, within which |
const doubleThenSquareThenHalfAsync =
async $ =>
half(await squareAsync(double($))); const doubleThenSquareThenHalfAsync =
double +> squareAsync +> half; From the proposal for syntactic functional composition by TheNavigateur. |
const toSlug =
$ => $
|> #.split(' ')
|> #.map($ => $.toLowerCase())
|> #.join('-')
|> encodeURIComponent; const toSlug =
+> #.split(' ')
|> #.map(+> #.toLowerCase())
|> #.join('-')
|> encodeURIComponent; When compared to the proposal for syntactic functional composition by Isiah Meadows, this syntax does not need to surround each non-function expression with an arrow function. The smart step syntax has more powerful expressive versatility, improving the readability of the code. |
const toSlug = $ =>
encodeURIComponent(
$.split(' ')
.map(str =>
str.toLowerCase())
.join('-')); const toSlug =
_ => _.split(" ")
:> _ => _.map(str =>
str.toLowerCase())
:> _ => _.join("-")
:> encodeURIComponent; From the proposal for syntactic functional composition by Isiah Meadows. |
const getTemperatureFromServerInLocalUnits =
async +>
|> await getTemperatureKelvinFromServerAsync
|> convertTemperatureKelvinToLocalUnits; Lifting of non-sync-function expressions into function expressions is unnecessary for composition with Additional Feature PF. Additional Feature BA is also useful here. |
Promise.prototype[Symbol.lift] =
f => x => x.then(f)
const getTemperatureFromServerInLocalUnits =
getTemperatureKelvinFromServerAsync
:> convertTemperatureKelvinToLocalUnits; From the proposal for syntactic functional composition by Isiah Meadows. |
// Functional Building Blocks
const car = +>
|> startMotor
|> useFuel
|> turnKey;
const electricCar = +>
|> startMotor
|> usePower
|> turnKey;
// Control Flow Management
const getData = +>
|> truncate
|> sort
|> filter
|> request;
// Argument Assignment
const sortBy = 'date';
const getData = +>
|> truncate
|> sort
|> #::filter(sortBy)
|> request; This example also uses function binding. |
// Functional Building Blocks
const car = startMotor.compose(
useFuel, turnKey);
const electricCar = startMotor.compose(
usePower, turnKey);
// Control Flow Management
const getData = truncate.compose(
sort, filter, request);
// Argument Assignment
const sortBy = 'date';
const getData = truncate.compose(
sort,
$ => filter.bind($, sortBy),
request); From the proposal for syntactic functional composition by Simon Staton. |
const pluck = +> map |> prop; |
const pluck = compose(map)(prop); From a comment about syntactic functional composition by Tom Harding. |
Terse partial application into a unary function is equivalent to piping a tacit parameter into a function-call expression, within which the one parameter is resolvable. array.map($ => $ |> f(2, #));
array.map(+> f(2, #)); |
Pipeline functions look similar to the proposal for partial function application by Ron Buckton, except that partial-application expressions are simply pipeline steps that are prefixed by the pipeline-function operator. array.map(f(2, ?));
array.map($ => f(2, $)); |
const addOne = +> add(1, #);
addOne(2); // 3 |
const addOne = add(1, ?);
addOne(2); // 3 |
const addTen = +> add(#, 10);
addTen(2); // 12 |
const addTen = add(?, 10);
addTen(2); // 12 |
let newScore = player.score
|> add(7, #)
|> clamp(0, 100, #); |
let newScore = player.score
|> add(7, ?)
|> clamp(0, 100, ?); |
const toSlug = +>
|> encodeURIComponent
|> _.split(#, " ")
|> _.map(#, _.toLower)
|> _.join(#, "-"); Additional Feature PF simultaneously handles function composition and partial application into unary functions. |
const toSlug =
encodeURIComponent
:> _.split(?, " ")
:> _.map(?, _.toLower)
:> _.join(?, "-"); From the proposal for syntactic functional composition by Isiah Meadows. |
Many kinds of method extraction can be addressed by pipeline functions
alone, as a natural result of their pipe-operator-like semantics. Promise.resolve(123)
.then(+> console.log); |
Promise.resolve(123)
.then(console.log.bind(console));
Promise.resolve(123)
.then(::console.log); |
$('.some-link').on('click', +> view.reset); |
$('.some-link').on('click', ::view.reset); |
Note that this is not the same as const consoleLog =
console.log.bind(console.log);
const arrayFrom =
Array.from.bind(Array.from);
const arrayMap =
Function.bind.call(Function.call,
Array.prototype.map);
…
input
|> process
|> consoleLog;
input
|> arrayFrom
|> arrayMap(#, $ => $ + 1)
|> consoleLog; This robust method extraction is a use case that this proposal leaves to
another operator, such as prefix |
const consoleLog =
console.log.bind(console.log);
const arrayFrom =
Array.from.bind(Array.from);
const arrayMap =
Function.bind.call(Function.call, Array.prototype.map);
…
consoleLog(
process(input));
consoleLog(
arrayMap(arrayFrom(input), $ => $ + 1)); |
…
input
|> process
|> &console.log;
input
|> &Array.from
|> #::&Array.prototype.map($ => $ + 1)
|> &console.log; Pipeline functions would not preclude adding another operator that addresses
robust method extraction with inline caching, such as the hypothetical prefix |
…
consoleLog(
process(input));
consoleLog(
&Array.from(input)
::&Array.prototype.map($ => $ + 1)); |
const { hasOwnProperty } =
Object.prototype;
const x = { key: 5 };
x::hasOwnProperty;
x::hasOwnProperty('key'); For terse method calling/binding, the infix |
const { hasOwnProperty } =
Object.prototype;
const x = { key: 5 };
x::hasOwnProperty;
x::hasOwnProperty('key'); |
Ramda is a utility library focused on functional programming with pure
functions and immutable objects. Its functions are automatically
curried. Smart pipelines with Additional Feature PF
would address many of Rambda’s use cases. The examples below were taken from the Ramda wiki
cookbook. They use smart pipelines with vanilla JavaScript APIs when possible
(such as Array.prototype.map
instead of R.map
), but they also use Ramda
functions wherever no terse JavaScript equivalent yet exists (such as with
R.zipWith
and R.adjust
).
Even more of Ramda’s use cases are covered when Additional Feature NP syntax is supported.
With smart pipelines | Status quo |
---|---|
const pickIndexes = +> R.values |> R.pickAll;
['a', 'b', 'c'] |> pickIndexes([0, 2], #);
// ['a', 'c'] |
const pickIndexes = R.compose(
R.values, R.pickAll);
pickIndexes([0, 2], ['a', 'b', 'c']);
// ['a', 'c'] |
const list = +> [...];
list(1, 2, 3);
// [1, 2, 3] |
const list = R.unapply(R.identity);
list(1, 2, 3);
// [1, 2, 3] |
const getNewTitles = async +>
|> await fetch
|> parseJSON
|> #.flatten()
|> #.map(+> #.items)
|> #.map(+> #.filter(+> #))
|> #.map(+> #.title);
try {
'/products.json'
|> getNewTitles
|> console.log;
}
catch
|> console.error;
const fetchDependent = async +>
|> await fetch
|> JSON.parse
|> #.flatten()
|> #.map(+> #.url)
|> #.map(fetch)
|> #.flatten();
try {
'urls.json'
|> fetchDependent
|> console.log;
}
catch
|> console.error; This example also uses Additional Feature TS for terse |
const getNewTitles = R.compose(
R.map(R.pluck('title')),
R.map(R.filter(R.prop('new'))),
R.pluck('items'),
R.chain(JSON.parse),
fetch
);
getNewTitles('/products.json')
.fork(console.error, console.log);
const fetchDependent = R.compose(
R.chain(fetch),
R.pluck('url'),
R.chain(parseJSON),
fetch
);
fetchDependent('urls.json')
.fork(console.error, console.log); |
number
|> R.repeat(Math.random, #)
|> #.map(+> #()); |
R.map(R.call,
R.repeat(Math.random, number)); |
const renameBy = (fn, obj) =>
[...obj]
|> #.map(R.adjust(fn, 0)),
|> ({...#});
{ A: 1, B: 2, C: 3 };
|> renameBy(+> `a${#}`));
// { aA: 1, aB: 2, aC: 3 } |
const renameBy = R.curry((fn, obj) =>
R.pipe(
R.toPairs,
R.map(R.adjust(fn, 0)),
R.fromPairs
)(obj)
);
renameBy(R.concat('a'), { A: 1, B: 2, C: 3 });
// { aA: 1, aB: 2, aC: 3 } |
The WHATWG Streams Standard provides an efficient, standardized stream API, inspired by Node.js’s Streams API, but also applicable to the DOM. The specification contains numerous usage examples that would become more readable with smart pipelines. The Core Proposal alone would untangle much of this code, and the additional features would further improve its terseness.
With smart pipelines | Status quo |
---|---|
class LipFuzzTransformer {
constructor(substitutions) {
this.substitutions = substitutions;
this.partialChunk = "";
this.lastIndex = undefined;
}
transform (chunk, controller) {
this.partialChunk = ""
this.lastIndex = 0
const partialAtEndRegexp =
/\{(\{([a-zA-Z0-9_-]+(\})?)?)?$/g
partialAtEndRegexp.lastIndex =
this.lastIndex
this.lastIndex = undefined
chunk
|> this.partialChunk + #
|> #.replace(
/\{\{([a-zA-Z0-9_-]+)\}\}/g,
+> this.replaceTag)
|> partialAtEndRegexp.exec
|> {
if (#) {
this.partialChunk =
|> #.index
|> chunk.substring;
#
|> #.index
|> chunk.substring(0, #);
}
else
chunk;
}
|> controller.enqueue;
}
flush (controller) {
this.partialChunk |> {
if (#.length > 0) {
|> controller.enqueue;
}
};
}
replaceTag (match, p1, offset) {
return this.substitutions
|> #[p1]
|> # === undefined ? '' : #
|> {
this.lastIndex =
|> #.length
|> offset + #;
#;
};
}
} |
class LipFuzzTransformer {
constructor (substitutions) {
this.substitutions = substitutions;
this.partialChunk = "";
this.lastIndex = undefined;
}
transform (chunk, controller) {
chunk = this.partialChunk + chunk;
this.partialChunk = "";
this.lastIndex = 0;
chunk = chunk.replace(
/\{\{([a-zA-Z0-9_-]+)\}\}/g,
this.replaceTag.bind(this));
const partialAtEndRegexp =
/\{(\{([a-zA-Z0-9_-]+(\})?)?)?$/g;
partialAtEndRegexp.lastIndex =
this.lastIndex;
this.lastIndex = undefined;
const match =
partialAtEndRegexp.exec(chunk);
if (match) {
this.partialChunk =
chunk.substring(match.index);
chunk =
chunk.substring(0, match.index);
}
controller.enqueue(chunk);
}
flush (controller) {
if (this.partialChunk.length > 0) {
controller.enqueue(
this.partialChunk);
}
}
replaceTag (match, p1, offset) {
let replacement = this.substitutions[p1];
if (replacement === undefined) {
replacement = "";
}
this.lastIndex =
offset + replacement.length;
return replacement;
}
} |