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!!!"
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'
“Aren’t you surprised that this syntax works?” is not praise for a language design.
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.
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.
Overall, cool library.
[0] https://tc39.es/ecma262/multipage/abstract-operations.html#s...
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.
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.
[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
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)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.
npm i aspipes
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;
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.
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.