Giter Club home page Giter Club logo

proposal-smart-pipelines's Introduction

This proposal has been archived in favor of a simpler Hack-pipes proposal, which is a subset of this proposal.

Smart pipelines

ECMAScript Stage-0 Proposal. Living Document. J. S. Choi, 2018-12.

This proposal introduces a new binary pipe operator |> to JavaScript. It's similar to the pipe operators of other languages: Clojure, Elixir and Erlang, Elm, F#, Hack, Julia, LiveScript, OCaml, Perl 6, R with magrittr, and Unix shells and PowerShell.

value
|> await #
|> doubleSay(#, ', ')
|> capitalize // This is a function call.
|> # + '!'
|> new User.Message(#)
|> await #
|> console.log; // This is a method call.

// (The # token isn't final; it might instead be @ or ? or %.)

The proposal is currently at Stage 0 of the TC39 process and is planned to be presented, along with a competing proposal, to TC39 by Daniel “littledan” Ehrenberg of Igalia. The Core Proposal is a variant of the first pipe-operator proposal also championed by Ehrenberg; this variant is listed as Proposal 4: Smart Mix in the pipe-proposal wiki. The variant resulted from previous discussions in the previous pipe-operator proposal, discussions which culminated in an invitation by Ehrenberg to try writing a specification draft.

An update to the existing pipeline Babel plugin is also being developed jointly between the author of this proposal and James DiGioia, the author of the competing proposal. The update will support both this proposal and the other proposal, configurable with a flag.

You can take part in discussions on the original proposal's GitHub issue tracker. When you file an issue, please note in it that you are talking specifically about “Proposal 4: Smart Mix”.

This specification currently uses # as a “topic reference”, but that choice is not set in stone. @, ?, %, or many other symbols could also be used. Bikeshedding discussions over what characters to use for the topic token have been occurring on GitHub at tc39/proposal-pipeline-operator issue #91.

This proposal makes many other trade-offs that are also not set in stone. The proposal can and will change in response to feedback once the Babel plugin is implemented. The Babel plugin is planned to be configurable, allowing hands-on experimentation with these trade-offs.

With smart pipelines Status quo

The infix “smart” pipe operator |> proposed here would provide a backwards- and forwards-compatible style of chaining nested expressions into a readable, left-to-right manner.

Using a zero-cost abstraction, nested data transformations become untangled into short steps.

Nested, deeply composed expressions occur often in real-world JavaScript. They occur whenever any single value must be processed by a series of data transformations, whether they be operations, functions, or constructors. Unfortunately, these deeply nested expressions often result in messy spaghetti code, due to their mixing of prefix, infix, and postfix expressions together. Writing such code requires many nested levels of indentation and parentheses. Reading such code requires checking both the left and right of each subexpression to understand its data flow.

promise
|> await #
|> # || throw new TypeError(
  `Invalid value from ${promise}`)
|> doubleSay(#, ', ')
|> capitalize
|> # + '!'
|> new User.Message(#)
|> await stream.write(#)
|> console.log;

With smart pipelines, code becomes terser and, literally, more straightforward. Prefix, infix, and postfix expressions would be less tangled together in threads of spaghetti. Instead, data values would be piped from left to right through a single linear thread of postfix expressions, with a single level of indentation and four fewer pairs of parentheses – essentially forming a reverse Polish notation.

The resulting code’s terseness and linearity may be both easier for the JavaScript developer to read and to edit. This uniform postfix notation preserves locality between related code; the reader may follow the flow of data more easily through this single linear thread of postfix operations. And the developer may more easily add or remove operations at the beginning, end, or middle of the thread, without changing the indentation of unrelated lines.

console.log(
  await stream.write(
    new User.Message(
      capitalize(
        doubledSay(
          await promise
            || throw new TypeError(
              `Invalid value from ${promise}`)
        ), ', '
      ) + '!'
    )
  )
);

Compared with the pipeline version, this non-pipeline version requires additional indentation and grouping on each step. This requires four more levels of indentation and four more pairs of parentheses.

In addition, much related code is here separated by unrelated code. Rather than a uniform postfix chain, operations appear either before the previous step’s expression (await stream.write(…),new User.Message(…), capitalize(…), doubledSay(…), await …) but also after (… || throw new TypeError(), … + '!'). An additional argument to function calls (such as , in doubledSay(…, ', ')) is also separated from its function calls, forming another easy-to-miss “postfix” argument.

Each step of a pipeline creates its own lexical scope, within which the # token (the topic reference) is immutably to the result of the previous step (the topic). In this way, it acts as a placeholder for each step’s input.

input // step 0
|> # + 1 // step 1
|> f(x, #, y) // step 2
|> await g(#, z) // step 3
|> console.log(`${#}!`); // step 4

The use of # as the topic reference is not set in stone. @, ?, %, or many other symbols could also be used. Bikeshedding discussions over what characters to use for the topic token have been occurring on GitHub at tc39/proposal-pipeline-operator issue #91.

console.log(`${ // step 4
  await g( // step 3
    f(x, // step 2
      input + 1, // step 1
      y), // step 2
    z) // step 3
}!`); // step 4
input |> (# = 50);
// 🚫 Reference Error:
// Cannot assign to topic reference.

The topic binding is immutable, established only once per pipeline step. It is an error to attempt to assign a value to it using =, whether inside or outside a pipeline step.

This chained pipeline:

input
|> # - 3
|> -#
|> # * 2
|> Math.max(#, 0)
|> console.log;

…is equivalent to the tangled nested expression:

console.log(
  Math.max(
    -(input - 3) * 2,
    0
  )
);

The syntax is statically term rewritable into already valid code in this way, with theoretically zero runtime cost.

Similar use cases appear numerous times in real-world JavaScript code, whenever any input is transformed by expressions of any type: function calls, property calls, method calls, object constructions, arithmetic operations, logical operations, bitwise operations, typeof, instanceof, await, yield and yield *, and throw expressions.

function doubleSay (str, separator) {
  return `${str}${separator}${string}`;
}

function capitalize (str) {
  return str[0].toUpperCase()
    + str.substring(1);
}

promise
|> await #
|> # || throw new TypeError()
|> doubleSay(#, ', ')
|> capitalize
|> # + '!'
|> new User.Message(#)
|> await stream.write(#)
|> console.log;

This pipeline is also relatively linear, with only one level of indentation, and with each transformation step on its own line.

… |> capitalize uses a special shortcut called the bare style, explained further below. It is a bare unary function call that is, in this case, equivalent to … |> capitalize(#).

function doubleSay (str, separator) {
  return `${str}${separator}${str}`;
}

function capitalize (str) {
  return str[0].toUpperCase()
    + str.substring(1);
}

console.log(
  await stream.write(
    new User.Message(
      capitalize(
        doubledSay(
          await promise
            || throw new TypeError(
              `Invalid value from ${promise}`)
        ), ', '
      ) + '!'
    )
  )
);

This deeply nested expression has four levels of indentation instead of two. Reading its data flow requires checking both the beginning of each expression (new User.Message, capitalizedString, doubledSay, await promise and end of each expression (|| throw new TypeError(), , ', ', + '!')).

x =  |> f(#, #);
x =  |> [#, # * 2, # * 3];

The topic reference may be used multiple times in a pipeline step. Each use refers to the same value (wherever the topic reference is not overridden by another, inner pipeline’s topic scope). Because it is bound to the result of the topic, the topic is still only ever evaluated once.

{
  const $ = ;
  x = f($, $);
}
{
  const $ = ;
  x = [$, $ * 2, $ * 3];
}

This is equivalent to assigning the topic value to a unique variable, then using that variable multiple times in an expression.

promise
|> await #
|> # || throw new TypeError()
|> `${#}, ${#}`
|> #[0].toUpperCase() + #.substring(1)
|> # + '!'
|> new User.Message(#)
|> stream.write
|> console.log;

When tiny functions are only used once, and when their bodies would be obvious and self-documenting in meaning, then they might be ritual boilerplate that a developer may prefer to inline: trading off self-documentation for localization of code.

{
  const promiseValue = await promise
    || throw new TypeError();
  const doubledValue =
    `${promiseValue}, ${promiseValue}`;
  const capitalizedValue
    = doubledValue[0].toUpperCase()
      + doubledValue.substring(1);
  const exclaimedValue
    = capitalizedValue + '!';
  const userMessage =
    new User.Message(exclaimedValue);
  const writeValue =
    stream.write(userMessage);
  console.log(writeValue);
}

Using a sequence of variables instead has both advantages and disadvantages. The variable names may be self-documenting. But they also are verbose. They visually distract from the crucial data transformations (overemphasizing the expressions’ nouns over their verbs), and it is easy to typo their names.

promise
|> await #
|> # || throw new TypeError()
|> normalize
|> `${#}, ${#}`
|> #[0].toUpperCase() + #.substring(1)
|> # + '!'
|> new User.Message(#)
|> await stream.write(#)
|> console.log;

With a pipeline, there are no unnecessary variable identifiers. Inserting a new step in between two steps (or deleting a step) only touches one new line. Here, a call of a function normalize was inserted between the second and third steps.

{
  const promiseValue = await promise
    || throw new TypeError();
  const normalizedValue = normalize();
  const doubledValue =
    `${normalizedValue}, ${normalizedValue}`;
  const capitalizedValue =
    doubledValue[0].toUpperCase()
      + doubledValue.substring(1);
  const exclaimedValue =
    capitalizedValue + '!';
  const userMessage =
    new User.Message(exclaimedValue);
  const writeValue =
    stream.write(userMessage);
  console.log(writeValue);
}

This code underwent a similar insertion of normalize. With a series of variables, inserting a new step in between two other steps (or deleting a step) requires editing the variable names in the following step.

input |> f |> [0, 1, 2, ...#] |> g;

A pipeline step may contain array literals.

g([0, 1, 2, ...f(input)]);

A topic-style pipeline step may also contain object literals. However, pipeline steps that are entirely object literals must be parenthesized. It is similar to how arrow functions distinguish between object literals and blocks.

input |> f |> ({ x: #, y: # }) |> g;

This is for forward compatibility with Additional Feature BP, which introduces block pipeline steps. (It is expected that block pipelines would eventually be much more common than pipelines with object literals.) This restriction could be dropped, although that would make Additional Feature BP impossible forever.

input |> f |> { x: #, y: # } |> g;
// 🚫 Syntax Error:
// Unexpected token `{`.
// Cannot parse base expression.
{
  const $ = f(input);
  g({ x, $: y: f($) });
}
f = input
|> f
|> (x => # + x);

A topic-style pipeline step may contain an inner arrow function. Both versions of this example result in an arrow function in a closure on the previous pipeline’s result input |> f.

{
  const $ = f(input);
  x => $ + x;
}

The arrow function lexically closes over the topic value, takes one parameter, and returns the sum of the topic value and the parameter.

input
|> f
|> settimeout(() => # * 5)
|> processIntervalID;

This ability to create arrow functions, which do not lexically shadow the topic, can be useful for using callbacks in a pipeline.

{
  const $ = f(input);
  const intervalID = settimeout(() => $ * 5);
  processIntervalID(intervalID);
}

The topic value of the second pipeline step (here represented by a normal variable $) is still lexically accessible within its body, an arrow function, in both examples.

input
|> f
|> (() => # * 5)
|> settimeout
|> processIntervalID;

The arrow function can also be created on a separate pipeline step.

{
  const $ = f(input);
  const callback = () => $ * 5;
  const intervalID = settimeout(callback);
  processIntervalID(intervalID);
}

The result here is the same.

input
|> f
|> () => # * 5
|> settimeout
|> processIntervalID;
// 🚫 Syntax Error:
// Unexpected token `=>`.
// Cannot parse base expression.

Note, however, that arrow functions have looser precedence than the pipe operator. This means that if a pipeline creates an arrow function alone in one of its steps, then the arrow-function expression must be parenthesized. (The same applies to assignment and yield operators, which are also looser than the pipe operator.) The example above is being parsed as if it were:

(input |> f |> ()) =>
  (# * 5 |> settimeout |> processIntervalID);
// 🚫 Syntax Error:
// Unexpected token `=>`.
// Cannot parse base expression.

The arrow function must be parenthesized, as with any other looser-precedence expression:

input
|> (f, g)
|> (() => # * 5)
|> settimeout
|> processIntervalID;

There is a shortcut for the very common case of unary function calls on variables or variables’ methods. In these cases, the topic reference can be left out. (This is the bare style of the pipe operator.)

promise
|> await #
|> # || throw new TypeError()
|> doubleSay(#, ', ')
|> capitalize
|> # + '!'
|> new User.Message(#)
|> await stream.write(#)
|> console.log;

In this example, it is not necessary to include the parenthesized argument (#) for capitalize and console.log. They were tacitly implied, forming tacit unary function calls. In other words, the example above is equivalent to the version in the next row.

console.log(
  await stream.write(
    new User.Message(
      capitalize(
        doubledSay(
          await promise
            || throw new TypeError(
              `Invalid value from ${promise}`)
        ), ', '
      ) + '!'
    )
  )
);
promise
|> await #
|> # || throw new TypeError(
    `Invalid value from ${#}`)
|> doubleSay(#, ', ')
|> capitalize(#)
|> # + '!'
|> new User.Message(#)
|> await stream.write(#)
|> console.log(#);

This version is equivalent to the version above, except that the |> capitalize(#) and |> console.log(#) pipeline steps explicitly include optional topic references #, making the expressions slightly wordier than necessary. (Any pipeline step that isn’t in bare style is said to be in topic style, because it uses the topic reference somewhere in its expression.)

console.log(
  await stream.write(
    new User.Message(
      capitalize(
        doubledSay(
          await promise
            || throw new TypeError(
              `Invalid value from ${promise}`)
        ), ', '
      ) + '!'
    )
  )
);

Being able to automatically detect this bare style is the smart part of the “smart pipe operator”. The styles of functional programming, dataflow programming, and tacit programming may particularly benefit from bare pipelines and their terse function application.

const object = input
|> f
|> # + 2
|> # * 3
|> -#
|> g(#, x)
|> o.unaryMethod
|> await asyncFunction(#)
|> await o.asyncMethod(#)
|> new Constructor(#);

This pipeline is a very linear expression, with only one level of indentation, and with each transformation step on its own line.

As with the previous examples, the … |> f is a bare unary function call, equivalent to … |> f(#). The topic reference # is unnecessary; it is invisibly, tacitly implied. The same goes for o.unaryMethod, which is a unary function call on o.unaryMethod.

This is the smart part of the smart pipe operator, which can distinguish between two syntax styles (bare style vs. topic style) by using a simple rule: bare style uses only identifiers and dots – and never parentheses, brackets, braces, or other operators. And topic style always contains at least one topic reference. For more information, see the reference below about the smart step syntax.

const object =
  new Constructor(
    await o.asyncMethod(
      await asyncFunction(
        o.unaryMethod(
          g(
            -(f(input) + 2)
              * 3,
            x
          )
        )
      )
    )
  );

In contrast to the version with pipes, this code without pipes is deeply nested, not linear.

The expression has two levels of indentation instead of one. Reading its data flow requires checking both the beginning and end of each expression, and each step expression gradually increases in size.

Inserting or removing any step of the data flow also requires changes to the indentation of any previous steps’ lines.

input |> x + 50 |> f |> g(x, 2);
// 🚫 Syntax Error:
// Topic-style pipeline step
// `|> x + 50`
// binds topic but contains
// no topic reference.
// 🚫 Syntax Error:
// Topic-style pipeline step
// `|> g(x, 2)`
// binds topic but contains
// no topic reference.

In order to fulfill the goal of “don’t shoot me in the foot”, when a pipeline step is in topic style but it contains no topic reference, that is currently an early error. Such a degenerate pipeline step has a very good chance of actually being an accidental bug. (The bare-style pipeline step |> f is not an error. The bare style is not supposed to contain any topic references #.)

For instance, this code may be clear enough:

input |> object.method;

It is a valid bare-style pipeline. Bare style is designed to be strictly simple: it must either be a simple reference or it is not in bare style.

It means:

object.method(input);

But this code would be less clear. That is why it is an early Syntax Error:

input |> object.method();
// 🚫 Syntax Error:
// Topic-style pipeline step
// `|> object.method()`
// binds topic but contains
// no topic reference.

It is an invalid topic-style pipeline. It is in topic style because it is not a simple reference; it has parentheses. And it is invalid because it is in topic style yet it does not have a topic reference.

Had that code not been an error, it could reasonably mean either of these lines:

object.method(input);
object.method()(input);

Instead, the developer must clarify what they mean, using a topic reference, into either of these two valid topic-style pipelines:

input |> object.method(#);
input |> object.method()(#);

The reading developer benefits from explicitness and clarity, without sacrificing the benefits of untangled flow that pipelines bring.

object.method(input);
object.method()(input);

Adding other arguments:

input |> object.method(x, y);
// 🚫 Syntax Error:
// Topic-style pipeline step
// `|> object.method(x, y)`
// binds topic but contains
// no topic reference.

…would make this problem of semantic ambiguity worse. But the reader is protected from this ambiguity by the same early error.

That code could have any of these reasonable interpretations:

object.method(input, x, y);
object.method(x, y, input);
object.method(x, y)(input);

Both inserting the input as the first argument and inserting it as the last argument are reasonable interpretations, as evidenced by how other programming languages’ pipe operators variously do either. Or it could be a factory method that creates a function that is in turn to be called with a unary input argument.

The writer must clarify which of these reasonable interpretations is correct:

input |> object.method(#, x, y);
input |> object.method(x, y, #);
input |> object.method(x, y)(#);
object.method(input, x, y);
object.method(x, y, input);
object.method(x, y)(input);

And this example’s ambiguity would be even worse:

input |> await object.method(x, y);
// 🚫 Syntax Error:
// Topic-style pipeline step
// `|> await object.method(x, y)`
// binds topic but contains
// no topic reference.

…were it not an invalid topic-style pipeline.

It could reasonably mean any of these lines:

await object.method(input, x, y);
await object.method(x, y, input);
await object.method(x, y)(input);
(await object.method(x, y))(input);

So the developer must clarify their intent using one of these lines:

input |> await object.method(#, x, y);
input |> await object.method(x, y, #);
input |> await object.method(x, y)(#);
input |> (await object.method(x, y))(#);
await object.method(input, x, y);
await object.method(x, y, input);
await object.method(x, y)(input);
(await object.method(x, y))(input);

Both the head and the steps of a pipeline may contain nested inner pipelines.

x = input
|> f(x =>
  # + x |> g |> # * 2)
|> #.toString();

However, just as with any other syntax, nested pipelines can quickly become complicated if not used judiciously; they are discouraged otherwise.

A nested pipeline works consistently. It merely shadows the outer context’s topic with the topic within its own steps’ inner contexts.

{
  const $ = input;
  x = f(x =>
    g($ + x) * 2
  ).toString();
}
x = input
|> # ** 2
|> f(x => #
  |> g(#, x)
  |> [# * 3, # * 5]);
{
  const $ = input ** 2;
  x = f(x => {
    const _$ = g($, x);
    return [_$ * 3, _$ * 5];
  });
}

Currently, four kinds of statements cannot use an outside context’s topic in their expressions. These are:

  • function definitions (including those for async functions, generators, and async generators; but not arrow functions, as explained above),
  • class definitions,
  • for and while statements,
  • catch clauses (but see Additional Feature TS), and
  • with statements.
x = input |> function () { return #; };
// 🚫 Syntax Error:
// Lexical context `function () { return #; }`
// contains a topic reference
// but has no topic binding.
// 🚫 Syntax Error:
// Pipeline step `|> function () { … }`
// binds topic but contains
// no topic reference.

This behavior is in order to fulfill the goals of simple scoping and of “don’t shoot me in the foot”: it prevents the origin of any topic from being difficult to find. It also fulfills the goal of forward compatibility with future additional features.

However, this behavior is subject to change, depending on feedback after the proposal is implemented in the Babel plugin.

x = input |> class { m: () { return #; } };
// 🚫 Syntax Error:
// Pipeline step `|> class { … }`
// binds topic but contains
// no topic reference.
x = input
|> await f(#, 5)
|> () => {
  if (#)
    return # + 30;
  else
    return #;
}
|> g;

Any other nested blocks may contain topic references from outer lexical environments. These include arrow functions, if statements, try statements and their finally clauses (though not their catch clauses), switch statements, and bare block statements.

x = g(await f(input, 5) + 30);

A function definition that is a pipeline step may contain topic references in its default parameters’ expressions, because their scoping is similar to that of the outside context’s: similar enough such that also allowing topic references in them would fulfill the goal of simple scoping. However, as stated above, the function body itself still may not contain topic references.

value
|> processing
|> function (x = #) { return x; }
function (x = processing(value)) {
  return x;
}

The same applies to the parenthesized antecedents of for and while loops.

input
|> process
|> (x, y) => {
  for (const element of #)
    
}
input
|> process
|> (x, y) => {
  let element;
  while (element = getNextFrom(#))
    
}
(x, y) => {
  for (const element
    of process(input))
    
}
(x, y) {
  let element;
  while (element =
    getNextFrom(input))
    
}

Real-world examples

See core-real-examples.md.

Additional features

This document is an explainer for the formal specification of a proposed smart pipe operator |> in JavaScript, along with several other additional features. The specification is divided into one Stage-0 Core Proposal plus six mutually independent-but-compatible Additional Features.

The Core Proposal is currently at Stage 0 of the TC39 process and is planned to be presented, along with a competing proposal, to TC39 by Daniel "littledan" Ehrenberg of Igalia.

There are also additional features are not part of the Stage-0 Core Proposal. They are included to illustrate possible separate follow-up proposals for the case in which the Core Proposal advances past Stage 1. Together, the Core Proposal and the additional features demonstrate a unified vision of a future in which composition, partial application, method extraction, and error handling are all tersely expressible with the same simple pipeline/topic concept.

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

Legacy link anchors

This explainer used to be a single long document before it was split up into separate appendices. These sections are to point links to subsections of the older versions of the explainer—toward the new appendices.

Core Proposal real-world examples

See core-real-examples.md.

WHATWG Fetch Standard (Core Proposal only)

See WHATWG Fetch + CP.

jQuery (Core Proposal only)

See jQuery + CP.

Underscore.js (Core Proposal only)

See Underscore.js + CP.

Lodash (Core Proposal only)

See Lodash + CP.

Smart pipe style

See core syntax.

Bare style

See core syntax, bare style.

Topic style

See core syntax, topic style.

Additional Feature BC

See Additional Feature BC.

Additional Feature BA

See Additional Feature BA.

Additional Feature BP

See Additional Feature BP.

WHATWG Fetch Standard (Core Proposal + Additional Feature BP)

See Additional Feature BP.

jQuery (Core Proposal + Additional Feature BP)

See Additional Feature BP.

Lodash (Core Proposal + Additional Feature BP)

See Additional Feature BP.

Additional Feature TS

See Additional Feature TS.

Additional Feature PF

See Additional Feature PF.

Ramda (Core Proposal + Additional Feature BP+PF)

See Additional Feature PF.

WHATWG Streams Standard (Core Proposal + Additional Features BP+PP+PF)

See Additional Feature PF.

Additional Feature NP

See Additional Feature NP.

Lodash (Core Proposal + Additional Features BP+PP+PF+NP)

See Additional Feature NP.

Ramda (Core Proposal + Additional Features BP+PF+NP)

See Additional Feature NP.

WHATWG Streams Standard (Core Proposal + Additional Features BP+PP+PF+NP)

See Additional Feature NP.

Goals

See Goals.

“Don’t break my code.”

See “Don’t break my code”.

Backward compatibility

See Backward compatibility.

Zero runtime cost

See Zero runtime cost.

Forward compatibility

See Forward compatibility.

“Don’t shoot me in the foot.”

See “Don’t shoot me in the foot”.

Opt-in behavior

See Opt-in behavior.

Simple scoping

See Simple scoping.

Static analyzability

See Static analyzability.

“Don’t make me overthink.”

See “Don’t make me overthink”.

Syntactic locality

See Syntactic locality.

Semantic clarity

See Semantic clarity.

Expressive versatility

See Expressive versatility.

Cyclomatic simplicity

See Cyclomatic simplicity.

“Make my code easier to read.”

See “Make my code easier to read”.

Untangled flow

See Untangled flow.

Distinguishable punctuators

See Distinguishable punctuators.

Terse parentheses

See Terse parentheses.

Terse variables

See Terse variables.

Terse function calls

See Terse function calls.

Terse composition

See Terse composition.

Terse partial application

See Terse partial application.

Other Goals

See Other Goals.

Conceptual generality

See Conceptual generality.

Human writability

See Human writability.

Novice learnability

See Novice learnability.

Relations to other work

See Relations to other work.

Pipelines in other programming languages

See Relations to other work.

Topic references in other programming languages

See Topic references in other programming languages.

do expressions

See do expressions.

Function binding

See Function binding.

Function composition

See Function composition.

Partial function application

See Partial function application.

Optional catch binding

See Optional catch binding.

Pattern matching

See Pattern matching.

Block parameters

See Block parameters.

do expressions

See do expressions.

Explanation of nomenclature

See Nomenclature.

Term rewriting

See Term rewriting.

proposal-smart-pipelines's People

Contributors

domenic avatar jakearchibald avatar js-choi avatar probins avatar robpalme avatar styfle avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

proposal-smart-pipelines's Issues

Object-literal bodies vs. block bodies

Feature BP conflicts with Core Proposal for pipelines whose bodies are object literals, like this: x |> { a: # }.

Arrow-function ConciseBody solves this by requiring object literals to be parenthesized. It’s probably reasonable for us to do the same. Object literals as pipeline bodies would be much less common than blocks.

The spec and explainer need to be updated in their Core Proposal sections.

Exaggeration under With No Pipelines

"Unfortunately, these deeply nested expressions often result in messy spaghetti code, due to their mixing of prefix, infix, and postfix expressions together. Writing such code requires many nested levels of indentation and parentheses. Reading such code requires checking both the left and right of each subexpression to understand its data flow."

This statement is an exaggerated view of the situation and IMHO driven by coding style rather than using simpler coding techniques that are defined in and driven by the proposal itself. For example, the following nested code, similar to the example given:

let testDataset = [ 'abc\n', 'def\n', 'ghi\n' ] ;
async function * toLine ( iterable ) { /* implementation */ }
async function * toUpperCase ( iterable ) { /* implementation */ }
async function toLogger ( iterable ) { /* implementation */ }
await toLogger(
  toUpperCase(
    toLine(
      testDataset
    )
  )
) ;

can simply be re-written as:

let testDataset = [ 'abc\n', 'def\n', 'ghi\n' ] ;
async function * toLine ( iterable ) { /* implementation */ }
async function * toUpperCase ( iterable ) { /* implementation */ }
async function toLogger ( iterable ) { /* implementation */ }
let temp = testDataset ;
temp = toLine( temp ) ;
temp = toUpperCase( temp ) ;
temp = toLogger( temp ) ;
await temp ;

This coding style has zero indentation, thus negating the overstatement in the proposal.

The general purpose of this proposal is to transform the result of the left expression and make it a parameter of the next function in the sequence, which is what the simpler coding style is doing:

let testDataset = [ 'abc\n', 'def\n', 'ghi\n' ] ;
async function * toLine ( iterable ) { /* implementation */ }
async function * toUpperCase ( iterable ) { /* implementation */ }
async function toLogger ( iterable ) { /* implementation */ }
await ( 
  testDataset
  |> toLine
  |> toUpperCase
  |> toLogger
) ;

I put parenthesis around the proposed syntax because it is unclear what using await in this situation means, e.g., is that await testDataset (no effect) or await the whole pipeline?

How would this interop with stateful closures?

To leave a particular example, how would this interoperate with stateful primitive-like operators like distinct or scan? I know it's not directly related to this proposal, but if we go with my lifted pipeline strawman as an extension of this, how would it interop with them? (The current syntax offers no convenient escape hatch to break the "smart" default, instead requiring a new name to be assigned.)

// Usage
books >:> distinct() >:> console.log(#)

// Implementation
function distinct(by = Object.is) {
    let hasPrev = false, prev
    return x => {
        const memo = hasPrev
        hasPrev = true
        return memo || by(prev, prev = x) ? [x] : []
    }
}

Bare constructor/awaited function call

I'm surprised by the inclusion of this construct. It seems a bit complicated to me, and it ends up necessitating disallowing arbitrary expressions on the right hand side of a pipeline (#2). I can see that it's used in some of your examples, but I wonder if it would be OK to just include an explicit (#) for these.

Readme/Spec: Revive grammatical right associativity

Consider bringing back right associativity so that x |> (f |> g) is valid. x |> f |> g would still also be valid. This may make any future short-circuiting conditional pipeline operator like with x ?> f |> g—as well as monadic binding, Kliesli arrows, or other sorts of higher-order composition—easier to explain. Right associativity was first introduced in 2254707 and removed in 90ada79.

Draft a version that uses braces to distinguish pipe styles

@littledan has asked whether there is any way to reconcile the F#-style-only proposal edited by @mAAdhaTTah with this F#-plus-Hack-style “smart-mix” proposal and make the latter a superset of the former. It seems likely that presenting the pipe operator to TC39 incrementally might be more likely to earn consensus for approval. That is, it might be desirable to present the simpler F#-style-only pipe proposal first, then, assuming TC39 accepts it, later also proposing a Hack-style smart-mix pipe proposal as an extension.

The obvious way to combine F# style with Hack style into a “smart mix” is to rely on the presence of the topic operator to determine which style a pipe uses. But I am still quite afraid of this from a human usability perspective; it’s a mode error waiting to happen. I suspect that it would be easy for human developers to miss the presence or absence of the topic operator, requiring careful scanning of the entire pipe expression to ensure its meaning, and undermining syntactic locality and semantic clarity.

That is why the current smart-pipe proposal restricts its tacit F# mode to identifiers. However, this choice breaks compatibility with @mAAdhaTTah’s F#-only proposal.

Another common concern people have expressed about the current smart-pipe proposal is that it disallows tacit use of metafunctions. An example of such concern may be found at tc39/proposal-pipeline-operator#134 (comment). Under the current smart-pipe proposal, using autocurried metafunctions would require explicit use of the topic reference. For instance, given divideBy, powerOf, and multiplyBy (unary functions that create unary functions), and assuming the current smart-pipe proposal using % for the topic, a developer would have to write code like this:

100|>divideBy(2)(%)|>powerOf(2)(%)|>multiplyBy(-1)(%)

It might be true that there are general issues with autocurrying in variadic-functional languages. And the idiomaticity of autocurrying in the APIs of JavaScript, the DOM, Node, etc. might be questionable. But it’s still very much worth figuring out whether the autocurrying use case can be made easier.

Most importantly, it is worth carefully thinking about whether backward compatibility can be achieved with an F#-only proposal, while avoiding human-usability hazards.

The answer is that yes: There is at least one alternative way to reconcile the two piping styles with one common operator: by marking placeholder expressions with brace-delimited blocks. Object literals can’t ever be functions, so this would be visually unambiguous…at least less so than relying on the presence of %.

In this case, |> would have tighter precedence than binary (and perhaps prefix/postfix) operators—basically functioning as a slightly looser member access—such that these are equivalent:

x|>o.m + 1
(x|>o.m) + 1

Phase One

In the initial proposal, tacit function application would look like this (assuming F# style):

x|>o.m
o.m(x)
x|>o.m(a)
o.m(a)(x)
x|>o.m(a)()
o.m(a)()(x)
x|>await
await x
x|>% + 1
SyntaxError: Unexpected %
x|>{o.m}
SyntaxError: Unexpected {
x|>{o.m(%, a)}
SyntaxError: Unexpected {
x|>{% + 1}
SyntaxError: Unexpected {

With this initial proposal, the example in tc39/proposal-pipeline-operator#134 (comment) could use the tacit syntax, because it doesn’t use braces:

100|>divideBy(2)|>powerOf(2)|>multiplyBy(-1)

Phase Two

A follow-up proposal for placeholders would look like this:

x|>o.m
o.m(x)
x|>o.m(a)
o.m(a)(x)
x|>o.m(a)()
o.m(a)()(x)
x|>await
await x
x|>% + 1
SyntaxError: Topic % is
used in pipe expression
without braces; surround
pipe expression with
braces to bind topic
x|>{o.m}
SyntaxError: Pipe
expression binds topic
% but does not use
topic; pipe expressions
surrounded by braces
always bind topic
x|>{o.m(%, a)}
o.m(x, a)
x|>{% + 1}
x + 1

Like with the current smart-pipe proposal, braces with placeholders would allow Elixir-style first-argument function calls, but they would be explicit, not tacit. The Elixir-style example from this comment by @zenparsing would look like:

import { map, filter, collect } from 'iter-funs';
someIterator|>{map(%, cb1)}|>{filter(%, cb2)}|>collect|>{%.forEach(cb3)};

Phase Three

If we eventually get to this point, I hope that tacit pipe functions, like those in Additional Feature PF, could eventually be considered by TC39 too. They would be compatible with using braces for placeholders, with just one additional operator:

+>o.m
(...$rest) => o.m(...$rest)
+>o.m(a)
(...$rest) => o.m(a)(...$rest)
+>o.m(a)()
(...$rest) => o.m(a)()(...$rest)
+>{o.m(%, a)}
o.m(x, a)
+>{% + 1}
$ => $ + 1
a.map(+>{%.toLowerCase()})
a.map($ => $.toLowerCase())
+>% + 1
SyntaxError: Topic % is
used in pipe expression
without braces; surround
pipe expression with
braces to bind topic
+>{o.m}
SyntaxError: Pipe
expression binds topic
% but does not use
topic; pipe expressions
surrounded by braces
always bind topic

Phase Four

And, assuming that Phase Three is adopted, then N-ary pipes (Additional Feature NP) would make them even more useful.

a.sort(+>{% - %%})
a.sort(($, $$) => $ - $$)
a.sort(+>{%.localeCompare(%%)})
a.sort(($, $$) => $.localeCompare($$))
const debug =
  +>{console.log('[debug]', ...)};
debug(1, 2, 3);
const debug =
  (...$rest) =>
    console.log('[debug]', ...$rest);
debug(1, 2, 3);

Questions

Some problems with braces would be that:

  • Braces would make it more difficult to create object literals, which would require nested grouping operators like with x|>{{a: %, b}}. But this would not be unique to pipes. Arrow functions already do something similar with x => ({a: x, b}).

  • Braces may make developers think that they could add statements into the braces, since they look like regular blocks. Developers might expect x|>{ console.log(%); % + 1 } to just work, but it would be a syntax error. This might occasionally be annoying but at least the error is an early error. And if do expressions ever get accepted then placeholder pipes could be extended to support statement lists with do-like semantics.

Other questions include:

  • How tight should the precedence be? Consider the following examples, which assume that the operator’s tightness is between the binary and unary operators:

    100|>divideBy(2)|>powerOf(2)|>multiplyBy(-1)
    
    someIterator|>{map(%, cb1)}|>{filter(%, cb2)}|>collect|>{%.forEach(cb3)}
    
    !flag|>processFlag
    
    !state.done || !(num0|>greaterThan(num1))
    
    num|>Math.log|>{new Message(%)}
    
    iterator|>{map(%, cb1)}|>{%.forEach(callback)}

    If the operator is tightened to between unary operators and method access, then the examples become:

    100|>divideBy(2)|>powerOf(2)|>multiplyBy(-1)
    
    someIterator|>{map(%, cb1)}|>{filter(%, cb2)}|>collect|>{%.forEach(cb3)}
    
    (!flag)|>processFlag
    
    !state.done || !num0|>greaterThan(num1)
    
    num|>Math.log|>{new Message(%)}
    
    iterator|>{map(%, cb1)}|>{%.forEach(callback)}

    If the operator is further tightened to have the same precedence as member access, then they become:

    100|>divideBy(2)|>powerOf(2)|>multiplyBy(-1)
    
    someIterator|>{map(%, cb1)}|>{filter(%, cb2)}|>collect|>{%.forEach(cb3)}
    
    (!flag)|>processFlag
    
    !state.done || !num0|>greaterThan(num1)
    
    num|>(Math.log)|>{new Message(%)}
    
    iterator|>{map(%, cb1)}.forEach(callback)

    Which tradeoffs would generally be best?

  • Would -> be a better choice for the operator than |>? Other programming languages use -> as a very tight operator for member access or related concepts, such as with C++’s pointer->MemberFunction(). If a tight precedence is chosen for the pipe operator, especially if it's equally or nearly as tight as method access, then -> might be better than |> at conveying the analogy between member access and function/expression application.

    100->divideBy(2)->powerOf(2)->multiplyBy(-1)
    
    someIterator->{map(%, cb1)}->{filter(%, cb2)}->collect->{%.forEach(cb3)}
    
    (!flag)->processFlag
    
    !state.done || !num0->greaterThan(num1)
    
    num->(Math.log)->{new Message(%)}
    
    iterator->{map(%, cb1)}.forEach(callback)
  • Should the tacit syntax favor F# style (as im the examples above) or Elixir style? It is probably a fundamental trade off that both tacit Elixir style and tacit F# style cannot be equally accommodated by the same operator. The example in tc39/proposal-pipeline-operator#134 (comment) would be the following if the tacit syntax favored F# style:

    100|>divideBy(2)|>powerOf(2)|>multiplyBy(-1)

    …but would change to the following if the syntax favored Elixir style:

    100|>divideBy(2)()|>powerOf(2)()|>multiplyBy(-1)()

    And tc39/proposal-pipeline-operator#143 (comment) would be the following if the tacit syntax favored F# style:

    import { map, filter, collect } from 'iter-funs';
    someIterator|>{map(%, cb1)}|>{filter(%, cb2)}|>collect|>{%.forEach(cb3)};

    …and if it favored Elixir style:

    import { map, filter, collect } from 'iter-funs';
    someIterator|>map(cb1)|>filter(cb2)|>collect|>{%.forEach(cb3)};

I don’t know what I’d call this brace-using idea, but I think I might already like it more than the current smart mix proposal, even if it’s slightly chunkier.

And it would automatically be a superset of @mAAdhaTTah’s current F#-only proposal (as long as the proposal tweaks its precedence and adds two early errors for cases that would never naturally occur in F# style alone anyway).

Disallowing arbitrary expressions as a pipeline function

Interesting idea here, with the "don't shoot yourself in the foot" reasoning. I agree that I'd rather not encourage currying as the main way to use the pipeline operator, but it seems like this syntax disallows that entirely. Is this intentional or desired?

Feature PP (pipelines with implicit head) seems weakly motivated

I'm struggling to see the reasoning for feature PP. Adding it lets people save exactly one character whenever it's used, but introduces ASI hazards and, imo, makes things look slightly more confusing in general. (I find myself looking for the topic in the examples that use it, then realizing that there is no topic and inferring that it's using PP.)

If you're not paying close attention to things, you can accidentally miss that something is a nested prefix pipeline - x |> foo + ( |> bar(#) ) can, depending on formatting, be mistaken for x |> foo |> bar(#).

If I'm not mistaken, this also allows topic-form to not include a #: x |> (|> bar) would use the outer topic, but wouldn't express it literally. Catch-22 here: if this is valid, it breaks the expectation that topic-form always has a # in it; if it's not valid, it's a confusing instance where prefix-pipeline can't be used due to surrounding context. (That is, x |> # + (|> bar) would be valid, so the error depends on surrounding context.)

It also complicates the variadic-handling of pipeline functions, as you note in #4 (comment).

I also think it ends up being somewhat confusing with pipeline-functions; both of them start with a pipeline-ish operator, but one creates a function to obtain the topic, the other just immediately uses the surrounding topic. In other words, I see xs.map(|> foo) being a footgun with PP, where the author meant xs.map(+> foo). Without PP this is a syntax error as written; if correctly written as xs.map(# |> foo), it's much more obvious what's happening.

(I can't find the link right now, but at least one person in the pipeline-proposal repo was asking for |> foo to automatically create a pipeline function, so the confusion is already possible.)

Overall, I just don't see the particular value of being able to omit the # from the beginning of a pipeline, considering the downsides.

Explainer: Remove optional chaining ??. from examples

Some examples use ??., which may confuse some people who are not up to date on optional chaining. Smart pipelines are not dependent on optional chaining (just like how they’re not dependent on do expressions, cf. #1). The examples need to be rewritten to not use ??..

Why was `#` chosen as the topic reference?

Q1
Can we use/is there a reason why we can't use this as the topic reference keyword instead of #? I think that the current use of this in JavaScript and the topic reference here are analogous. In addition, consider that this proposal would in practice cover many of the same use cases as the bind operator proposal, which uses this in the spot analogous to where # is used here.

Q2
Why was # chosen in particular?

Readme: Separate additional features, goals, nomenclature, term rewriting, and relations to other proposals and languages

From IRC #tc39 today with @littledan:

littledan (Daniel Ehrenberg): …I think this could partly be addressed by a simpler-looking document layout (e.g., maybe split off the explainer pieces about follow-on proposals into separate md documents?) …

jschoi (J. S. Choi): Regarding the explainer: I can try separating the additional features into their own explainer documents. This is probably long overdue, but I have been focusing on the specification for the past week. I’ll make an issue for this in the smart-pipelines repository.

jschoi (J. S. Choi): The specification is currently a single document too, with an annex for each additional feature. Is this similarly too confusing or overwhelming for the reader? Should I consider separating the specification also? Perhaps I should.

littledan (Daniel Ehrenberg): I think the specification being in one document vs multiple documents is less important, as many fewer people read and understand the specification compared to the explainer. The specification is usually unintelligible to most audiences, and difficult to the rest of them, no matter how you cut it.

jschoi (J. S. Choi): Noted; thank you.

littledan (Daniel Ehrenberg): oh, another contradiction: explaining everything fully vs concisely emphasizing the important stuff

jschoi (J. S. Choi): The real-world examples hopefully help concretize how it would look, but…yes.

10:35 AM littledan (Daniel Ehrenberg): the readme is very long! this means you have spoken to many things, but a portion of the audience will just be unaware of your thought process. This is an inherent difficulty

10:36 AM jschoi (J. S. Choi): That’s true too. Maybe I should make an explainer explainer, pfft. Or separate the examples…? I think the examples do help make it seem more compelling.

Yes. Hopefully the readme split will help.

At least on a communicative level, though not so much at a fundamental-tradeoff level.

Full bare style option

@js-choi at least could you allow the syntaxe with a plugin option ? it's handy and the # give a parsing error in prettier so we can't format our code, it should be a prettier issue but as this # is not 'definitive' nor pure functionnal we would have to remove it anyway when using F# pipelines or this one in pure bare style

image

image

Feature: Pipeline transforms

Let's go further and play with the language to parse a mapper


Christmas is coming.. 🙏

map

[1,2] ||> # + 1 |> console.log // [2,3]

const add = a => b => a + b
// bare style :)
[1,2] ||> add(1) |> console.log // [2,3]

// to
console.log([1,2].map(add(1)))

filter

[1,2] <|> (# > 1) |> console.log // [2]

const above = a => b => b > a
[1,2] <|> above(1) |> console.log // [2]

// to
console.log([1,2].filter(above(1)))

reduce

[1,2] <|| # + ## |> console.log // 3

const add = a => b => a + b
[1,2] <|| add |> console.log // 3

// bare bare bare
// [1,2] <|| add(#)(##) |> console.log // 3

// to
console.log([1,2].reduce((a,b) => a + b))

Relationship to do-expressions

The do expressions proposal is at Stage 1, and there are some serious concerns that some TC39 members have about it (especially about completion values). Does this proposal depend on do expressions? I don't see do expressions as part of the specification here. If there is no dependency, I'd suggest moving examples based on do expressions into a separate section, to make the lack of dependency clear.

Clarify relationship with robust method extraction

See tc39/proposal-pipeline-operator#110 (comment), tc39/proposal-pipeline-operator#107 (comment), tc39/proposal-pipeline-operator#101 (comment).

I had forgotten that a discussion about robust method pre-extraction happened last September. By the time they returned to it in November, time was up. January was skipped while preparing for this, though @ljharb gave a comment about the importance of the prefix ::, as well as recently that it must be robust against delete Function.prototype.bind. ljharb/function.prototype.name gives examples of this pattern. There has been discussion of method-binding caching in tc39/proposal-bind-operator#46, in which a WeakMap is associated with each realm.

In any case, pipelines, including smart pipelines, and robust cached method pre-extraction can coexist. Using ljharb/function.prototype.name/implementation.js, line 19 as an example of useful coexistence:

return fn
|> &Function.prototype.toString
|> &String.prototype.match(#, classRegex)
|> !!#;

…which is just:

return !!&String.prototype.match(
  &Function.prototype.toString(fn),
  classRegex);

This security robustness is quite a different use case than simply wanting to express the callbacks in promise.then($ => console.log($)) more tersely, as well as the other goals of smart pipelines as well as the goals of other pipelines: most importantly, the untangled composition of expressions and calls.

I think secure cached method binding is out of scope for smart pipelines, even with Feature PF. It is also out of scope for @mAAdhaTTah’s Proposal-1 pipelines. These are orthogonal use cases. They cannot kill each other.

Spec: PipelineStep must cover ConditionalExpression with supplemental production

The current context-free grammar:

Pipeline[In, Yield, Await] :
  PipelineStep[?In, ?Yield, ?Await]
  PipelineStep[?In, ?Yield, ?Await] `|>` Pipeline[?In, ?Yield, ?Await]

PipelineStep[In, Yield, Await] :
  PipelineBareFunction
  [lookahead ∉ {`{`}] ConditionalExpression[?In, ?Yield, ?Await]

…is ambiguous, despite the early error that requires PipelineStep : ConditionalExpression to contain a topic reference.

This can be fixed by changing it to:

Pipeline[In, Yield, Await] :
  CoverPipelineBareStepAndTopicStep[?In, ?Yield, ?Await]
  CoverPipelineBareStepAndTopicStep[?In, ?Yield, ?Await] `|>` Pipeline[?In, ?Yield, ?Await]

…then movingPipelineBareFunction : SimpleReference (and other bare-style productions) to a supplemental grammar:

PipelineBareStep[In, Yield, Await] :
  SimpleReference
  `new` SimpleReference [if Feature BC]
  `await` SimpleReference [if Feature BA]

PipelineBareStep[In, Yield, Await] :
  SimpleReference
  `new` SimpleReference [if Feature BC]
  `await` SimpleReference [if Feature BA]

PipelineTopicStep[In, Yield, Await] :
  [lookahead ∉ {`{`}] ConditionalExpression[?In, ?Yield, ?Await]

A static semantic rule should then, given a CoverPipelineBareStepAndTopicStep, return either a PipelineBareStep or a PipelineTopicStep.

Explainer: Add goal about semantic nonambiguity

An advantage that this proposal has is that parentheses can never change the meaning of a code without giving an early error. x |> (await #) is the same as x |> await #, and x |> (await y) and x |> await y are both early errors that require the developer to disambiguate between x |> (await y)(#) and x |> await y(#). This should be noted in Goals and in the Motivation, Core Proposal too, as well as the pipeline-operator wiki.

Edit: That advantage is moot. However, this proposal still has an advantage in that it forces the writer to make explicit which of these reasonable alternatives do they mean by x |> foo: x |> foo(#, y), x |> foo(y, #), and x |> foo(y)(#). This is also important, so I’m reusing this issue to track better explaining this in § Goals, in § Motivation, Core Proposal, and in the wiki. See #2 (comment).

Feature PF: Pipeline functions need to be variadic even without Feature NP

The current draft’s Feature PF uses +> to create unary-parameter functions. This means that arguments after the first argument are discarded. This may result in surprising behavior, especially given that +> is intended to address method extraction. For instance, (+> console.log)(5, 2) is equivalent to ($ => $ |> console.log)(5, 2), which prints only 5. This is not equivalent to console.log.bind(console), contrary to what the explainer currently says. In addition, this makes Feature PF without Feature NP incompatible with adding Feature NP in the future.

The spec needs to be changed so that Feature PF creates variadic functions that apply all their arguments to the first. +> console.log would instead be equivalent to (...$) => console.log(...$), and (+> console.log)(5, 2) would correctly print 5 and 2.

Much of the work is already done, because even the Core Proposal allows for multiple topics. Feature PP would also need to be changed such that, if a pipeline’s lexical environment has multiple topics, then all of them are applied to the pipeline’s body.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.