[HN Gopher] Working pipe operator today in pure JavaScript
___________________________________________________________________
Working pipe operator today in pure JavaScript
Author : urvader
Score : 241 points
Date : 2025-10-04 09:54 UTC (4 days ago)
(HTM) web link (github.com)
(TXT) w3m dump (github.com)
| koito17 wrote:
| Since this library leverages Symbol.toPrimitive, you may also use
| operators besides bitwise-OR. Additionally, the library does not
| seem to dispatch on the `hint` parameter[0]. Now I want to open a
| JS REPL, try placing this library's pipe object into string
| template literals, and see what happens.
|
| Overall, cool library.
|
| [0] https://tc39.es/ecma262/multipage/abstract-
| operations.html#s...
| bryanrasmussen wrote:
| what are your ideas regarding the pipe object in string
| template literals? I'm just looking for an overview to see if
| it sparks some ideas.
| bckr wrote:
| Very appealing.
| stephenlf wrote:
| Cool work!
| c249709 wrote:
| this is sick
| tinyspacewizard wrote:
| Sad that the pipe operator proposal seems to have stalled.
|
| The F# version of the proposal was probably the simplest choice.
| pavlov wrote:
| Further proof that JavaScript accidentally became the new C++.
|
| "Aren't you surprised that this syntax works?" is not praise for
| a language design.
| asah wrote:
| Agreed!!!!
|
| Serious q: but how does this sentiment change with LLMs? They
| can pickup new syntax pretty fast, then use fewer tokens...
| fph wrote:
| I imagine the error messages must be terrible to read, since
| this hack is based on reusing syntax that was meant for
| something entirely different.
| Cthulhu_ wrote:
| It sounds like using less tokens (or, less output due to a
| more compact syntax) is like a micro-optimization; code
| should be written for readability, not for compactness. That
| said, there are some really compact programming languages out
| there if this is what you need to optimize for.
| pavlov wrote:
| I've heard it said before on HN that this is not true in
| general because more tokens in familiar patterns helps the
| model understand what it's doing (vs. very terse and novel
| syntax).
|
| Otherwise LLMs would excel at writing APL and similar
| languages, but seems like that's not the case.
| throwawaymaths wrote:
| probably because there arent enough apl examples to imbue
| the rare weird apl tokens with sufficient semantic meaning
| to be useful.
| miningape wrote:
| IMO it's more likely to get confused because there are less
| unique tokens to differentiate between syntax (e.x. pipe when
| we want bitwise-or or vice-versa)
| rco8786 wrote:
| Is there a language that can't be contorted in surprising ways
| that I'm unaware of?
| IshKebab wrote:
| Probably not, but there are definitely languages that don't
| do automatic type coercion - so at least one fewer contortion
| available.
| bobbylarrybobby wrote:
| LISP -- the contortions are expected, not surprises
| dominicrose wrote:
| A language where there's only one way to do things, maybe a
| very early version of CSS. The things is all languages end up
| bloated with new features.
| dymk wrote:
| I think that depends on the person, the language, and how
| familiar they are with the language. Someone's "what the
| fuck" is another's "obviously it can do that".
| refulgentis wrote:
| Swift, IMHO. Grew up on ObjC and the absolutely crazy things
| you could pull off dynamically at runtime. You can definitely
| feel they did _not_ want that in Swift. There 's operator
| overriding but idk if I'd count that as contorting in
| surprising ways _shrugs_
| cluckindan wrote:
| It is not a surprise that overriding the implementation of an
| operator's type coercion works and overrides the behavior of
| the operator's type coercion.
| pavlov wrote:
| Do you really think that most JavaScript users are aware that
| "overriding the implementation of an operator's type
| coercion" is a language feature?
|
| Sure, you can claim that everyone should know this obscure
| feature when they don't. But that's how this language enters
| C++ territory.
| catapart wrote:
| I actually don't think you are wrong, but I'm not backing
| that up with any actual data.
|
| 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.
| cluckindan wrote:
| Well, Proxy objects do allow you to override the behavior
| of any property, including Symbol properties.
| Symbol.iterator is pretty widely used to create custom
| iterable objects, so I would expect curious devs to have
| taken a look at what else can be done through the use of
| Symbol properties.
| overgard wrote:
| No way dude, this does a disservice to the insanity that is
| C++'s syntax. Wake me up when you have 6 different
| initialization syntaxes or fun things like 4[array]
| goobert wrote:
| Nice! I love it when a language introduces new syntax for things
| that weren't remotely difficult in the first place!
| Ciantic wrote:
| First example doesn't work though: 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.
| byteknight wrote:
| Seems similar to the problem encountered when making the stupid
| idea PyNQ:
|
| https://github.com/IAmStoxe/PyNQ
| md224 wrote:
| I suspect this was written with an LLM and the author didn't
| actually verify that the examples in the README worked.
| dymk wrote:
| Recently, I ripped usage examples out of a rust project's
| README.md, and put them in doc comments. Almost all of them
| were broken due to small changes over time, and I never
| remembered to update the readme. `cargo test` runs doc
| comments like mini integration tests, so now the examples
| never rot. I wish more languages and tools had this feature.
|
| 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.
| urvader wrote:
| I wrote this with an LLM but manually changed the README.
| Thanks for pointing this out, it is now updated.
| nticompass wrote:
| I was playing with it, and you can do this, which looks a
| little better. 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!!!"
| urvader wrote:
| Thanks for pointing this out, I updated the examples now to
| this syntax.
| hinkley wrote:
| That's not better because it implies these are all
| destructive function calls.
|
| Mutating your inputs is not functional programming. And pipes
| are effectively compact list comprehensions. Comprehensions
| without FP is Frankensteinian.
| hinkley wrote:
| That is a big enough DX problem that I would veto using this on
| a project.
|
| 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.
| sproutini wrote:
| Overengineered in my view, what is wrong with `x | f` is `f(x)`?
| Then `x | f | g` can be read as `g(f(x))` and you're done. I
| don't see any reason to make it more complicated than that.
| xigoi wrote:
| You can't make it work like that in current JavaScript.
| flanked-evergl wrote:
| It would be nice to have well-maintained fluent/pipe/streaming
| API solution for Python.
| reverseblade2 wrote:
| Alternatively just use F# and Fable
| jappgar wrote:
| is this solving a problem people actually have?
|
| other libraries like rxjs use .pipe(f,g,h) which works just fine.
| whizzter wrote:
| Fully agreed, var-arg functions are well established in JS so
| no need to abuse operators for these kinds of things.
| whizzter wrote:
| This kind of stuff is why C++ developers has an almost overly
| allergic reaction to operator overloading.
| jagged-chisel wrote:
| C++ is the reason people have that reaction. The quintessential
| example in introductory texts for operator overloading is using
| bit-shift operators to output text. I mean, come on - if that's
| your example, don't complain when people follow suit and get it
| wrong.
| whizzter wrote:
| C++ has std::format these days that does a far more sane
| thing, people are too quick to throw out the baby with the
| bathwater when it comes to bad things.
|
| 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.
| zamadatix wrote:
| std::format in C++20 is just for the string manipulation
| half but you still left shift cout by the resulting string
| to output text in canonical C++.
|
| 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!
| cluckindan wrote:
| This isn't overloading the operator, it is replacing the
| implementation of type coercion when | is used with pipe() or
| asPipe() objects.
|
| | itself still works exactly as before.
| whizzter wrote:
| Oh wait, looked at the source again, so it's some weird
| stateful collection thing triggered by the type coercion? By
| now I'm wishing that it was operator overloading.
| cluckindan wrote:
| Imagine the possibilities for control flow obfuscation when
| this trick is used with parentheses wrapping a part of the
| pipeline. :-)
| keepamovin wrote:
| Damn, that's really clever. I love seeing these expressive
| explorations of JavaScript syntax.
| keepamovin wrote:
| Damn, that's really clever. I love seeing these expressive
| explorations of JavaScript syntax.
| gregabbott wrote:
| In case it might interest anyone, I wrote a similar vanilla JS
| function last year called Chute. Chute chains methods and
| function calls using dot-notation.
|
| https://github.com/gregabbott/chute
| suspended_state wrote:
| That's Point-free style programming.
|
| https://en.wikipedia.org/wiki/Tacit_programming
| xixixao wrote:
| Won't work in TS.
|
| 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.
| CharlieDigital wrote:
| Check out C#. CliWrap does exactly this:
| https://github.com/Tyrrrz/CliWrap/blob/master/CliWrap/Comman...
| // 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;
| juliend2 wrote:
| This cargo seem to give magical superpowers.
| MathMonkeyMan wrote:
| Neat, but I think that functions already do what we need.
|
| 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 + '!!!');
| nonethewiser wrote:
| Are any of the cases compelling? Thinking of the actual
| proposal. It creates some new magic with |> and % just for
| syntactic sugar.
| accrual wrote:
| I am all for clean syntax but I feel like JS has already
| reached a nice middle ground between expressiveness
| (especially w/ map/reduce/filter) and readability. I'd
| personally rather not have another syntax that everyone will
| have to learn unless we're already moving to a new language.
| nonethewiser wrote:
| I agree but to steelman it, what about custom functions? I
| think just doing it naively is perfectly fine. Or if you
| want use some pipe utility. Or wrap the array, string, etc.
| with your own custom methods.
| IshKebab wrote:
| I think JS's map/reduce/filter design is one of the worst
| ones out there actually - map has footguns with its extra
| arguments and everything gets converted to an array at the
| drop of a hat. Still, pipeline syntax probably won't help
| fix any of that.
| eyelidlessness wrote:
| > everything gets converted to an array at the drop of a
| hat
|
| 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.
| IshKebab wrote:
| `map` returns an array and can only be called on an
| array.
| eyelidlessness wrote:
| Right, but I'm not clear on what gets _converted_ to an
| array. Do you mean more or less what I said in my
| previous comment? That it requires _you_ (your code, or
| calling code in general) to perform that conversion
| excessively?
| recursive wrote:
| People write a lot of stuff like [...iterable].map(fn).
| They do it so much it's as if they do it each time a hat
| drops.
| eyelidlessness wrote:
| Thank you for clarifying. (I think?)
|
| 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.
| Timwi wrote:
| One gripe I have is that the result of map/filter is
| always an array. As a result, doing
| `foo.map(...).filter(...).slice(0, 3)` will run the map
| and the filter on the _entire_ array even if it has
| hundreds of entries and I only need the first 10 to find
| the 3 that match the filter.
| thdhhghgbhy wrote:
| I always thought JS map filter reduce felt quite nice,
| especially playing around with data in the REPL. Java
| maps with all the conversions back and forth to streams
| are clumsy.
| Timwi wrote:
| Well in JS you have to convert to arrays instead. You
| can't do `document.querySelectorAll(...).map(...)`.
| rco8786 wrote:
| Whatever that thrush thing is feels 10x more gross than the
| pipe
| svieira wrote:
| Thrush is the "T combinator" - I believe that the "Thrush"
| name comes from _To Mock a Mockingbird_ by Raymond Smullyan
| [1].
|
| [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
| flexagoon wrote:
| Not if you consider that the linked repo requires you to use
| asPipe on all functions first. So it's this:
| 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)
| fergie wrote:
| Pipes are great in environments where "everything is a string"
| (bash, etc), but do we really need them in javascript? I have yet
| to see a compelling example.
| tinyspacewizard wrote:
| Pipes are great where you want to chain several operations
| together. Piping is very common in statically typed functional
| langauges, where there are lots of different types in play.
|
| 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.
| discomrobertul8 wrote:
| if the language or std lib already allows for chaining then
| pipes aren't as attractive. They're a much nicer alternative
| when the other answer is nested function calls.
|
| 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.
| nonethewiser wrote:
| First of all, with the actual proposal, wouldnt it actually
| be like this? with the %. 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.
| tinyspacewizard wrote:
| There is more than one proposal; the F#-style one doesn't
| have the (weird) placeholder syntax.
|
| > You can still make it work by adding it to the prototype
|
| This is exactly what we want to avoid!
| nonethewiser wrote:
| wrap the object?
|
| Why would you want to avoid that? It's controversial
| syntactic sugar. Enforcing a convention locally seems
| ideal.
| nymalt wrote:
| That's clever! But I still want JS to get the actual pipeline
| operator.
| bonquesha99 wrote:
| If you're interested in the Ruby language too, check out this PoC
| gem for an "operator-less" syntax for pipe operations using
| regular blocks/expressions like every other Ruby DSL.
|
| 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"]
| dominicrose wrote:
| It's an interesting experiment but standard Ruby is expressive
| enough.
|
| [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.
| pwdisswordfishy wrote:
| 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.
| sethcalebweeks wrote:
| I love the idea! The creativity of (ab)using JavaScript type
| coersion is really neat. I did something similar using proxies to
| create a chainable API.
|
| 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'
| rco8786 wrote:
| Very clever. Love seeing stuff like this that pushes the bounds
| don_searchcraft wrote:
| These TC39 proposals take way too long to get approved and
| implemented.
| user____name wrote:
| Is this intended for code golf or something? This buys you
| literally nothing and just makes the language needlessly cryptic.
| ilaksh wrote:
| See also https://livescript.net/
| aprilnya wrote:
| This is just different syntax for nesting function calls (i.e.
| c(b(a(value))) becomes value | a | b | c), right? Definitely
| would make code more readable if this was just something in JS or
| a compiler where it's the same as normally calling functions.
| urvader wrote:
| I'm not sure this is at all a good idea but thanks to the great
| discussion here at Hacker News - it is now up on npm:
|
| npm i aspipes
| adamddev1 wrote:
| Now we just need 'do notation' for monads! :-)
| mlajtos wrote:
| Pipe "operator" for the rest of us:
| 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('!!!'))
| taylorallred wrote:
| Piping syntax is nice for reading, but it's hard to debug.
| There's no clear way to "step through" each stage of the pipe to
| see the intermediate results.
| bitwize wrote:
| It's simultaneously a miracle and _deeply_ wrong that this
| worked.
___________________________________________________________________
(page generated 2025-10-08 23:00 UTC)