[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)