@@ -21,17 +21,18 @@ This [choice of token is not a final decision][token bikeshedding];
21
21
In the State of JS 2020 survey, the ** fourth top answer** to
22
22
[ “What do you feel is currently missing from
23
23
JavaScript?”] ( https://2020.stateofjs.com/en-US/opinions/?missing_from_js )
24
- was the ** pipe operator** .
24
+ was a ** pipe operator** . Why?
25
25
26
26
When we perform ** consecutive operations** (e.g., function calls)
27
27
on a ** value** in JavaScript,
28
- there are currently two fundamental ways to do so :
28
+ there are currently two fundamental styles :
29
29
* passing the value as an argument to the operation
30
30
(** nesting** the operations if there are multiple operations),
31
31
* or calling the function as a method on the value
32
32
(** chaining** more method calls if there are multiple methods).
33
33
34
34
That is, ` three(two(one(value))) ` versus ` value.one().two().three() ` .
35
+ However, these styles differ much in readability, fluency, and applicability.
35
36
36
37
### Deep nesting is hard to read
37
38
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:
84
85
5 . ` chalk.dim() ` (left side), then
85
86
6 . ` console.log() ` (left side).
86
87
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
+
87
95
</details >
88
96
89
97
### Method chaining is limited
90
- The second style, ** chaining** , is ** only** usable
98
+ The second style, ** method chaining** , is ** only** usable
91
99
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.
94
103
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.
97
106
And editing the code later to ** insert or delete** more method calls is trivial,
98
107
since we would just have to put our cursor in one spot,
99
108
then start typing or deleting one ** contiguous** run of characters.
100
109
101
110
Indeed, the benefits of method chaining are ** so attractive**
102
111
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.
106
115
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** .
108
117
There is even a name for this style of programming:
109
- [ fluent interfaces] [ ] .
118
+ ** [ fluent interfaces] [ ] ** .
110
119
111
120
[ jQuery ] : https://jquery.com/
112
121
[ fluent interfaces ] : https://en.wikipedia.org/wiki/Fluent_interface
@@ -218,22 +227,30 @@ It is often simply too **tedious and wordy** to **write**
218
227
code with a long sequence of temporary, single-use variables.
219
228
It is arguably even tedious and visually noisy for a human to ** read** , too.
220
229
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 ] ,
222
231
then programmers will ** inevitably avoid naming** variables
223
232
when they perceive their benefit to be relatively small.
224
233
225
234
[ naming hard ] : https://martinfowler.com/bliki/TwoHardThings.html
226
235
227
236
## Why the Hack pipe operator
228
237
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 ` |> ` .
231
238
(There ** was** a [ third proposal for a “smart mix” of the first two proposals] [ smart mix ] ,
232
239
but it has been withdrawn,
233
240
since its syntax is strictly a superset of one of the proposals’.)
234
241
235
242
[ smart mix ] : https://github.com/js-choi/proposal-smart-pipelines/
236
243
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
+
237
254
### This proposal: Hack pipes
238
255
In the ** Hack language** ’s pipe syntax,
239
256
the righthand side of the pipe is an ** expression** containing a special ** placeholder** ,
@@ -245,25 +262,38 @@ to pipe `value` through the three functions.
245
262
and the placeholder can go anywhere any normal variable identifier could go,
246
263
so we can pipe to any code we want ** without any special rules** :
247
264
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,
252
268
* ` value |> % + 1 ` for arithmetic,
269
+ * ` value |> [%, 0] ` for array literals,
270
+ * ` value |> {foo: %} ` for object literals,
271
+ * `` value |> `${%}` `` for template literals,
253
272
* ` value |> new Foo(%) ` for constructing objects,
254
273
* ` value |> await % ` for awaiting promises,
274
+ * ` value |> (yield %) ` for yielding generator values,
255
275
* ` value |> import(%) ` for calling function-like keywords,
256
276
* etc.
257
277
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
262
290
263
291
### Alternative proposal: F# pipes
264
292
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** .
267
297
That is, we write ` value |> one |> two |> three ` to pipe ` value `
268
298
through the three functions.
269
299
` left |> right ` becomes ` right(left) ` .
@@ -295,71 +325,102 @@ envars
295
325
``` js
296
326
envars
297
327
| > Object .keys
298
- | > x => x .map (envar =>
328
+ | > x => x .map (envar =>
299
329
` ${ envar} =${ envars[envar]} ` ,
300
330
)
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 (' ' ))
304
334
| > console .log ;
305
335
```
306
336
307
337
</details >
308
338
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
310
341
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 ` (%) ` ).
312
353
313
354
** Con:** The restriction means that ** any operations**
314
355
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.
334
381
335
382
[ enhanced F# pipes ] : https://github.com/valtech-nyc/proposal-fsharp-pipelines/
336
383
337
- ### Hack pipes favor more- common use cases
384
+ ### Hack pipes favor more common expressions
338
385
** 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] [ ] ** ,
355
415
** [ do expressions] [ ] ** ,
356
416
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.
361
422
362
- [ extension ] : https://github.com/tc39/proposal-extensions/
423
+ [ extension calling ] : https://github.com/tc39/proposal-extensions/
363
424
[ do expressions ] : https://github.com/tc39/proposal-do-expressions/
364
425
[ record/tuple literals ] : https://github.com/tc39/proposal-record-tuple/
365
426
[ operator overloading ] : https://github.com/tc39/proposal-operator-overloading/
@@ -368,11 +429,12 @@ All of these syntaxes would be better accommodated by Hack pipes.
368
429
The syntax tax of Hack pipes on unary function calls
369
430
(i.e., the ` (%) ` to invoke the righthand side’s unary function)
370
431
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.
372
434
373
435
On the other hand, ** F# pipes require** us to ** distinguish**
374
436
between “code that resolves to an unary function”
375
- versus ** “anything else ”** –
437
+ versus ** “any other expression ”** –
376
438
and to remember to add the arrow-function wrapper around the latter case.
377
439
378
440
For example, with Hack pipes, ` value |> someFunction + 1 `
@@ -390,19 +452,27 @@ The **topic reference** `%` is a **nullary operator**.
390
452
It acts as a placeholder for a ** topic value** ,
391
453
and it is ** lexically scoped** and ** immutable** .
392
454
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 ] .
394
459
` % ` 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 ] .)
399
465
466
+ [ least syntactically problematic ] : https://github.com/js-choi/proposal-hack-pipes/issues/2
467
+ [ Clojure function literals ] : https://clojure.org/reference/reader#_dispatch
400
468
[ printf format strings ] : https://en.wikipedia.org/wiki/Printf_format_string
401
469
470
+ </details >
471
+
402
472
The ** pipe operator** ` |> ` is a bidirectionally ** associative infix operator**
403
473
that forms a ** pipe expression** (also called a ** pipeline** ).
404
474
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** ,
406
476
then evaluates its righthand side (the ** pipe body** ) with that binding.
407
477
The resulting value of the righthand side
408
478
becomes the whole pipe expression’s ** final value** or ** pipe output** .
@@ -418,13 +488,14 @@ For example, `v => v |> % == null |> foo(%, 0)`\
418
488
would group into ` v => (v |> (% == null) |> foo(%, 0)) ` ,\
419
489
which in turn is equivalent to ` v => foo(v == null, 0) ` .
420
490
421
- A pipe body ** must** use its topic reference at least once.
491
+ A pipe body ** must** use its topic value ** at least once** .
422
492
For example, ` value |> foo + 1 ` is ** invalid syntax** ,
423
493
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.
426
497
427
- Likewise, a topic reference ** must** be in a pipe body.
498
+ Likewise, a topic reference ** must** be contained in a pipe body.
428
499
Using a topic reference outside of a pipe body
429
500
is also ** invalid syntax** .
430
501
@@ -436,10 +507,11 @@ when the `eval` expression is evaluated at runtime.
436
507
437
508
There are ** no other special rules** .
438
509
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**
440
512
in the middle of a chain of pipe expressions,
441
513
without modifying the data being piped through,
442
- we could use a ** comma expression** ,
514
+ then we could use a ** comma expression** ,
443
515
such as with ` value |> (sideEffect(), %) ` .
444
516
As usual, the comma expression will evaluate to its righthand side ` % ` ,
445
517
essentially passing through the topic value without modifying it.
@@ -779,10 +851,8 @@ return context
779
851
If Hack pipes are added to JavaScript,
780
852
then they could also elegantly handle
781
853
** 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 ] .
786
856
787
857
There is ** already** a [ proposed special syntax
788
858
for partial function application (PFA) with ` ? ` placeholders] [ PFA ]
0 commit comments