Skip to content
This repository was archived by the owner on Jan 26, 2022. It is now read-only.

Commit f53a6e3

Browse files
authored
explainer: Elaborate on trade-off symmetry, other rewording
Discusses destructuring too. Closes #4.
1 parent 498dabd commit f53a6e3

File tree

1 file changed

+160
-90
lines changed

1 file changed

+160
-90
lines changed

README.md

Lines changed: 160 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,18 @@ This [choice of token is not a final decision][token bikeshedding];
2121
In the State of JS 2020 survey, the **fourth top answer** to
2222
[“What do you feel is currently missing from
2323
JavaScript?”](https://2020.stateofjs.com/en-US/opinions/?missing_from_js)
24-
was the **pipe operator**.
24+
was a **pipe operator**. Why?
2525

2626
When we perform **consecutive operations** (e.g., function calls)
2727
on a **value** in JavaScript,
28-
there are currently two fundamental ways to do so:
28+
there are currently two fundamental styles:
2929
* passing the value as an argument to the operation
3030
(**nesting** the operations if there are multiple operations),
3131
* or calling the function as a method on the value
3232
(**chaining** more method calls if there are multiple methods).
3333

3434
That is, `three(two(one(value)))` versus `value.one().two().three()`.
35+
However, these styles differ much in readability, fluency, and applicability.
3536

3637
### Deep nesting is hard to read
3738
The first style, **nesting**, is generally applicable –
@@ -84,29 +85,37 @@ In order to read its flow of data, a human’s eyes must first:
8485
5. `chalk.dim()` (left side), then
8586
6. `console.log()` (left side).
8687

88+
As a result of deeply nesting many expressions
89+
(some of which use **prefix** operators,
90+
some of which use **postfix** operators,
91+
and some of which use **circumfix** operators),
92+
we must check **both left and right sides**
93+
to find the **head** of **each expression**.
94+
8795
</details>
8896

8997
### Method chaining is limited
90-
The second style, **chaining**, is **only** usable
98+
The second style, **method chaining**, is **only** usable
9199
if the value has the functions designated as **methods** for its class.
92-
This **limits** its applicability, but **when** it applies,
93-
it’s generally more usable and **easier** to read and write:
100+
This **limits** its applicability.
101+
But **when** it applies, thanks to its postfix structure,
102+
it is generally more usable and **easier** to read and write.
94103
Code execution flows **left to right**.
95-
The deeply nested expressions are **untangled**.
96-
All the arguments for a given function are **grouped** with the function name.
104+
Deeply nested expressions are **untangled**.
105+
All arguments for a function call are **grouped** with the function’s name.
97106
And editing the code later to **insert or delete** more method calls is trivial,
98107
since we would just have to put our cursor in one spot,
99108
then start typing or deleting one **contiguous** run of characters.
100109

101110
Indeed, the benefits of method chaining are **so attractive**
102111
that some **popular libraries contort** their code structure
103-
*specifically* to allow **more method chaining**.
104-
The most prominent example is **[jQuery][]**, which is
105-
*still* the most popular JS library in the world.
112+
specifically to allow **more method chaining**.
113+
The most prominent example is **[jQuery][]**, which
114+
still remains the **most popular JS library** in the world.
106115
jQuery’s core design is a single über-object with dozens of methods on it,
107-
all of which return the same object type so that we can continue chaining.
116+
all of which return the same object type so that we can **continue chaining**.
108117
There is even a name for this style of programming:
109-
[fluent interfaces][].
118+
**[fluent interfaces][]**.
110119

111120
[jQuery]: https://jquery.com/
112121
[fluent interfaces]: https://en.wikipedia.org/wiki/Fluent_interface
@@ -218,22 +227,30 @@ It is often simply too **tedious and wordy** to **write**
218227
code with a long sequence of temporary, single-use variables.
219228
It is arguably even tedious and visually noisy for a human to **read**, too.
220229

221-
If [naming is one of the **most difficult tasks** in programming][naming hard],
230+
If [**naming** is one of the **most difficult tasks** in programming][naming hard],
222231
then programmers will **inevitably avoid naming** variables
223232
when they perceive their benefit to be relatively small.
224233

225234
[naming hard]: https://martinfowler.com/bliki/TwoHardThings.html
226235

227236
## Why the Hack pipe operator
228237
There are **two competing proposals** for the pipe operator: Hack pipes and F# pipes.
229-
The two pipe proposals just differ slightly on what the “magic” is,
230-
and thus on precisely how we spell our code when using `|>`.
231238
(There **was** a [third proposal for a “smart mix” of the first two proposals][smart mix],
232239
but it has been withdrawn,
233240
since its syntax is strictly a superset of one of the proposals’.)
234241

235242
[smart mix]: https://github.com/js-choi/proposal-smart-pipelines/
236243

244+
The two pipe proposals just differ **slightly** on what the “magic” is,
245+
when we spell our code when using `|>`.
246+
247+
**Both** proposals **reuse** existing language concepts:
248+
Hack pipes are based on the concept of the **expression**,
249+
while F# pipes are based on the concept of the **unary function**.
250+
251+
Piping **expressions** and piping **unary functions**
252+
correspondingly have **small** and nearly **symmetrical trade-offs**.
253+
237254
### This proposal: Hack pipes
238255
In the **Hack language**’s pipe syntax,
239256
the righthand side of the pipe is an **expression** containing a special **placeholder**,
@@ -245,25 +262,38 @@ to pipe `value` through the three functions.
245262
and the placeholder can go anywhere any normal variable identifier could go,
246263
so we can pipe to any code we want **without any special rules**:
247264

248-
* `value |> one(%)` for function calls,
249-
* `value |> one(1, %)` for multi-argument function calls,
250-
* `value |> %.foo()` for method calls
251-
(or `value |> obj.foo(%)`, for the other side),
265+
* `value |> foo(%)` for unary function calls,
266+
* `value |> foo(1, %)` for n-ary function calls,
267+
* `value |> %.foo()` for method calls,
252268
* `value |> % + 1` for arithmetic,
269+
* `value |> [%, 0]` for array literals,
270+
* `value |> {foo: %}` for object literals,
271+
* `` value |> `${%}` `` for template literals,
253272
* `value |> new Foo(%)` for constructing objects,
254273
* `value |> await %` for awaiting promises,
274+
* `value |> (yield %)` for yielding generator values,
255275
* `value |> import(%)` for calling function-like keywords,
256276
* etc.
257277

258-
**Con:** If **all** we’re doing is piping through **already-defined unary functions**,
259-
Hack pipes are **slightly** more verbose than F# pipes,
260-
since we need to **actually write** the function-call syntax
261-
by adding a `(%)` to it.
278+
**Con:** Piping through **unary functions**
279+
is **slightly more verbose** with Hack pipes than with F# pipes.
280+
This includes unary functions
281+
that were created by **[function-currying][] libraries** like [Ramda][],
282+
as well as [unary arrow functions
283+
that perform **complex destructuring** on their arguments][destruct]:
284+
Hack pipes would be slightly more verbose
285+
with an **explicit** function call suffix `(%)`.
286+
287+
[function-currying]: https://en.wikipedia.org/wiki/Currying
288+
[Ramda]: https://ramdajs.com/
289+
[destruct]: https://github.com/js-choi/proposal-hack-pipes/issues/4#issuecomment-817208635
262290

263291
### Alternative proposal: F# pipes
264292
In the [**F# language**’s pipe syntax][F# pipes],
265-
the righthand side of the pipe is an expression that must **evaluate into a function**,
266-
which is then **tacitly called** with the lefthand side’s value as its **sole argument**.
293+
the righthand side of the pipe is an expression
294+
that must **evaluate into a unary function**,
295+
which is then **tacitly called**
296+
with the lefthand side’s value as its **sole argument**.
267297
That is, we write `value |> one |> two |> three` to pipe `value`
268298
through the three functions.
269299
`left |> right` becomes `right(left)`.
@@ -295,71 +325,102 @@ envars
295325
```js
296326
envars
297327
|> Object.keys
298-
|> x => x.map(envar =>
328+
|> x=> x.map(envar =>
299329
`${envar}=${envars[envar]}`,
300330
)
301-
|> x => x.join(' ')
302-
|> x => `$ ${x}`
303-
|> x => chalk.dim(x, 'node', args.join(' '))
331+
|> x=> x.join(' ')
332+
|> x=> `$ ${x}`
333+
|> x=> chalk.dim(x, 'node', args.join(' '))
304334
|> console.log;
305335
```
306336

307337
</details>
308338

309-
**Pro:** The restriction that the righthand side *must* resolve to a function
339+
**Pro:** The restriction that the righthand side
340+
**must** resolve to a unary function
310341
lets us write very terse pipes
311-
**when** the operation we want to perform is **already a unary function**.
342+
**when** the operation we want to perform
343+
is a **unary function call**:
344+
345+
* `value |> foo` for unary function calls.
346+
347+
This includes unary functions
348+
that were created by **[function-currying][] libraries** like [Ramda][],
349+
as well as [unary arrow functions
350+
that perform **complex destructuring** on their arguments][destruct]:
351+
F# pipes would be **slightly less verbose**
352+
with an **implicit** function call (no `(%)`).
312353

313354
**Con:** The restriction means that **any operations**
314355
that are performed by **other syntax**
315-
must be done by **wrapping** the operation in a unary **arrow function**:\
316-
`value |> x=>x[0]`,\
317-
`value |> x=>x.foo()`,\
318-
`value |> x=>x+1`,\
319-
`value |> x=>new Foo(x)`,\
320-
`value |> x=>import(x)`,\
321-
etc.\
322-
Even calling **named functions requires wrapping**
323-
when we need to pass **more than one argument**:\
324-
`value |> x=>f(1, x)`.
325-
326-
**Con:** The **`yield` and `await`** operations are scoped
327-
to their containing function,
328-
and thus can’t be handled by the arrow-function workaround
329-
from the previous paragraph.
330-
If we want to integrate them into a pipe expression
331-
(rather than requiring the pipe to be parenthesis-wrapped and prefixed with `await`),
332-
[`await` and `yield` need to be handled as **special syntax cases**][enhanced F# pipes]:
333-
`value |> await |> one` to simulate `one(await value)`, etc.
356+
must be made **slightly more verbose** by **wrapping** the operation
357+
in a unary **arrow function**:
358+
359+
* `value |> x=> x.foo()` for method calls,
360+
* `value |> x=> x + 1` for arithmetic,
361+
* `value |> x=> [x, 0]` for array literals,
362+
* `value |> x=> {foo: x}` for object literals,
363+
* `` value |> x=> `${x}` `` for template literals,
364+
* `value |> x=> new Foo(x)` for constructing objects,
365+
* `value |> x=> import(x)` for calling function-like keywords,
366+
* etc.
367+
368+
Even calling **named functions** requires **wrapping**
369+
when we need to pass **more than one argument**:
370+
371+
* `value |> x=> foo(1, x)` for n-ary function calls.
372+
373+
**Con:** The **`await` and `yield`** operations are **scoped**
374+
to their **containing function**,
375+
and thus **cannot be handled by unary functions** alone.
376+
If we want to integrate them into a pipe expression,
377+
[`await` and `yield` must be handled as **special syntax cases**][enhanced F# pipes]:
378+
379+
* `value |> await` for awaiting promises, and
380+
* `value |> yield` for yielding generator values.
334381

335382
[enhanced F# pipes]: https://github.com/valtech-nyc/proposal-fsharp-pipelines/
336383

337-
### Hack pipes favor more-common use cases
384+
### Hack pipes favor more common expressions
338385
**Both** Hack pipes and F# pipes respectively impose
339-
a small **syntax tax** on different cases:\
340-
**Hack pipes** tax only **unary functions**;\
341-
**F# pipes** tax **everything besides unary functions**.
342-
343-
The case of “unary function” is in general **less common**
344-
than “**everything besides** unary functions”,
345-
so it may make more sense to impose a tax on the former rather than the latter.
346-
347-
In particular, **method** calling and **non-unary function** calling
348-
will **always** be **popular**.
349-
Those two cases **alone** equal or exceed
350-
unary function calling in frequency,
351-
let alone other syntaxes such as **array/object literals** and **arithmetic operations**.
352-
353-
Several other proposed **new syntaxes**,
354-
such as **[extension][]** calling,
386+
a small **syntax tax** on different expressions:\
387+
**Hack pipes** slightly tax only **unary function calls**, and\
388+
**F# pipes** slightly tax **all expressions except** unary function calls.
389+
390+
In **both** proposals, the syntax tax per taxed expression is **small**
391+
(**both** `(%)` and `x=>` are **only three characters**).
392+
However, the tax is **multiplied** by the **prevalence**
393+
of its respectively taxed expressions.
394+
It therefore might make sense
395+
to impose a tax on whichever expressions are **less common**
396+
and to **optimize** in favor of whichever expressions are **more common**.
397+
398+
Unary function calls are in general **less common**
399+
than **all** expressions **except** unary functions.
400+
In particular, **method** calling and **n-ary function** calling
401+
will **always** be **popular**;
402+
in general frequency,
403+
**unary** function calling is equal to or exceeded by
404+
those two cases **alone**
405+
let alone by other ubiquitous syntaxes
406+
such as **array literals**, **object literals**,
407+
and **arithmetic operations**.
408+
This explainer contains several [real-world examples][]
409+
of this difference in prevalence.
410+
411+
[real-world examples]: #real-world-examples
412+
413+
Furthermore, several other proposed **new syntaxes**,
414+
such as **[extension calling][]**,
355415
**[do expressions][]**,
356416
and **[record/tuple literals][]**,
357-
will also likely become common in the future.
358-
And **arithmetic** operations would become even more common
359-
if TC39 standardized **[operator overloading][]**.
360-
All of these syntaxes would be better accommodated by Hack pipes.
417+
will also likely become **pervasive** in the **future**.
418+
Likewise, **arithmetic** operations would also become **even more common**
419+
if TC39 standardizes **[operator overloading][]**.
420+
Untangling these future syntaxes’ expressions would be more fluent
421+
with Hack pipes compared to F# pipes.
361422

362-
[extension]: https://github.com/tc39/proposal-extensions/
423+
[extension calling]: https://github.com/tc39/proposal-extensions/
363424
[do expressions]: https://github.com/tc39/proposal-do-expressions/
364425
[record/tuple literals]: https://github.com/tc39/proposal-record-tuple/
365426
[operator overloading]: https://github.com/tc39/proposal-operator-overloading/
@@ -368,11 +429,12 @@ All of these syntaxes would be better accommodated by Hack pipes.
368429
The syntax tax of Hack pipes on unary function calls
369430
(i.e., the `(%)` to invoke the righthand side’s unary function)
370431
is **not a special case**:
371-
it’s just **writing ordinary code** in **the way we normally would** without a pipe.
432+
it simply is **explicitly writing ordinary code**,
433+
in **the way we normally would** without a pipe.
372434

373435
On the other hand, **F# pipes require** us to **distinguish**
374436
between “code that resolves to an unary function”
375-
versus **anything else**
437+
versus **any other expression**
376438
and to remember to add the arrow-function wrapper around the latter case.
377439

378440
For example, with Hack pipes, `value |> someFunction + 1`
@@ -390,19 +452,27 @@ The **topic reference** `%` is a **nullary operator**.
390452
It acts as a placeholder for a **topic value**,
391453
and it is **lexically scoped** and **immutable**.
392454

393-
(The precise [token for the topic reference is not final][token bikeshedding].
455+
<details>
456+
<summary><code>%</code> is not a final choice</summary>
457+
458+
(The precise [**token** for the topic reference is **not final**][token bikeshedding].
394459
`%` could instead be `#`, `@`, `?`, or many other tokens.
395-
We plan to [bikeshed what actual token to use][token bikeshedding]
396-
later, if TC39 advances this proposal.
397-
However, `%` seems to be the least syntactically problematic.
398-
It also resembles the placeholders of [printf format strings][].)
460+
We plan to [**bikeshed** what actual token to use][token bikeshedding]
461+
**later**, if TC39 advances this proposal.
462+
However, `%` seems to be the [least syntactically problematic][],
463+
and it also resembles the placeholders of **[printf format strings][]**
464+
and [**Clojure**’s `#(%)` **function literals**][Clojure function literals].)
399465

466+
[least syntactically problematic]: https://github.com/js-choi/proposal-hack-pipes/issues/2
467+
[Clojure function literals]: https://clojure.org/reference/reader#_dispatch
400468
[printf format strings]: https://en.wikipedia.org/wiki/Printf_format_string
401469

470+
</details>
471+
402472
The **pipe operator** `|>` is a bidirectionally **associative infix operator**
403473
that forms a **pipe expression** (also called a **pipeline**).
404474
It evaluates its lefthand side (the **pipe head** or **pipe input**),
405-
immutably **binds** the resulting value to the topic reference,
475+
immutably **binds** the resulting value (the **topic value**) to the **topic reference**,
406476
then evaluates its righthand side (the **pipe body**) with that binding.
407477
The resulting value of the righthand side
408478
becomes the whole pipe expression’s **final value** or **pipe output**.
@@ -418,13 +488,14 @@ For example, `v => v |> % == null |> foo(%, 0)`\
418488
would group into `v => (v |> (% == null) |> foo(%, 0))`,\
419489
which in turn is equivalent to `v => foo(v == null, 0)`.
420490

421-
A pipe body **must** use its topic reference at least once.
491+
A pipe body **must** use its topic value **at least once**.
422492
For example, `value |> foo + 1` is **invalid syntax**,
423493
because its body does not contain a topic reference.
424-
This design is because omission of the topic reference from a pipe expression’s body
425-
is almost certainly an accidental programmer error.
494+
This design is because **omission** of the topic reference
495+
from a pipe expression’s body
496+
is almost certainly an **accidental** programmer error.
426497

427-
Likewise, a topic reference **must** be in a pipe body.
498+
Likewise, a topic reference **must** be contained in a pipe body.
428499
Using a topic reference outside of a pipe body
429500
is also **invalid syntax**.
430501

@@ -436,10 +507,11 @@ when the `eval` expression is evaluated at runtime.
436507

437508
There are **no other special rules**.
438509

439-
If we need to interpose a **side effect**
510+
A natural result of these rules is that,
511+
if we need to interpose a **side effect**
440512
in the middle of a chain of pipe expressions,
441513
without modifying the data being piped through,
442-
we could use a **comma expression**,
514+
then we could use a **comma expression**,
443515
such as with `value |> (sideEffect(), %)`.
444516
As usual, the comma expression will evaluate to its righthand side `%`,
445517
essentially passing through the topic value without modifying it.
@@ -779,10 +851,8 @@ return context
779851
If Hack pipes are added to JavaScript,
780852
then they could also elegantly handle
781853
**partial function application** in the future
782-
with a syntax inspired by
783-
[Clojure’s `#(+ %1 %2)` function literals][Clojure function literals].
784-
785-
[Clojure function literals]: https://clojure.org/reference/reader#_dispatch
854+
with a syntax further inspired by
855+
[Clojure’s `#(%1 %2)` function literals][Clojure function literals].
786856

787857
There is **already** a [proposed special syntax
788858
for partial function application (PFA) with `?` placeholders][PFA]

0 commit comments

Comments
 (0)