const greeting = pipe('hello')
| upper
| ex('!!!')
await greeting.run() // → "HELLO!!!"
If you look at the tests file, it needs to be written like this to make it work: let greeting;
(greeting = pipe('hello')) | upper | ex('!!!');
await greeting.run();
Which is not anymore as ergonomic.It means having to go to the linked docs (which are automatically pushed to the repo's github pages) to see examples, but I think this is a reasonable tradeoff.
const greeting = pipe('hello');
greeting | upper | ex('!!!');
await greeting.run(); // → "HELLO!!!"
Since it uses the "Symbol.toPrimitive" method, you can use any operator (not just "bitwise OR" (|)). const greeting = pipe('hello');
greeting / upper * ex('!!!');
await greeting.run(); // → "HELLO!!!"
Mutating your inputs is not functional programming. And pipes are effectively compact list comprehensions. Comprehensions without FP is Frankensteinian.
You’ve implied what I’ll state clearly:
Pipes are for composing transformations, one per line, so that reading comprehension doesn’t nosedive too fast with accumulation of subsequent operations.
Chaining on the same line is shit for readying and worst for git merges and PR reviews.
“Aren’t you surprised that this syntax works?” is not praise for a language design.
It's a clever hack. This is Hacker News. Let's try to appreciate clever hacks while here.
Sure, you can claim that everyone should know this obscure feature when they don’t. But that’s how this language enters C++ territory.
I happened to know it because of how the hyperHTML micro-library works; the author went into great detail about it and a ton of other topics. But my gut would say that the average js dev doesn't know about it.
But then... it's useful for creating component frameworks which... most js devs use. Which doesn't mean they know how they work under the hood. But... a lot of devs I've met specifically choose a framework because of how it works under the hood.
... so... I really have no idea how many people know this. I'm still betting it's less than average.
https://metacpan.org/dist/perlsecret/view/lib/perlsecret.pod...
Serious q: but how does this sentiment change with LLMs? They can pickup new syntax pretty fast, then use fewer tokens...
Otherwise LLMs would excel at writing APL and similar languages, but seems like that’s not the case.
https://dev.to/sethcalebweeks/fluent-api-for-piping-standalo...
const shuffle = (arr) => arr.sort(() => Math.random() - 0.5);
const zipWith = (a, b, fn) => a.slice(0, Math.min(a.length, b.length)).map((x, i) => fn(x, b[i]));
const log = (arr) => {
console.log(arr);
return arr;
};
const chain = chainWith({shuffle, zipWith, log});
chain([1, 2, 3, 4, 5, 6, 7, 8, 9])
.map((i) => i + 10)
.log() // [ 11, 12, 13, 14, 15, 16, 17, 18, 19 ]
.shuffle()
.log() // e.g. [ 16, 15, 11, 19, 12, 13, 18, 14, 17 ]
.zipWith(["a", "b", "c", "d", "e"], (a, b) => a + b)
.log() // e.g. [ '16a', '15b', '11c', '19d', '12e' ]
[0]; // e.g. '16a'
In a similar way to the featured project, Chute also uses proxies to work like a pipeline operator. But like in your reply, Chute uses a dot-notation style to chain and send data through a mix of functions and methods.
You might like to see how Chute uses proxies, as it requires no `chainWith` or similar setup step before use. Without setup, Chute can send data through global or local, top-level or nested, native or custom, unary, curried or non-unary functions and methods. It gives non-unary functions the current data at a specific argument position by using a custom-nameable placeholder variable.
The Chute page describes some more of its features: https://gregabbott.pages.dev/chute/
Overall, cool library.
[0] https://tc39.es/ecma262/multipage/abstract-operations.html#s...
For one thing, the example isn't the most compelling, because you can:
const greeting = 'hello'.toUpperCase() + '!!!';
or const greeting = 'HELLO!!!';
That said, there is already: function thrush(initial, ...funcs) {
return funcs.reduce(
(current, func) => func(current),
initial);
}
const greeting = thrush('hello', s => s.toUpperCase(), s => s + '!!!');
Can you name an example? IME the opposite is a more common complaint: needing to explicitly convert values to arrays from many common APIs which return eg iterables/iterators.
I think what confused me is the passive language: "everything gets converted" sounds (to me) like the runtime or some aspect of language semantics is converting everything, rather than developers. Whereas this is the same complaint I mentioned.
const greeting = thrush(
'hello',
s => s.toUpperCase(),
s => s + '!!!'
);
Vs this: const upper = asPipe(s => s.toUpperCase())
const ex = asPipe((s) => s + '!!!')
const greeting = pipe('hello')
| upper
| ex
await greeting.run()
(And that doesn't work in reality, as the top comment here notes)[1]: https://www.amazon.com/Mock-Mockingbird-Other-Logic-Puzzles/...
[2]: https://en.wikipedia.org/wiki/Combinatory_logic#In_computing
[3]: https://leanpub.com/combinators/read#leanpub-auto-the-thrush
https://github.com/lendinghome/pipe_operator#-pipe_operator
"https://api.github.com/repos/ruby/ruby".pipe do
URI.parse
Net::HTTP.get
JSON.parse.fetch("stargazers_count")
yield_self { |n| "Ruby has #{n} stars" }
Kernel.puts
end
#=> Ruby has 15120 stars
[9, 64].map(&Math.pipe.sqrt) #=> [3.0, 8.0]
[9, 64].map(&Math.pipe.sqrt.to_i.to_s) #=> ["3", "8"]
[9, 64].map { Math.sqrt(_1) } #=> [3.0, 8.0]
For the first example I would just define a method that uses local variables. They're local so it's not polluting context.
Some OO is fine, just don't make your architecture or language entirely dependent on it. Same with operator overloading.
When it comes to math heavy workloads, you really want a language that supports operator overloading (or have a language full of heavy vector primitives), doing it all without just becomes painful for other reasons.
Yes, the early C++ _STDLIB_ was shit early on due to boneheaded architectural and syntactic decisions (and memory safety issues is another whole chapter), but that doesn't take away that the language is a damn powerful and useful one.
C++23 introduced std::print(), which is more or less the modernized printf() C++ probably should have started with and also includes the functionality of std::format(). Unfortunately, it'll be another 10 years before I can actually use it outside of home projects... but at least it's there now!
| itself still works exactly as before.
The F# version of the proposal was probably the simplest choice.
Sequences are a common example.
So this:
xs.map(x => x * 2).filter(x => x > 4).sorted().take(5)
In pipes this might look like: xs |> map(x => x * 2) |> filter(x => x > 4) |> sorted() |> take(5)
In functional languages (of the ML variety), convention is to put each operation on its own line: xs
|> map(x => x * 2)
|> filter(x => x > 4)
|> sorted()
|> take(5)
Note this makes for really nice diffs with the standard Git diff tool!But why is this better?
Well, suppose the operation you want is not implemented as a method on `xs`. For a long time JavaScript did not offer `flatMap` on arrays.
You'll need to add it somehow, such as on the prototype (nasty) or by wrapping `xs` in another type (overhead, verbose).
With the pipe operator, each operation is just a plain-ol function.
This:
xs |> f
Is syntactic sugar for: f(xs)
This allows us to "extend" `xs` in a manner that can be compiled with zero run-time overhead.e.g.
So this:
take(sorted(filter(map(xs, x => x \* 2), x => x > 4)), 5)
To your example: xs |> map(x => x \* 2) |> filter(x => x > 4) |> sorted() |> take(5)
is a marked improvement to me. Much easier to read the order of operations and which args belong to which call. xs
|> map(%, x => x * 2)
|> filter(%, x => x > 4)
|> sorted(%)
|> take(%, 5);
Anything that can currently just chain functions seems like a terrible example because this is perfectly fine: xs.map(x => x * 2)
.filter(x => x > 4)
.sorted()
.take(5)
Not just fine but much better. No new operators required and less verbose. Just strictly better. This ignores the fact that sorted and take are not actually array methods, but there are equivalent.But besides that, I think the better steelman would use methods that dont already exist on the prototype. You can still make it work by adding it to the prototype but... meh. Not that I even liket he proposal in that case.
> You can still make it work by adding it to the prototype
This is exactly what we want to avoid!
Why would you want to avoid that? It's controversial syntactic sugar. Enforcing a convention locally seems ideal.
2. There is a run-time overhead to wrapping
IMO a design goal of programming langauges should be for the most readable code to also be the most performant.
Language features tend to be controversial until they are mainstream.
I would actually love extension of TS with operator overloading for vector maths (games, other linear algebra, ML use cases). I wouldn’t want libraries to rely on it, but in my own application code, it can sometimes be really helpful.
// Examples
var cmd = Cli.Wrap("foo") | (stdOut, stdErr);
var target = PipeTarget.Merge(
PipeTarget.ToFile("file1.txt"),
PipeTarget.ToFile("file2.txt"),
PipeTarget.ToFile("file3.txt")
);
var cmd = Cli.Wrap("foo") | target;
Object.prototype.pipe = function(fn) { return fn(this) }
'hello'.pipe(upper).pipe(ex('!!!'))
Or code golf version: Object.prototype.P=function(...F){return F.reduce((v,f)=>f(v),this)}
'hello'.P(upper,ex('!!!'))
I am wondering if it could be useful for libraries:
grid.columns.name.format(v => v | trim | truncate | bold)
form.fields.name.validate(v => v | trim | required | email)
npm i aspipes
new Proxy(function(){}, {
get(_, prop) {
if (prop === Symbol.toPrimitive)
return () => ...
As opposed to, you know, just defining a method. Proxy has apparently become the new adding custom methods to built-in prototypes.