This proposal introduces a new syntax using the ?
and ...
tokens which allows you to partially apply an argument list to
a call expression by acting as placeholders for an argument or arguments.
Stage: 0
Champion: Ron Buckton (@rbuckton)
For more information see the TC39 proposal process.
- Ron Buckton (@rbuckton)
Partial function application allows you to fix a number of arguments to a function call, returning
a new function. Partial application is supported after a fashion in ECMAScript today through the use of either
Function#bind
or arrow functions:
function add(x, y) { return x + y; }
// Function#bind
const addOne = add.bind(null, 1);
addOne(2); // 3
// arrow functions
const addTen = x => add(x, 10);
addTen(2); // 12
// arrow functions and pipeline
const newScore = player.score
|> _ => add(7, _)
|> _ => clamp(0, 100, _); // deeply nested stack, the pipe to `clamp` is *inside* the previous arrow function.
However, there are several of limitations with these approaches:
bind
can only fix the leading arguments of a function.bind
requires you explicitly specify thethis
receiver.- Arrow functions can be cumbersome when paired with the pipeline proposal:
- Need to write
|> _ =>
for each step in the pipeline. - Unclear as to which stack frame we are in for the call to
clamp
. This can affect available stack space and is harder to debug.
- Need to write
To resolve these concerns, we propose leveraging the ?
token to act as an "argument placeholder"
for a non-fixed argument, and the ...
token to act as a "remaining arguments placeholder":
const addOne = add(1, ?); // apply from the left
addOne(2); // 3
const addTen = add(?, 10); // apply from the right
addTen(2); // 12
// with pipeline
let newScore = player.score
|> add(7, ?)
|> clamp(0, 100, ?); // shallow stack, the pipe to `clamp` is the same frame as the pipe to `add`.
const maxGreaterThanZero = Math.max(0, ...);
maxGreaterThanZero(1, 2); // 2
maxGreaterThanZero(-1, -2); // 0
f(x, ?) // partial application from left
f(x, ...) // partial application from left with rest
f(?, x) // partial application from right
f(..., x) // partial application from right with rest
f(?, x, ?) // partial application for any arg
f(..., x, ...) // partial application for any arg with rest
The ?
and ...
placeholder tokens can only be used in an argument list of a call expression. When present,
the result of the call is a new function with a parameter for each ?
token in the argument list. Any excess
parameters are spread into the call at the position of the ...
token. Any non-placeholder expression in the
argument list becomes fixed in its position. This is illustrated by the following syntactic conversion:
const g = f(?, 1, ...)
is roughly identical in its behavior to:
const g = (x, ...y) => f(x, 1, ...y);
However, this is a somewhat trivial example. Partial application in this fashion has the following semantic rules:
- Given
f(?)
, the expressionf
is not evaluated immediately. Side effects that replacef
can be observed with successive calls to the resulting function:let f = (x, y) => x + y; const g = f(?, 3); g(1); // 4 // replace the value of `f` f = (x, y) => x * y; g(1); // 3
- Given
o.f(?)
, the references too
ando.f
are not evaluated immediately. Side effects that replaceo
oro.f
can be observed with successive calls to the resulting function:Note that this also means that more involved references are captured in their entirety and should be stored in a local variable if they may have unintended side-effects should the partially applied function result be called more than once:let o = { f(x, y) { return x + y + this.z; }, z: 0 }; const g = o.f(?, 3); g(1); // 4 // replace the value of `o` o = { f(x, y) { return x + y + this.z; }, z: 2 }; g(1); // 6 // replace the value of `o.f` o.f = (x, y) => x * y; g(1); // 5
const a = [{ c: x => x + 1 }, { c: x => x + 2 }]; let b = 0; const g = a[b++].c(?); b; // 0 g(1); // 2 g(1); // 3 b; // 2 // vs const a = [{ c: x => x + 1 }, { c: x => x + 2 }]; let b = 0; const o = a[b++]; const g = o.c(?); b; // 1 g(1); // 2 g(1); // 2 b; // 1
- Given
f(?)
, while the non-placeholder arguments tof
are fixed in their positions, they are not evaluated immediately. Side effects that mutate references in these arguments can be observed with successive calls to the resulting function:let a = 3; const f = (x, y) => x + y; const g = f(?, a); g(1); // 4 // replace the value of `a` a = 10; g(1); // 11
- Given
g = f(?)
, excess arguments supplied to the partially applied function resultg
are ignored:const f = (x, ...y) => [x, ...y]; const g = f(?, 1); g(2, 3, 4); // [2, 1]
- Given
g = f(?, ?)
the partially applied function resultg
will have a parameter for each placeholder token that is supplied in that token's position in the argument list:const f = (x, y, z) => [x, y, z]; const g = f(?, 4, ?); g(1, 2); // [1, 4, 2]
- Given
g = f(...)
, excess arguments supplied to the partially applied function resultg
are spread into the original function at the indicated position:const f = (x, ...y) => [x, ...y]; const g = f(?, 1, ...); g(2, 3, 4); // [2, 1, 3, 4];
- Given
g = f(..., ...)
, the excess arguments supplied to the partially applied function resultg
are collected once but are spread into the call once for each position:const f = (...x) => x; const g = f(..., 9, ...); g(1, 2, 3); // [1, 2, 3, 9, 1, 2, 3]
- Given
f(this, ?)
, thethis
in the argument list is the lexicalthis
:const fader = { color: "#00ffff", async fade() { const fadeOut = desaturate(this.color, ?); // capture lexical `this` here. for (let i = 100; i > 0; i -= 10) { fadeOut(i); await delay(10); } } }
- Given
g = f(?)
, thethis
receiver of the functionf
is fixed asundefined
in the partially applied function resultg
:However, you may uncurryfunction f(x) { return `this: ${this}, x: ${x}`; } const o = { g: f(?) }; o.g(2); // 'this: undefined, x: 2'
this
usingf.call
:function f(x) { return `this: ${this}, x: ${x}.`; } const g = f.call(?, 2); g(1, 2); // `this: 1, x: 2`
- Given
g = o.f(?)
, thethis
receiver of the functiono.f
is fixed aso
in the partially applied function resultg
:However, you may uncurryconst o = { f(x) { return `this.y: ${this.y}, x: ${x}`; }, y: 1 }; const g = o.f(?); g(2); // 'this.y: 1, x: 2'
this
usingo.f.call
:const o = { f(x) { return `this.y: ${this.y}, x: ${x}`; }, y: 1 }; const g = o.f.call(?, 3); g({ y: 4 }); // 'this.y: 4, x: 3'
- Given
g = new f(?)
, the partially applied function resultg
is a function that when called will construct a new instance off
:function f(x, y) { this.z = `${x}, ${y}` } const g = new f("a", ?); const obj = g(1); // creates an f instance obj.z; // 'a, 1'
- Given
g = f(?)
, thelength
of the partially applied function resultg
is equal to the number of?
placeholder tokens in the argument list:const f = (x, y) => x + y; const g = f(?, 2); f.length; // 2 g.length; // 1
This proposal is designed to dove-tail into the pipeline operator (|>
) proposal as a way to interop
with libraries like Lodash (which accepts lists from the front of the argument list), and Ramda (which
accepts lists from the end of the argument list):
// Underscore/lodash style:
const result = books
|> filter(?, x => x.category === "programming");
// Ramda style:
const result = books
|> filter(x => x.category === "programming", ?);
It also allows you to pipeline into functions that expect lists to be the this
argument:
// bind style:
const result = books
|> filter.call(?, x => x.category === "programming");
An efficient implementation can statically determine that a pipe into a partially applied function could be reduced into fewer steps:
const res = a |> f(?, 1) |> g(?, 2);
is approximately identical to:
const res = g(f(a, 1), 2);
though a more accurate conversion would be:
let _temp;
const (_temp = a, _temp = f(_temp, 1), g(_temp, 2));
While this proposal leverages the existing ?
token used in conditional expressions, it does not
introduce parsing ambiguity as the ?
placeholder token can only be used in an argument list and
cannot have an expression immediately precedeing it (e.g. f(a?
is definitely a conditional
while f(?
is definitely a placeholder).
ArgumentList[Yield, Await]:
`?`
AssignmentExpression[+In, ?Yield, ?Await]
`...` AssignmentExpression[+In, ?Yield, ?Await]?
ArgumentList[?Yield, ?Await] `,` `?`
ArgumentList[?Yield, ?Await] `,` AssignmentExpression[+In, ?Yield, ?Await]
ArgumentList[?Yield, ?Await] `,` `...` AssignmentExpression[+In, ?Yield, ?Await]?
The following is a high-level list of tasks to progress through each stage of the TC39 proposal process:
- Identified a "champion" who will advance the addition.
- Prose outlining the problem or need and the general shape of a solution.
- Illustrative examples of usage.
-
High-level API(proposal does not introduce an API).
- Initial specification text.
- Optional. Transpiler support.
- Complete specification text.
- Designated reviewers have signed off on the current spec text.
- The ECMAScript editor has signed off on the current spec text.
- Test262 acceptance tests have been written for mainline usage scenarios and merged.
- Two compatible implementations which pass the acceptance tests: [1], [2].
- A pull request has been sent to tc39/ecma262 with the integrated spec text.
- The ECMAScript editor has signed off on the pull request.