Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Named placeholders #203

Open
shuckster opened this issue Sep 11, 2021 · 38 comments
Open

Named placeholders #203

shuckster opened this issue Sep 11, 2021 · 38 comments
Labels
follow-on proposal Discussion about a future follow-on proposal

Comments

@shuckster
Copy link

Just wondering if it wouldn't guide our intuition a little better if we could gives names to placeholders in the same manner as arrow-function arguments?

envars
     x |> Object.keys(x)
  keys |> keys.map(x => `${x}=${envars[x]}`)
   arr |> arr.join(' ')
   str |> `$ ${str}`
  line |> chalk.dim(line, 'node', args.join(' '))
   out |> console.log(out);

No need to choose between ^ or %, no conflicts with mod/pow, and has a better chance of producing self-documenting code.

What are the drawbacks to this approach?

@samhh
Copy link

samhh commented Sep 11, 2021

It now looks quite similar to the F# proposal except it disallows point-free in exchange for dropping an arrow:

envars
  |> x => Object.keys(x)
  |> keys => keys.map(x => `${x}=${envars[x]}`)
  |> arr => arr.join(' ')
  |> str => `$ ${str}`
  |> line => chalk.dim(line, 'node', args.join(' '))
  |> out => console.log(out)

Where for clarity point-free means something more like this, which would've been deemed more idiomatic with that proposal:

envars
  |> keys
  |> map(x => `${x}=${envars[x]}`)
  |> join(' ')
  |> prefix('$ ')
  |> chalk.dim('node')(args.join(' '))
  |> console.log

If there is actually any chance of an F#-style operator following the blessed Hack one then I think it covers this use case without introducing more special syntax.

@Avaq
Copy link

Avaq commented Sep 11, 2021

Related comments:

The named placeholder idea was also proposed here: #91 (comment)
With a similar response here: #91 (comment)

@kiprasmel

This comment has been minimized.

@runarberg

This comment has been minimized.

@kiprasmel

This comment has been minimized.

@mAAdhaTTah

This comment has been minimized.

@shuckster

This comment has been minimized.

@kiprasmel

This comment has been minimized.

@aadamsx

This comment has been minimized.

@mAAdhaTTah

This comment has been minimized.

@kiprasmel

This comment has been minimized.

@mAAdhaTTah

This comment has been minimized.

@aadamsx

This comment has been minimized.

@mAAdhaTTah

This comment has been minimized.

@runarberg

This comment has been minimized.

@tabatkins
Copy link
Collaborator

I'm disappointed that I've had to once again mark a large chunk of an issue thread as off-topic. The OP and the first few comments presented a reasonable issue and discussion; this is not a place to diverge into yet another discussion of F#-vs-Hack or baseless speculation on committee proceedings. There are dedicated threads for that.

@shuckster
Copy link
Author

@tabatkins - Apologies for my part in that. I've since caught up in much of the discussion and regret getting caught-up in the politics of it.

@tabatkins
Copy link
Collaborator

I'm not certain the syntax suggested in the OP is actually viable; there might be parsing ambiguities with the preceding line.

Putting that to the side, tho, this runs into some of the arguments against temp variables from the README. Definitely not all, since the bindings are still scoped to a single pipe step, which avoids several issues, but the "naming is hard" part still applies.

Most of the time, you don't need to give a name to the topic; it's clear from context, and the topic value doesn't represent a semantically significant unit in your project. (See the README again, where several of the names are just restatements of the operation happening on the line, rather than actually meaningful names.) So I expect most of these lines would, in practice, just use the name x or something, whatever the user typically uses for temp var names. I'd prefer we just use a chosen standard name in those cases, to communicate the semantic that the value is just flowing from the previous line and isn't significant on its own.

That said, I'm not opposed to naming the placeholder sometimes, when it helps. It would just need a good syntax that doesn't cause issues.

@kiprasmel

This comment has been minimized.

@shuckster

This comment has been minimized.

@shuckster
Copy link
Author

shuckster commented Sep 13, 2021

Thank you for the reply @tabatkins . My initial crack at placeholder naming is as follows:

envars
  |> Object.keys(^)
  |> ^keys.map(x => `${x}=${envars[x]}`)
  |> ^arr.join(' ')
  |> `$ ${^}`
  |> chalk.dim(^line, 'node', args.join(' '))
  |> console.log(^chalked);

Essentially, everything following the token is working like an inline comment up until the next consumable character.

Janky, but that's all I got at the moment. I do appreciate the arguments against temp-variables, but I'd like to think the Hack token is just about comparable to the i in the uncountable for-loops of times gone by. But still today we have a chance of renaming them!

@lightmare
Copy link

