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

Draft a version that uses braces to distinguish pipe styles #31

Closed
js-choi opened this issue Feb 1, 2019 · 5 comments
Closed

Draft a version that uses braces to distinguish pipe styles #31

js-choi opened this issue Feb 1, 2019 · 5 comments
Assignees
Labels
explainer/readme About readme.md

Comments

@js-choi
Copy link
Collaborator

js-choi commented Feb 1, 2019

@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 Where does the F# and Smart proposal conflict? Let's split them up into two proposals, one predicate on the other. 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 Consider Elixir-style pipeline as one of the blessed alternatives 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).

@js-choi js-choi added the explainer/readme About readme.md label Feb 1, 2019
@js-choi js-choi self-assigned this Feb 1, 2019
@js-choi js-choi changed the title Draft version that uses braces to distinguish pipe styles Draft a version that uses braces to distinguish pipe styles Feb 1, 2019
@mAAdhaTTah
Copy link

Phase 1:

x |>await o.m(a)

would be invalid. F# pipeline uses x |> await, which I've been calling "bare await".

@js-choi
Copy link
Collaborator Author

js-choi commented Feb 4, 2019

Ah yeah, whoops, forgot about that. I’ll have to tweak those examples. Thanks!

@mAAdhaTTah
Copy link

@js-choi Does that materially impact our ability to make the "brace-style smart pipe" a superset of F# pipes?

@js-choi
Copy link
Collaborator Author

js-choi commented Feb 4, 2019

It does not affect phase two’s backward compatibility with phase one (F# pipes); it still works. I made corrections in the original post.

It only, as you said elsewhere, raises the question of whether x|>await is actually necessary versus x|>{await %}. I would make the case that it might be a reasonable shortcut to introduce from the beginning in the phase-one proposal, due to the special non-function-composable nature of await (and yield). In fact I was going to file an issue on your repository suggesting supporting also x|>yield, since they clearly form a unique pair.

@Sceat
Copy link

Sceat commented Jun 1, 2019

yea hope the do proposal will pass or anyway there is no problems with

1 |> ( console.log(#), #+1 ) |> foo

const through = a => b => (a(), b)
1 |> through(()=> {
	//
})(#) |> foo

@js-choi js-choi closed this as completed Mar 8, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
explainer/readme About readme.md
Projects
None yet
Development

No branches or pull requests

3 participants