import {map, join} from 'lodash/fp' //iterators have better performance
envars
|> Object.entries
|> map(([key, val]) => `${key}=${val}`)
|> join(' ')
|> x => chalk.dim('$ ' + x, 'node', join(' ', args))
|> console.log
Even without lodash, it's still easy to read. envars
|> Object.entries
|> x => x.map(([key, val]) => `${key}=${val}`)
|> x => x.join(' ')
|> x => chalk.dim('$ ' + x, 'node', join(' ', args))
|> console.log
For sake of completeness, here's the hack-based variant envars
|> Object.entries(%)
|> %.map(([key, val]) => `${key}=${val}`)
|> %.join(' ')
|> chalk.dim('$ ' + %, 'node', join(' ', args))
|> console.log(%)(the requirement being that join(' ') returns a function that takes one arg)
The middle example would be using native stuff which is why you have the wrapper function (kinda like you'd have a function wrapping a callback)
Just replace this:
x |> f
With this: f(x)
For the join example, you must do this: xs
|> x => x.join(' ')
It de-sugars to: (x => x.join(' '))(xs)
... which of course is simply: xs.join(' ')Does join(a)(b) mean join(a, b) or join(b, a)? Does it mean a.join(b) or b.join(a)? And is a or b the delimiter? I don't really feel confident without looking it up or trying it out. I have a similar problem with Haskell's function syntax a -> a -> a.
If the function was called makeJoinerBy(sep)(array), it would be somewhat clearer:
join = makeJoinerBy(' ')
result = join(array)That’s highly subjective I’m afraid.
“Take entries of envvars, turn that into k=v, join that by a space, make dim $, previous that, and args, log that”.
Personally I have no clue what’s going on at first glance with all these %%% “thats”. It’s meant to be declarative, but reads imperatively instead.
console.log(chalk.dim(
'$',
...Object.keys(envars)
.map(k => `${k}=${envars[k]}`),
'node',
...args,
))
“Log dimmed $, k=v pairs of envvars, node, then args”.With pipes you can visually follow the manipulations and function calls in the order that they happen instead of being forced to scan the code inside-out & outside-in, matching parentheses and function call parameters in your head, while still visualizing intermediate results to get 1 final return value.
I find Elixir code much easier and quicker to understand, in large part thanks to its (admittedly, imperfect) pipe syntax. Code written in this way is also much easier to debug because you can quickly add `console.log`, breakpoints, or equivalent between the pipes.
I find this unnecessarily time-consuming and difficult to parse and I'd likely raise some flags in a code review:
console.log(chalk.dim(
`$ ${Object.keys(envars)
.map(envar => `${envar}=${envars[envar]}`)
.join(' ')
}`,
'node',
args.join(' ')
));
Without pipe syntax, I'd refactor this to: const envStr = Object.keys(envars)
.map(envar => `${envar}=${envars[envar]}`)
.join(' ');
const styled = chalk.dim(`$ ${envStr}`, 'node', args.join(' '));
console.log(styled);
But you often find yourself having to add additional logic, e.g. to scrub sensitive values, so it would probably end up closer to: const sensitiveEnv = [...];
const envStr = Object.keys(envars)
.filter(envar => !sensitiveEnv.includes(envar))
.map(envar => `${envar}=${envars[envar]}`)
.join(' ');
const styled = chalk.dim(`$ ${envStr}`, 'node', args.join(' '));
console.log(styled);personally i detest pointfree syntax. having intermediate values makes it much easier to step through code with a debugger & see what is happening. and it gives the reader some name for what the thing is, which is incredibly useful context. the enablement of pointsfree styles is one of my main concerns about potential pipe operator syntaxes: the various syntaxes that have been raised often introduce implicit variables which are passed, and i greatly fear the loss of clarity pointsfree style brings.
maybe there's something beyond the pointsfree vs not debate here that i'm missing, that makes you dislike the refactored example. personally i greatly enjoy the flatness, the step by step production of intermediate values, each of which can be clearly seen, and then assembled in a last final clear step. that is much more legible to me than one complex expression.
(1) named intermediate values are sometimes more readable ... though I have examples where it's very hard to come up with names and not sure it helped
(2) debugging is easier.
For (2) though, this IMO is a problem with the debugger. The debugger should allow stepping by statement/expression instead of only by line (or whatever it's currently doing). If the debugger stopped at each pipe and showed in values (2) would mostly be solved. I used a debugger that worked by statements instead of lines once 34 years ago. Sadly I haven't seen once since. It should be optional though as it's a tradeoff. Stepping through some code can get really tedious if there are lots of steps.
I have tools that can do it, but I'll still have a better time splitting out a variable for it, especially since what I really want is a log of all the intermediate values, so I can replicate what it's doing on paper
In languages that more easily support repl-driven development (e.g. Clojure), I think this is less of an issue. If you have a handful of pure functions, you can quickly and easily execute them via the repl, so you get a lot of clarity as to what those intermediate values look like even if the functions are ultimately used in a more point-free style.
But on the other hand, this would be a nightmare in C# (what I use in my day job). Sure, you can execute arbitrary expressions while debugging C#, but IMO you can't really achieve the same clarity. I'd rather see intermediate values like you suggest since it's easier while debugging, vs a bunch of nested function calls.
"styled" seems entirely pointless here.
This is why I'm happy to see the pipe syntax proposal, it avoids unnecessary temporary variables while simultaneously aiding readability.
The thing is I don't think it's all that much more readable. No matter which syntax you use, there's still the same number of "things" going on in a single statement.
I do have to admit I never worked with a language that uses |>, so I'm sure that with increased familiarly with this it would become "more readable" to me, but one has to wonder: just how many calling syntaxes does one language have to support? More syntax also means more potential for confusion, more ways to abuse the language/feature, more "individual programming styles", more argueing over "should we write it like this or that?", more overhead in deciding how to write something, harder to implement the language and write tooling for it, and things like that.
There is always a trade-off involved. The question to ask isn't "would this be helpful in some scenarios?" because the answer to that is always "yes" for practically any language feature. The question to ask "is this useful enough to warrant the downsides of extra syntax?" I'm not so sure that it is, as it doesn't really allow me to do anything new that I couldn't do before as far as I can see. It just allows me to make things that are already too complex a bit more readable (arguably).
Have you never worked with Bash? It's basically the same thing
One habit introduced to my current team by a former co-worker involves having even more intermediate keys than that:
const sensitiveEnv = [...];
const envKeys = Object.keys(envars)
const safeKeys = envKeys.filter(envar => !sensitiveEnv.includes(envar));
const safeEnv = safeKeys.map(envar => `${envar}=${envars[envar]}`).join(' ');
const styled = chalk.dim(`$ ${envStr}`, 'node', args.join(' '));
console.log(styled);
In the beginning I wasn't a fan of it, as I do a lot of Haskell, and it's obviously unnecessary. But with time I learned to appreciate this kind of function for its simplicity and consistence.Now, of course, with a pipe operator (or other similar constructions) you can get the consistency without the intermediate names.
I'm also one of those people that likes single-letter variables. I know some people hate it with a passion, but I find it very convenient. Just makes it easier to read as there's less to read.
I'm not smart enough to do Haskell, so I can't say much about that.
I agree with your remarks, there's no right or wrong, it's like tabs and spaces.
Btw I'm also not smart enough for Haskell, but it hasn't stopped me so far ;)
Given that the OP stated that they like the pipe syntax, and the refactored version illustrates the hoops you'd have to jump through without it, I guess your comment is just a strange way to agree with the OP?
const envs = Object.entries(envars).map(entry => entry.join('='));
console.log(chalk.dim('$', ...envs, 'node', ...args));
I don't really have a point to make, perhaps just that there's often simplifications possible with extra creativity.It makes me really question the judgement of the people that are working on this language, and it explains how some of the shittier proposals manage to slip in and why the good ones are misused all over the place in every real world codebase when this is the kind of guidance devs have on where to use fancy new features.
In a few years everyone is going to collectively lose their fucking mind once again and decide that ternaries are now the devil and every React component should be chock full of do expressions. I can see that coming clear as day. That might finally be enough to get me to throw in the towel if the also incoming decorator hell doesn't do it.
This example was literally taken directly from real-world code in the React codebase (a script to call jest-cli). While you're correct that there are probably a lot of clearer and more readable ways to write this snippet, the fact of the matter remains that people write code exactly like these "real world examples" all of the time.
Only now?
I was an early adopter of the original decorators proposal as well as the F#-style pipeline operators. The more time has moved on and other bits made it into JS proper, I'm far more inclined to stick to what's "in the box"... Have even considered just writing straight JS + ESM (modules). Of course, I also like JSX, though not sure of any proposals of how to get that "in the box" as e4x died on the vine and a lot of other efforts didn't gain much traction either.
Examples?
If the motivation behind these features are React components, javascript clearly lacks proper logic in array (and object) literals, like
'[' if (cond) […]expr1 [else […]expr2] ']'
Although it seems pretty strange to add syntax only because some specific user interface library together with a specific language extension could benefit from it. [cond ? expr1 : expr2]
Or maybe this: [(() => {/*code*/})()] [...(cond ? [expr1] : [])]I mean it looks exactly like JS with an additional % placeholder.
- we already have operators on values, such as +
- |> is just another operator on values
Use |> for the Hack proposal
and -> for F# -style.
const slowSum = (lst: {x: number}[]) =>
switch(list) {
[{x}, ...y] -> x + slowSum(y) //not tail recursive
[{x}, {x: y}] -> x + y //alias second x to y
[{x}] -> x //handle length 1
[] -> 0 //handle length 0
}It's incredible to me TC39 would rather have this monstruosity over F# pipes which are actually pretty similar to how most pipes work and read in most functional languages including unix `|` one.
For me the piped version seems way better, way cleaner, with cleanly separated processing steps.
I don't like nested steps because for nesting multiple arguments functions your expression tends to grow in both directions and parts belonging to the same step tend to end up really far from one another.
% is short so it keeps parts of the same processing step together.
Then again, this is my typical opinion to a lot of the proposals - I see what this is useful for but I haven't experienced enough pain to really argue for it. However, if it does make it into the spec then I will use it probably because it's there.
I also recognize my stance as one that not many people like in other contexts because it can be interpreted as me not looking to improve my situation. But on the flip side I think cluttering the language specification with a lot of superficial syntactic sugar is a mistake.
Very much this. They need a new question on the survey "does JavaScript need new syntax or can we just leave it alone and work on perf/tooling/etc. of the existing stuff without throwing a bunch of new junk in"?
I also have a feeling this doc lists every sort of usecase, not because they are advertising it as "always the better version", but because it's a design doc that needs to factor in edge cases, such as using a pipe following chained function calls.
You cherry picked one example of a tangled/messy block of code that they used to communicate specific idea around "left to right" comprehension and flow of the data using the new syntax. For that specifically it did a fine job.
But that doesn't mean that's the way you should be writing code in the first place, given it started with a mess and only used one piece of syntax to change it.
I will admit it's a poor example to open with. But for a design doc about exploring and debating ideas it's fine.
I don't think it's fair to say it's "cherry picking" to focus on the example they focus on themselves. They have a few other examples too, but I would say "I don't see how [NEW] is better than [OLD]" for many of those as well (and for some, I think the [NEW] is significantly worse).
Plus the doc is #1 on HN after all, which could IRL push it beyond a proposal stage if done right.
On the other hand, pipe is less useful when you don’t have partial application of function built-in. So yeah I think this is not worth it.
Ironically, OO gets thrown under the bus a lot lately because it ain’t functional. But in reality, early OO languages like Smalltalk and CLOS were much more functional in this regard, you always had an implicit return of self, which could be chained easily.
I (over)use the piping operator (|>) in Elixir a lot. I just like the way it reads. But one thing I don’t love, is that it’s not an easy sequence to type.
I personally find pipe style much more readable.
If i read a code and i want to now what it will do, i wanna first read that it will output something. If that is what i search for, i read the nested code.
With the pipe style i waste more time, even if the code looks clearer.
I not overused example could look like this:
console.log(
Object.keys(envars)
.map(envar => `${envar}=${envars[envar]}`)
.join(' ')
|> `$ ${%}`
|> chalk.dim(%, 'node', args.join(' '))
)
If i don't care about console.log, i don't have to read the nested code. chalk.dim(Object.keys(envars)
.map(envar => `${envar}=${envars[envar]}`)
.join(' ')
|> `$ ${%}`, 'node', args.join(' '))
|> console.log(%);
And it doesn't even work on objects. Lame. I know the reason: JS has no typing. Still is lame.X
.this()
.that()
.then()
.do()
.a()
.thing()
And I expect this to let me keep that chain going for when I need to run a static function against the chain
X
.this()
.that()
.then()
.do()
.a()
.thing()
|>JSON.stringify(%)
> `${50}: ${[1,2,3].map(a => `${a+1}`)}`
'50: 2,3,4'
Whether that's a good idea or clear code is another issue. But it's allowed.I can only hope this leads to Javascript becoming even more unbearable. Perhaps only this can weaken Google's resistance to WASM.
let keys = Object.keys(envars);
let text = keys.map(envar => `${envar}=${envars[envar]}`).join(" ");
console.log(chalk.dim(`$ ${text}`), "node", args.join(' '));
That way you can easily inspect and verify the intermediate values at runtime. Helpful for you to see if your code works as expected, helpful for others to see what the code is doing.Pipes are probably better used on a set of operations that takes input and runs multiple functions on the input, rather than fiddling with string concatenation multiple times.
What do we use multi-pipes for in unix? I can't recall the last time I used one where the middle action wasn't a filter of some sort. grep -v to remove lines, or colrm or awk print to cherry-pick fields from a multi-field line. Once in a very long while I need to do three commands with filters between them. Beyond that it's too complicated and I make a script to handle several of the steps.
A more aggressive reformulation would be to prefix the original code with
const envOutput = Object.keys(envars).map(envar => `${envar}=${envars[envar]}`).join(' ');
const argsOutput = args.join(' ');
Leaving the example being converted as simply: console.log(chalk.dim(`$ ${envOutput}`, 'node', argsOutput));
vs envOutput |> `$ ${%}` |> chalk.dim(%, 'node', argsOutput) |> console.log(%);
This more clearly highlights the obvious limitations of pipelines - piping envOutput but not 'node' nor argsOutput is a jarring syntax-mix here. Though I think it offers some hope for them working well in other scenarios - possibly in curry-heavy applications.Only if the first parameter of the function is the sole subject & subsequent parameters are "additional". Which isn't the case in this example: all params are equal subjects.
But you can still think about this as merging in two additional pipes into the one you are processing. So 'node' and argsOutput are just very short pipes that you are merging into the flow of current one.
Btw... chalk API feels super weird.
> I also feel this:
>> In the State of JS 2020 survey, the fourth top answer to “What do you feel is currently missing from JavaScript?” was a pipe operator.
> Is the wrong way to go about language design.
Let's call it Signor Rossi language design…"Signor Rossi cosa vuoi? … E poi, e poi, e poi" (Viva la felicità by Franco Godi [1])
I suspect that many developers expressed their desire for this operator under the believe that it would make their style of programming easier, which F# indeed does for many code bases, particularly those that use libraries such as fp-ts or rxjs.
However the advanced version fails to deliver that
let $= Object.keys(envars)
.map(envar => `${envar}=${envars[envar]}`)
.join(' ')
$= `$ ${$}`
$= chalk.dim($, 'node', args.join(' '))
$= console.log($);
This is possible in current JS syntax.You can also cram it into one line with semicolon:
let $= Object.keys(envars)
.map(envar => `${envar}=${envars[envar]}`)
.join(' ') ;$= `$ ${$}` ;$= chalk.dim($, 'node', args.join(' ')) ;$= console.log($);
So it's basically Hack syntax for pipes with just ;$= instead of |> and $ instead of %. And you need to 'mark' the beginning of the pipeline with `let $=`Additional 'benefit' is that until you leave the scope you can access output of the last pipe through $.
You can always use ;$= for consistency and pre-experess you intent to use the piping in the current scope by doing `let $;` ahead of time:
let $;
;$= Object.keys(envars)
.map(envar => `${envar}=${envars[envar]}`)
.join(' ')
;$= `$ ${$}`
;$= chalk.dim($, 'node', args.join(' '))
;$= console.log($);
Full disclosure, I hate all of the above but I love Hack syntax with |> and %.Also any valid JS name is good:
let _= Object.keys(envars)
.map(envar => `${envar}=${envars[envar]}`)
.join(' ')
_= `$ ${_}`
_= chalk.dim(_, 'node', args.join(' '))
_= console.log(_);
so you have a lot of flexibility in your pipes. :-DTo better confer the direction of the pipe you might even use the letter that is oriented to the right:
let D;
;D= Object.keys(envars)
.map(envar => `${envar}=${envars[envar]}`)
.join(' ')
;D= `$ ${D}`
;D= chalk.dim(D, 'node', args.join(' '))
;D= console.log(D); Object.keys(envars)
|> %.map(envar => `${envar}=${envars[envar]}`)
|> %.join(' ')
|> `$ ${%}`
|> chalk.dim(%, 'node', args.join(' '))
|> console.log(%); Object.keys(envars)
|> % => %.map(envar => `${envar}=${envars[envar]}`)
|> % => %.join(' ')
|> % => `$ ${%}`
|> % => chalk.dim(%, 'node', args.join(' '))
|> % => console.log(%);
To be honest, I've pretty much given up the hopes that TC39 would actually resolve pipelines and decorators at this point... I think it's been around a decade now.Reading this version though I also notice something else that the F# style enables that the Hack style doesn’t which is that it supports destructuring. So in F# style, I can more easily make pipe steps that pass on multiple values in a structured object or array, then access them easily down-pipe.
Is this Perl?
DROP jerkings_tab;
Is this really Perl?
Can you imagine a survey where everyone says they're satisfied with the language as-is? Of course not. That's statistically impossible. Even if 99% of the needs of developers are satisfied by a language, people are going to eventually answer that they'd like something that the language doesn't have, and that's always the case.
Let's say JavaScript implemented nearly every single language feature that's ever been invented. That is except the goto statement. Upon being surveyed on what they think is currently missing from JavaScript, a significant number of developers will have to respond with goto. Does that mean JavaScript is actually "missing" goto and that it was a mistake that it was never implemented in the first place? Of course not.
I just write things as stupidly as possible now, and just do the second one even though there are nicer "ruby-ways" to do them - maybe this is "bad" but I find it easy to read and grok...and it doesn't cause arguments during review <.<
Yes! A hundred thousand times yes!
I have not seen this syntax in JavaScript before, but just by having the vague notion that it's pipe-like, I was able to generally understand what was happening within seconds of reading it. I'd have to read up on the syntax a bit to confidently write code in this style, or perhaps to fix a bug (does "${%}" do what I think it does) but I can quite literally comprehend the author's intent almost at almost the same speed as I can read.
1. get all the keys of envars.
2. convert every key into a "var=value" format.
3. put a space between them all
4. stick a dollar sign in front of the whole thing
5. no clue what the `chalk` library is for?
a. this *appears* to prefix a "node ${args}" call with the previously-created environment variable bits
b. that tracks with what we've seen so far
c. close enough for now
6. log the whole thing
Note that even without knowing what chalk is or does, the flow of everything makes it extremely clear what exactly the high-level outcome is supposed to be. We're building up a string like "$ FOO=bar BAZ=qux node --arg thing --arg2 more_args". We're doing something fancy with it that I don't quite know about just yet, but the above knowledge makes it very easy to fill in that gap.With the second one, I have to construct a tree in my head
1. console.log…
a. the result of chalk.dim, which is
i. all the env vars
ii. mapped to key=value format
iii. joined by a space
b. ^ actually do chalk.dim on that thing above
i. sorry, back up, there were some additional arguments
c. ^ actually do chalk.dim on the above with 'node' and space-separated args
i. I think this concatenates them?
ii. double-check [1.a] to confirm
iii. did I miss `args` defined somewhere in [1.a]?
A. no, it's apparently inherited from scope
2. ^ actually log all the above
a. wait is this a correct reading, based on reassembling the above?
The confusion is compounded by not knowing the details of chalk. I have to jump backwards and reason about what the result of a non-linear chunk of steps is to make sure my guess is consistent.I have no opinion on the merits of this particular syntax, its implementation details, or in comparison to alternate proposals of similar ideas. I'm sure there's worthwhile debate to be had on those details. But from a high level, I'm a fan. My head is not a meat-based tree traverser and I'm guessing most people's isn't either. Human brains are big fans of linear narratives. While I love movies and shows like Memento and Westworld, this kind of disjoint storytelling is quite literally done for the purpose of generating confusion and not for promoting comprehension.
However, the 5th most requested feature in that poll is somehow "functions", and "sanity" also appears in the results, so this particular source may not be a good one.
You can even use an array if you don't need to peek the value in the middle of a chain of operations
Example:
createChain(
Object.entries(envars)
.map(([key, value]) => `${key}=${value}`)
.join(' ')
)
.map(keys => `$ ${keys}`)
.map(pattern => chalk.dim(%, 'node', args.join(' ')))
.use(line => console.log(line))
In each step, you can name the intermediate result accordingly to it's meaning, should be more readable than calling it %
Can also write it as:
Which seems clearer than either because it splits out "print env variables" and "print out node args". And it would be even better with some sort of helper to convert an object to k=v string: ---I also feel this:
> In the State of JS 2020 survey, the fourth top answer to “What do you feel is currently missing from JavaScript?” was a pipe operator.
Is the wrong way to go about language design. Everyone wants something different, and if you just implement the "top 5 most requested features" you're going to end up with some frankenbeast of a language.