It took me a while to realize that I suggested the exact same thing in another topic a few days after @shuckster, apologies for not giving proper credit, I must've tricked myself into thinking "iOriginal".

Although I like the OP syntax the most (out of all the named-placeholder options I've seen), I share @tabatkins concern regarding grammar. I'm not sure how exactly it'd play out, at first I thought it's fairly simple: Expr Identifier |> Expr. But it seems like it would require more lookahead for any expression followed by a newline and an identifier.
Currently, when the parser reads Expr \n Identifier, it inserts a semicolon at the newline. But if that could be the left-hand-side of a pipeline, it would need to check the next token as well.

So it might be safer, though not as pretty, to place the identifier after the operator: Expr |> Identifier Expr.

Exploring further, here's something crazy:

In this variant you could replace the Identifier with (BindingIdentifier | BindingPattern) — since the context is clear from the preceding |> you can have destructuring right there.
And then, if we disallow newline between the binding and the pipe body, we could allow a pipe without that binding to behave differently...

// Expr |> BindingIdentifier [noLineBreak] Expr
// single-binding Hack behaviour
obj |> _ Object.keys(_) |> _ log(_.join(", "));

// Expr |> BindingPattern [noLineBreak] Expr
// destructuring Hack behaviour
/(?<k>\w+)=(?<v>.*)/.exec(input)
  |> { groups: { k, v }} (options[k] = v);

// Expr |> Expr
// F# behaviour
arg |> foo |> (x => bar(x, 2));
// yes I would absolutely require parentheses around arrow functions
// to avoid all kinds of ambiguities

@js-choi js-choi added the follow-on proposal Discussion about a future follow-on proposal label Sep 19, 2021
@lightmare
Copy link

lightmare commented Oct 5, 2021

@js-choi I just noticed you added the follow-on-proposal label here. May I suggest considering named placeholder an alternative baseline instead?

If we started with grammar such as: Expr |> BindingIdentifier [noLineBreak] Expr
The topic would be user-defined identifier, so you could introduce the pipe operator without a new topic token.

Expr is a placeholder for whatever the precedence would dictate. I would give the pipe the highest precedence among binary operators — putting a writer-tax on binary expressions and yield in pipeline. But that's a separate discussion.

Then if need arises you could allow destructuring the topic in place in a follow-up: Expr |> BindingPattern [noLineBreak] Expr

And when bikeshedding the topic token finally concludes, you could enable: Expr |> Expr without explicit topic binding. Or if implicit topic becomes unnecessary, reserve that for F# style. (edit: this has serious potential for human-reader confusion, so probably not viable; but Expr |> PFA might be viable)

@aikeru
Copy link

aikeru commented Oct 5, 2021

I did a search of this repository for the word "with" and didn't see this idea, which I take as an imperfect hint that this hasn't been suggested in the repo. Just thought, since with use is discouraged, maybe something like this could work...

with name |> expr(name)

such as...

// A single name
const shoutedName = with userName |> capitalize(userName) |> amplifyVolume(userName)

// multiple names??
const shoutedName = with userName |> capitalize(userName)
  with capitalName |> amplifyVolume(capitalName)

// or perhaps it only names the next one?
const shoutedName = with userName |> capitalize(userName) |> amplifyVolume(^)

This would seem to be unambiguous to the untrained eye, since old-style with requires parens with(expr).
It also seems coincidentally teachable since "with x" and "const x" are similar.

@ljharb
Copy link
Member

ljharb commented Oct 5, 2021

@aikeru none of those pipelines seem to have an initial value.

@aikeru
Copy link

aikeru commented Oct 5, 2021

@aikeru none of those pipelines seem to have an initial value.

Ah, ha! Perhaps that foils this idea, then, but maybe something like this?

const result = unNamed with named |> make(named)

...or not. Thanks anyway!

@js-choi
Copy link
Collaborator

js-choi commented Oct 6, 2021

I just noticed you added the follow-on-proposal label here. May I suggest considering named placeholder an alternative baseline instead?

Whether to make declaring an identifier at each pipe step required in the initial proposal…

It’s not only up to me, of course.

But I’m personally lukewarm towards this. It seems pretty verbose, since it’d be required in each step.

By the time we’re naming variables, we might as well be just using variable assignment, right? A lot of the purpose of this proposal is to Not Name the Thing When You Don’t Want to.

Anyways, sorry if that response is disappointing. Hopefully it’s understandable. Also, it’s not only up to me—there are other champions—but I also suspect that it would be unpopular with the greater Committee…

@lightmare
Copy link

By the time we’re naming variables, we might as well be just using variable assignment, right?

No. Because apples-to-apples equivalent would use const which you can't re-assign. Also variables don't go out of scope afterwards.

The difference is who gets to name the variable: the committee, or the user.

@tabatkins
Copy link
Collaborator

Yeah, baking this in as a requirement for each pipeline step is a no-go. The existing piping syntaxes in JS (method chaining, userland pipe()) don't name the topic variable being passed between each stage, and that still makes for perfectly reasonable and readable code. We don't need to require additional verbosity at each stage for this piping syntax.

@lightmare
Copy link

The existing piping syntaxes in JS (method chaining, userland pipe()) don't name the topic variable being passed between each stage, and that still makes for perfectly reasonable and readable code.

Not sure how that's relevant, as this piping syntax does name the topic.

We don't need to require additional verbosity at each stage for this piping syntax.

That's assuming a single-character topic token. Otherwise there's no additional verbosity required.

@js-choi
Copy link
Collaborator

js-choi commented Oct 6, 2021

No. Because apples-to-apples equivalent would use const which you can't re-assign.

Yeah, sorry, by “variable assignment” I meant “constant assignment”. By the time we’re naming constants, we might as well be just using constant assignment, right?

Also variables don't go out of scope afterwards.

Well, right now Hack pipes are associative. (a |> f(^)) |> g(^) and a |> (f(^) |> g(^)) are equivalent.

Having f() |> a g(a) |> b h(a, b) throw a ReferenceError would impose left associativity on |> and get rid of its right associativity. I personally would expect f() |> a g(a) |> b h(a, b) to work.

In fact, my own mental model of |> is right associative, which matches that of nested monadic-bind callbacks: how (a => (b => h(a, b))(g(a)))(f()) works. In fact, I had brainstormed a whole big draft proposal for named topics with monadic modifiers based on that intuition, and that proposal would have imposed right associativity. (I scrapped it in favor of exploring F#-style computation expressions and/or algebraic effects.)

In any case, I also suspect that it would be unpopular with the greater Committee, but yeah.

@lightmare
Copy link

Having f() |> a g(a) |> c h(a, b) throw a ReferenceError would impose left associativity on |> and get rid of its right associativity. I personally would expect f() |> a g(a) |> b h(a, b) to work.

I didn't mean to imply ReferenceError in that case.

Whether f() |> a g(a) |> b h(a, b) would work as you expect, or would throw ReferenceError, could be another discussion. I tend to agree with your expectation — if someone used different topic names, they probably wanted to use them together at some point, so it makes more sense to impose right-associativity.

I meant that if you used const a = ..., b = ...; those are in scope till the end of block, meaning not only that you cannot re-purpose those names later in the block (which is one annoying difference from topic binding), but also that you can use those values arbitrarily far away (another annoying difference).

@runarberg

This comment has been minimized.

@jithujoshyjy
Copy link

I would like to propose a syntax for the named placeholder idea;

const superhero = [] as powers
    |> grantPower("nightvision", powers)
    |> make("invincible", powers)
    |> powers.map(x => x.toUpperCase())

We've been discussing about it at TC39 Discourse Group
More details mentioned there☝

@ljharb
Copy link
Member

ljharb commented Nov 7, 2021

I’m unclear on the argument for naming the token that doesn’t allow you to name it in each step.

@ghost
Copy link

ghost commented Jan 31, 2022

Not sure if this has been said before but 2 things about this proposal:

  • Nesting needs the ability to name the argument:

    data
    |> join(data2 |> select(^)) // what is ^?

  • It will allow destructuring in the future

    data
    {prop1, prop2} |> f(prop1,prop2)

Which is neat.

Anyways, just my 2 cents.

@shuckster
Copy link
Author

@edeboursetty I'm not sure the current proposal permits a |> without a balancing ^, so your first example would be a syntax error. I'm not sure I fully understand the second example - there don't appear to be any placeholder ^ tokens?

@theScottyJam
Copy link

theScottyJam commented Mar 1, 2022

One option would be to allow us to provide a name after the pipeline token. For example, say we go with %% as the hack-pipe token. Then, if you want to provide a name, just stick it right after the token, like this %%user.

As an example:

id
  |> getUserById(%%)
  |> getGroupFromUser(%%user)
  |> doOperation(%%group) + anotherOperation(%%group)

// And this would be a syntax error
// because you can't use different names within the same pipeline step
data |> doOperation(%%xyz) + anotherOperation(%%abc)

This would require us picking a hack-pipe token that can actually be used in this way (i.e. using a binary operator, such as ^ wouldn't work, because x ^y would cause ambiguity)

This solution also doesn't fix the nested pipeline issue, but what I care about more is the ability to name the token (to help self-document it), then the ability to nest pipelines in a more readable way.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
follow-on proposal Discussion about a future follow-on proposal
Projects
None yet
Development

No branches or pull requests