Skip to content
J. S. Choi edited this page Sep 11, 2021 · 126 revisions

Overview

This page summarizes our public discussions, which have been occurring since 2017.


Goals

  • Easy composition of functions for immediate invocation
  • Support for any function arity, not just unary functions
  • Easy composition of other expressions (method calls, math, array/object literals, new object construction, etc.)
  • Able to await in the middle of a pipeline
  • Method calls without binding
  • Avoiding accidental footguns that would result in runtime errors

Summary of proposals’ behavior

Original expression Proposal 0 (minimal F#) Proposal 1 (F# pipes + await) Proposal 2 (Hack pipes)
o.m(x) x |> o.m x |> o.m x |> o.m(^)
o.m(0, x) x |> y=>o.m(0, y) x |> y=>o.m(0, y) x |> o.m(0, ^)
new o.m(x) x |> y=>new o.m(y) x |> y=>new o.m(y) x |> new o.m(^)
o[x] x |> y=>o[x] x |> y=>o[y] x |> o[^]
x[i] x |> y=>x[i] x |> y=>y[i] x |> ^[i]
x + 1 x |> y=>y + 1 x |> y=>y + 1 x |> ^ + 1
[0, x] x |> y=>[0, y] x |> y=>[0, y] x |> [0, ^]
{ key: x } x |> y=>({ key: y }) x |> y=>({ key: y }) x |> { key: ^ }
await o.m(x) Not supported x |> o.m |> await x |> await o.m(^)
yield o.m(x) Not supported Not supported x |> (yield o.m(^))

Proposal 0: Minimal F# style

Minimal-F#-style explainer
Minimal-F#-style explainer

The F# (F-sharp / F♯) style |> invokes the right-hand side with the evaluated result of the left. It has also been called “implicit call”, “tacit” and “point-free” style.

  • Unary function calls are very terse
  • Anything else must be wrapped in an arrow function
  • Arrow functions might or might not have to be wrapped in parentheses
  • Cannot handle await or yield expressions, which are scoped to their innermost function.

Proposal 1: F# style with await syntax

F#-style-with-await explainer
F#-style-with-await specification

The F# (F-sharp / F♯) style |> invokes the right-hand side with the evaluated result of the left. It has also been called “implicit call”, “tacit” and “point-free” style. Proposal 1 is specifically for F# style only along with special extra syntax for await. Proposal 0 is similar except it forbids await in its RHS.

  • Unary function calls are very terse
  • Anything else must be wrapped in an arrow function
  • Arrow functions might or might not have to be wrapped in parentheses
  • Handles await expressions with special extra syntax (|> await)
  • Cannot handle yield expressions, which are scoped to their innermost function.
// Basic Usage
x |> f     //-->  f(x)
x |> f(y)  //-->  f(y)(x)

// 2+ Arity Usage
x |> (a => f(a,10))   //-->  f(x,10)

// Async Solution
x |> f |> await       //-->  await f(x)
x |> f |> await |> g  //-->  g(await f(x))

// Other Expressions
f(x) |> (a => a.data)           //-->  f(x).data
f(x) |> (a => a[a.length-1])    //-->  let temp=f(x), temp[temp.length-1]
f(x) |> (a => ({ result: a }))  //-->  { result: f(x) }

// Complex example
anArray => (anArray
 |> a => pickEveryN(a, 2)
 |> a => a.filter(...)
 |> makeQuery
 |> a => readDB(a, config)
 |> await
 |> extractRemoteUrl
 |> fetch
 |> await
 |> parse
 |> console.log
);

Proposal 2: Hack style

Hack-style explainer
Hack-style specification

The Hack style |> evaluates the left-hand side and assigns it to a temporary binding scoped to the right-hand side. First proposed in issue #84. It has also been called “topic”, “binding”, “placeholder”, and “parameterized” style.

  • Pipe to any expression using an explicit placeholder token ^
  • Placeholder token can go where any normal variable can go
  • Placeholder token might be ? or ^ or @ or # (see issue #91)
  • No tacit unary function calls, need to add (^) to their ends
  • Handles await and yield expressions without special extra syntax
  • Forward compatible with both split mix (Proposal 3) and smart mix (Proposal 4)

Tab Atkins explains why he prefers Hack style.

// Basic Usage
x |> f(^)     //-->   f(x)
x |> f(y)(^)  //-->   f(y)(x)
x |> f        //-->   Syntax Error

// 2+ Arity Usage
x |> f(^,10)   //-->  f(x,10)

// Async Solution (Note this would not require special casing)
x |> await f(^)          //-->  await f(x)
x |> await f(^) |> g(^)  //-->  g(await f(x))

// Other Expressions
f(x) |> ^.data           //-->  f(x).data
f(x) |> ^[^.length-1]    //-->  let temp=f(x), temp[temp.length-1]
f(x) |> { result: ^ }    //-->  { result: f(x) }

// Complex example
anArray => anArray
 |> pickEveryN(^, 2)
 |> ^.filter(...)
 |> makeQuery(^)
 |> await readDB(^, config)
 |> extractRemoteUrl(^)
 |> await fetch(^)
 |> parse(^)
 |> console.log(^);

Proposal 3: Split mix

Since both F# and Hack style proposals have desirable properties, it’s worth considering proposals that mix them together in a cohesive manner. Discussed in issue #89; previously discussed in issue #75 and issue #84.

  • Requires browser vendors to agree to implement two similar pipe operators, so nobody is currently backing this proposal
  • One operator for expressions with placeholder tokens (Hack style), e.g., |> or |:
    • Pipe to any expression using an explicit placeholder token ^
    • Placeholder token can go where any normal variable can go
  • Placeholder token might be ? or ^ or @ or # (see issue #91)
    • Handles await and yield expressions without special extra syntax
  • One operator for tacit unary function calls (F# style), e.g., |>> or |>
    • Unary function calls are very terse
// Basic Usage
x |>> f     //-->   f(x)
x |> f(^)  //-->   f(x)

x |>> f(y)     //-->   f(y)(x)
x |> f(y)(^)  //-->   Syntax Error

// 2+ Arity Usage
x |> f(^, 10)   //-->  f(x,10)

// Async solution (does not require special casing)
x |> await f(^)       //-->  await f(x)
x |> await f(^) |> g  //-->  g(await f(x))

// Other expressions
f(x) |> ^.data           //-->  f(x).data
f(x) |> ^[^.length-1]    //-->  let temp=f(x), temp[temp.length-1]
f(x) |> { result: ^ }    //-->  { result: f(x) }

// Complex example
anArray => anArray
 |> pickEveryN(^, 2)
 |> ^.filter(...)
 |>> makeQuery
 |> await readDB(^, config)
 |>> extractRemoteUrl
 |>> await fetch(^)
 |>> parse
 |>> console.log;

Proposal 4: Smart mix

Smart-mix explainer
Smart-mix specification

  • This proposal was withdrawn in favor of Hack style, which is forward compatible with this proposal
  • Combines features of the two main proposals into a single operator
  • Topic/Hack style:
    • Pipe to any expression using an explicit placeholder token ^
    • Placeholder token can go where any normal variable can go
    • Placeholder token might be ^ or ? or % or @ or # (see issue #91)
    • Handles await and yield expressions without special extra syntax
  • Tacit/bare/F# style:
    • Unary function calls are very terse if a specific syntax
    • The expression must be a “simple” reference: one or more identifiers separated by ., like console.log
    • The cannot contain parentheses, brackets, braces, the placeholder token, or other operators
  • Syntax error when body of pipeline does not match either style
    • Ensures easy distinguishability between topic mode and F# mode
    • x |> f(a, b) is invalid and must be disambiguated into x |> f(^, a, b), x |> f(a, ^, b), x |> f(a, b, ^), or x |> f(a, b)(^)
  • If body of pipeline is currently written in F# style, and you realize you want to do something else to it, you must shift it to Hack style;
    it’s a compile-time syntax error if you forget
// Basic Usage
x |> f     //-->   f(x)
x |> f(^)  //-->   f(x)

// 2+ Arity Usage
x |> f(y)     //-->   Syntax Error
x |> f(y, ^)  //-->   f(y, x)
x |> f(^, y)  //-->   f(x, y)
x |> f(y)(^)  //-->   f(y)(x)

// Async Solution (Note this would not require special casing)
x |> await f(^)       //-->  await f(x)
x |> await f(^) |> g  //-->  g(await f(x))

// Arbitrary Expressions
f(x) |> ^.data           //-->  f(x).data
f(x) |> ^[^.length-1]    //-->  let temp=f(x), temp[temp.length-1]
f(x) |> { result: ^ }    //-->  { result: f(x) }

// Complex Example
anArray => anArray
 |> pickEveryN(^, 2)
 |> ^.filter($ => $ > 0)
 |> makeQuery
 |> await readDB(^, config)
 |> extractRemoteUrl
 |> await fetch(^)
 |> parse
 |> new User.Result(^)
 |> console.log;
Clone this wiki locally