[HN Gopher] Pipelining might be my favorite programming language...
       ___________________________________________________________________
        
       Pipelining might be my favorite programming language feature
        
       Author : Mond_
       Score  : 232 points
       Date   : 2025-04-21 12:16 UTC (10 hours ago)
        
 (HTM) web link (herecomesthemoon.net)
 (TXT) w3m dump (herecomesthemoon.net)
        
       | SimonDorfman wrote:
       | The tidyverse folks in R have been using that for a while:
       | https://magrittr.tidyverse.org/reference/pipe.html
        
         | flobosg wrote:
         | Base R as well: |> was implemented as a pipe operator in 4.1.0.
        
           | tylermw wrote:
           | Importantly, the base R pipe implements the operation at the
           | language parsing level, so it has basically zero overhead.
        
         | madcaptenor wrote:
         | And base R has had a pipe for a couple years now, although
         | there are some differences between base R's |> and tidyverse's
         | %>%: https://www.tidyverse.org/blog/2023/04/base-vs-magrittr-
         | pipe...
        
         | thom wrote:
         | I've always found magrittr mildly hilarious. R has vestigial
         | Lisp DNA, but somehow the R implementation of pipes was
         | incredibly long, complex and produced stack traces, so it moved
         | to a native C implementation, which nevertheless has to
         | manipulate the SEXPs that secretly underlie the language.
         | Compared to something like Clojure's threading macros it's wild
         | how much work is needed.
        
         | steine65 wrote:
         | R, specifically tidyverse, has a special place in my heart.
         | Tidy principles makes data analysis easy to read and easy to
         | use new functions, since there are standards that must be met
         | to call a function "tidy."
         | 
         | Recently I started using Nushell, which feels very similar.
        
       | jaymbo wrote:
       | This is why I love Scala so much
        
         | minebreaker wrote:
         | `tap` is cool.
        
         | rad_gruchalski wrote:
         | Scala is by far one of the nicest programming languages I have
         | ever worked with. Scala with no JVM dependency would a killer
         | programming language BUT only when all async features work out
         | of the box like they do JVM. It's been attempted a couple of
         | times and it never succeeded.
        
       | shae wrote:
       | If Python object methods returned `self` by default instead of
       | `None` you could do this in Python too!
       | 
       | This is my biggest complaint about Python.
        
         | incognito124 wrote:
         | there are https://github.com/JulienPalard/Pipe and
         | https://github.com/0101/pipetools
        
       | mexicocitinluez wrote:
       | LINQ is easily one of C#'s best features.
        
         | adzm wrote:
         | Interestingly though the actual integrated query part is much
         | less useful or widely used as the methods on IEnumerable etc.
        
       | kordlessagain wrote:
       | While the author claims "semantics beat syntax every day of the
       | week," the entire article focuses on syntax preferences rather
       | than semantic differences.
       | 
       | Pipelining can become hard to debug when chains get very long.
       | The author doesn't address how hard it can be to identify which
       | step in a long chain caused an error.
       | 
       | They do make fun of Python, however. But don't say much about why
       | they don't like it other than showing a low-res photo of a rock
       | with a pipe routed around it.
       | 
       | Ambiguity about what constitutes "pipelining" is the real issue
       | here. The definition keeps shifting throughout the article. Is it
       | method chaining? Operator overloading? First-class functions? The
       | author uses examples that function very differently.
        
         | pavel_lishin wrote:
         | The article also clearly points that that it's just a hot-take,
         | and to not take it too seriously.
        
         | zelphirkalt wrote:
         | You can add peek steps in pipelines and inspect the in between
         | results. Not really any different from normal function call
         | debugging imo.
        
           | krapht wrote:
           | Yes, but here's my hot take - what if you didn't have to edit
           | the source code to debug it? Instead of chaining method calls
           | you just assign to a temporary variable. Then you can set
           | breakpoints and inspect variable values like you do normally
           | without editing source.
           | 
           | It's not like you lose that much readability from
           | foo(bar(baz(c)))            c |> baz |> bar |> foo
           | c.baz().bar().foo()            t = c.baz()       t = t.bar()
           | t = t.foo()
        
             | Mond_ wrote:
             | I feel like a sufficiently good debugger should allow you
             | to place a breakpoint at any of the lines here, and it
             | should break exactly at that specific line.
             | fn get_ids(data: Vec<Widget>) -> Vec<Id> {
             | data.iter()               .filter(|w| w.alive)
             | .map(|w| w.id)               .collect()       }
             | 
             | It sounds to me like you're asking for linebreaks. Chaining
             | doesn't seem to be the issue here.
        
               | krapht wrote:
               | I'm only familiar with C++, Python, and SQL. Neither GDB
               | nor PDB helps here, and I've never heard of a SQL
               | debugger that will break apart expressions and let you
               | view intermediate query results.
        
               | Mond_ wrote:
               | That'd be problematic, but also sounds like a (solvable)
               | tooling problem to me.
        
               | jen20 wrote:
               | It's been a while since I've used one, but I'm fairly
               | sure the common debuggers for C#, F#, Rust and Java would
               | all behave correctly when breakpointed like this.
        
               | Merad wrote:
               | Jetbrains Rider does this does for C# code (I think
               | Visual Studio does as well). Its inlay hints feature will
               | also show you hints with the result type of each line as
               | the data is transformed. I haven't explicitly tested but
               | I would imagine their IDEs for other languages behave the
               | same.
        
             | erichocean wrote:
             | The Clojure equivalent of `c |> baz |> bar |> foo` are the
             | threading macros:                   (-> c baz bar foo)
             | 
             | But people usually put it on separate lines:
             | (-> c             baz             bar             foo)
        
               | joeevans1000 wrote:
               | And with the Emacs Enlighten feature the second version
               | enables seeing the results of each step right in the
               | editor, to the right of the step.
        
               | erichocean wrote:
               | You can achieve something similar in Clojure with the
               | Flowstorm debugger[0] (it's free).
               | 
               | [0] https://www.flow-storm.org/
        
             | andyferris wrote:
             | A debugger should let you inspect the value of any
             | expression, not just variables.
        
         | Mond_ wrote:
         | > Pipelining can become hard to debug when chains get very
         | long. The author doesn't address how hard it can be to identify
         | which step in a long chain caused an error.
         | 
         | Yeah, I agree that this can be problem when you lean heavily
         | into monadic handling (i.e. you have fallible operations and
         | then pipe the error or null all the way through, losing the
         | information of where it came from).
         | 
         | But that doesn't have much to do with the article: You have the
         | same problem with non-pipelined functional code. (And in either
         | case, I think that it's not that big of a problem in practice.)
         | 
         | > The author uses examples that function very differently.
         | 
         | Yeah, this is addressed in one of the later sections. Imo,
         | having a unified word for such a convenience feature (no matter
         | how it's implemented) is better than thinking of these features
         | as completely separate.
        
         | fsckboy wrote:
         | the paragraph you quoted (atm, 7 mins ago, did it change?)
         | says:
         | 
         | > _Let me make it very clear: This is [not an] article it 's a
         | hot take about syntax. In practice, semantics beat syntax every
         | day of the week. In other words, don't take it too seriously._
        
         | bena wrote:
         | I think you may have misinterpreted his motive here.
         | 
         | Just before that statement, he says that it _is_ an article
         | /hot take about syntax. He acknowledges your point.
         | 
         | So I think when he says "semantics beat syntax every day of the
         | week", that's him acknowledging that while he _prefers_ certain
         | syntax, it may not be the best for a given situation.
        
         | AYBABTME wrote:
         | It's just as difficult to debug when function calls are nested
         | inline instead of assigning to variables and passing the
         | variables around.
        
         | steine65 wrote:
         | Agreed that long chains are hard to debug. I like to keep
         | chains around the size of a short paragraph.
        
       | kuon wrote:
       | That's also why I enjoy elixir a lot.
       | 
       | The |> operator is really cool.
        
       | drchickensalad wrote:
       | I miss F#
        
         | aloisdg wrote:
         | So do I sibling. so do I
        
       | osigurdson wrote:
       | C# has had "Pipelining" (aka Linq) for 17 years. I do miss this
       | kind of stuff in Go a little.
        
         | vjvjvjvjghv wrote:
         | Agreed. It would be nice if SQL databases supported something
         | similar.
        
           | NortySpock wrote:
           | I've used "a series of CTEs" to apply a series of
           | transformations and filters, but it's not nearly as elegant
           | as the pipe syntax.
        
           | sidpatil wrote:
           | PRQL [1] is a pipeline-based query language that compiles to
           | SQL.
           | 
           | [1] https://prql-lang.org/
        
         | bob1029 wrote:
         | I don't see how LINQ provides an especially illuminating
         | example of what is effectively method chaining.
         | 
         | It is an exemplar of expressions [0] more than anything else,
         | which have little to do with the idea of passing results from
         | one method to another.
         | 
         | [0]: https://learn.microsoft.com/en-us/dotnet/csharp/language-
         | ref...
        
           | delusional wrote:
           | You might be talking about LINQ queries, while the person you
           | are responding to is probably talking about LINQ in Method
           | Syntax[1]
           | 
           | [1]: https://learn.microsoft.com/en-
           | us/dotnet/csharp/linq/get-sta...
        
           | hahn-kev wrote:
           | So many things have been called Linq over the years it's hard
           | to talk about at this point. I've written C# for many years
           | now and I'm not even sure what I would say it's referring to,
           | so I avoid the term.
           | 
           | In this case I would say extension methods are what he's
           | really referring to, of which Linq to objects is built on top
           | of.
        
             | osigurdson wrote:
             | I'd say there are just two things:
             | 
             | 1) The method chaining extension methods on IEnumerable<T>
             | like Select, Where, GroupBy, etc. This is identical to the
             | rust example in the article.
             | 
             | 2) The weird / bad (in my opinion) language keywords
             | analogous to the above such as "from", "where", "select"
             | etc.
        
           | osigurdson wrote:
           | Example from article:
           | 
           | fn get_ids(data: Vec<Widget>) -> Vec<Id> { data.iter() // get
           | iterator over elements of the list .filter(|w| w.alive) //
           | use lambda to ignore tombstoned widgets .map(|w| w.id) //
           | extract ids from widgets .collect() // assemble iterator into
           | data structure (Vec) }
           | 
           | Same thing in 15 year old C# code.
           | 
           | List<Guid> GetIds(List<Widget> data)
           | 
           | {                   return data                     .Where(w
           | => w.IsAlive())                     .Select(w => w.Id)
           | .ToList();          }
        
       | zelphirkalt wrote:
       | To one up this: Of course it is even better, if your language
       | allows you to implement proper pipelining with implicit argument
       | passing by yourself. Then the standard language does not need to
       | provide it and assign meaning to some symbols for pipelining. You
       | can decide for yourself what symbols are used and what you find
       | intuitive.
       | 
       | Pipelining can guide one to write a bit cleaner code, viewing
       | steps of computation as such, and not as modifications of global
       | state. It forces one to make each step return a result, write
       | proper functions. I like proper pipelining a lot.
        
         | Mond_ wrote:
         | > if your language allows you to implement proper pipelining
         | with implicit argument passing by yourself > You can decide for
         | yourself what symbols are used and what you find intuitive
         | 
         | i mean this sounds fun
         | 
         | but tbh it also sounds like it'd result in my colleague Carl
         | defining an utterly bespoke DSL in the language, and using it
         | to write the worst spaghetti code the world has ever seen,
         | leaving the code base an unreadable mess full of sharp edges
         | and implicit behavior
        
           | 0x1ceb00da wrote:
           | Even veterans mess things up if you use too much of these
           | exotic syntaxes. For loops and if statements rock, but they
           | aren't cool and functional so they aren't discussed much.
        
       | mrkeen wrote:
       | data.iter()           .filter(|w| w.alive)           .map(|w|
       | w.id)           .collect()
       | collect(map(filter(iter(data), |w| w.alive), |w| w.id))
       | 
       | The second approach is open for extension - it allows you to
       | write new functions on old datatypes.
       | 
       | > Quick challenge for the curious Rustacean, can you explain why
       | we cannot rewrite the above code like this, even if we import all
       | of the symbols?
       | 
       | Probably for lack of
       | 
       | > weird operators like <$>, <*>, $, or >>=
        
         | esafak wrote:
         | Extension methods to the rescue:
         | https://en.wikipedia.org/wiki/Extension_method
         | 
         | Examples:
         | 
         | https://kotlinlang.org/docs/extensions.html
         | 
         | https://docs.scala-lang.org/scala3/reference/contextual/exte...
         | 
         | See also:
         | https://en.wikipedia.org/wiki/Uniform_function_call_syntax
        
           | vips7L wrote:
           | I really wish you couldn't write extensions on nullable
           | types. It's confusing to be able to call what look like
           | instance functions on something clearly nullable without
           | checking.                   fun main() {             val s:
           | String? = null             println(s.isS()) // false
           | }              fun String?.isS() = "s" == this
        
           | rikthevik wrote:
           | Came here for the Uniform function call syntax link. This is
           | one of the little choices that has a big impact on a
           | language! I love it!
           | 
           | I wrote a little pipeline macro in https://nim-lang.org/ for
           | Advent of Code years ago and as far as I know it worked okay.
           | 
           | ``` import macros                 macro `|>`\* (left, right :
           | expr): expr =         result = newNimNode(nnkCall)
           | case right.kind         of nnkCall:
           | result.add(right[0])           result.add(left)           for
           | i in 1..<right.len:             result.add(right[i])
           | else:           error("Unsupported node type")
           | 
           | ```
           | 
           | Makes me want to go write more nim.
        
         | Mond_ wrote:
         | > The second approach is open for extension - it allows you to
         | write new functions on old datatypes.
         | 
         | I prefer to just generalize the function (make it generic,
         | leverage traits/typeclasses) tbh.
         | 
         | > Probably for lack of > weird operators like <$>, <*>, $, or
         | >>=
         | 
         | Nope btw. I mean, maybe? I don't know Haskell well enough to
         | say. The answer that I was looking for here is a specific Rust
         | idiosyncrasy. It doesn't allow you to import
         | `std::iter::Iterator::collect` on its own. It's an associated
         | function, and needs to be qualified. (So you need to write
         | `Iterator::collect` at the very least.)
        
           | higherhalf wrote:
           | > It doesn't allow you to import
           | `std::iter::Iterator::collect` on its own. It's an associated
           | function, and needs to be qualified.
           | 
           | You probably noticed, but it should become a thing in RFC
           | 3591: https://github.com/rust-lang/rust/issues/134691
           | 
           | So it does kind of work on current nightly:
           | #![feature(import_trait_associated_functions)]
           | use std::iter::Iterator::{filter, map, collect};
           | fn get_ids2(data: Vec<Widget>) -> Vec<Id> {
           | collect(map(filter(Vec::into_iter(data), |w| w.alive), |w|
           | w.id))       }              fn get_ids3(data: impl
           | Iterator<Item = Widget>) -> Vec<Id> {
           | collect(map(filter(data, |w| w.alive), |w| w.id))       }
        
             | Mond_ wrote:
             | Oh, interesting! Thank you, I did not know about that,
             | actually.
        
         | pornel wrote:
         | Rust has such open extensibility through traits. The prime
         | example is Itertools that already adds a bunch of extra
         | pipelining helper methods.
        
       | epolanski wrote:
       | I personally like how effect-ts allows you to write both
       | pipelines or imperative code to express the very same things.
       | 
       | Building pipelines:
       | 
       | https://effect.website/docs/getting-started/building-pipelin...
       | 
       | Using generators:
       | 
       | https://effect.website/docs/getting-started/using-generators...
       | 
       | Having both options is great (at the beginning effect had only
       | pipe-based pipelines), after years of writing effect I'm
       | convinced that most of the time you'd rather write and read
       | imperative code than pipelines which definitely have their place
       | in code bases.
       | 
       | In fact most of the community, at large, converged at using
       | imperative-style generators over pipelines and having onboarded
       | many devs and having seen many long-time pipeliners converging to
       | classical imperative control flow seems to confirm both debugging
       | and maintenance seem easier.
        
       | bnchrch wrote:
       | I'm personally someone who advocates for languages to keep their
       | feature set small and shoot to achieve a finished feature set
       | quickly.
       | 
       | However.
       | 
       | I would be lying if I didn't secretly wish that all languages
       | adopted the `|>` syntax from Elixir.
       | 
       | ```
       | 
       | params
       | 
       | |> Map.get("user")
       | 
       | |> create_user()
       | 
       | |> notify_admin()
       | 
       | ```
        
         | valenterry wrote:
         | I prefer Scala. You can write
         | 
         | ``` params.get("user") |> create_user |> notify_admin ```
         | 
         | Even more concise and it doesn't even require a special
         | language feature, it's just regular syntax of the language ( |>
         | is a method like .get(...) so you could even write
         | `params.get("user").|>(create_user) if you wanted to)
        
           | elbasti wrote:
           | In elixir, ```Map.get("user") |> create_user |> notify_admin
           | ``` would aso be valid, standard elixir, just not idiomatic
           | (parens are optional, but preferred in most cases, and one-
           | line pipes are also frowned upon except for scripting).
        
             | MaxBarraclough wrote:
             | With the disclaimer that I don't know Elixir and haven't
             | programmed with the pipeline operator before: I don't like
             | that special _()_ syntax. That syntax denotes application
             | of the function without passing any arguments, but the
             | whole point here is that an argument _is_ being passed. It
             | seems clearer to me to just put the pipeline operator and
             | the name of the function that it 's being used with. I
             | don't see how it's unclear that application is being
             | handled by the pipeline operator.
             | 
             | Also, what if the function you want to use is returned by
             | some nullary function? You couldn't just do _| >
             | getfunc()_, as presumably the pipeline operator will
             | interfere with the usual meaning of the parentheses and
             | will try to pass something to _getfunc_. Would _| > (
             | getfunc() )_ work? This is the kind of problem that can
             | arise when one language feature is permitted to change the
             | ordinary behaviour of an existing feature in the name of
             | convenience. (Unless of course I'm just missing something.)
        
         | Cyykratahk wrote:
         | We might be able to cross one more language off your wishlist
         | soon, Javascript is on the way to getting a pipeline operator,
         | the proposal is currently at Stage 2
         | 
         | https://github.com/tc39/proposal-pipeline-operator
         | 
         | I'm very excited for it.
        
           | zdragnar wrote:
           | I worry about "soon" here. I've been excited for this
           | proposal for _years_ now (8 maybe? I forget), and I 'm not
           | sure it'll ever actually get traction at this point.
        
           | hoppp wrote:
           | Cool I love it, but another thing we will need polyfills
           | for...
        
             | bobbylarrybobby wrote:
             | How do you polyfill syntax?
        
               | jononor wrote:
               | Letting your JS/TS compiler convert it into supported
               | form. Not really a polyfill, but it allows to use new
               | features in the source and still support older targets.
               | This was done a lot when ES6 was new, I remember.
        
               | zdragnar wrote:
               | Polyfills are for runtime behavior that can't be
               | replicated with a simple syntax transformation, such as
               | adding new functions to built-in objects like
               | string.prototype contains or the Symbol constructor and
               | prototype or custom elements.
               | 
               | I haven't looked at the member properties bits but I
               | suspect the pipeline syntax just needs the transform to
               | be supported in build tools, rather than adding yet
               | another polyfill.
        
             | hathawsh wrote:
             | I believe you meant to say we will need a transpiler, not
             | polyfill. Of course, a lot of us are already using
             | transpilers, so that's nothing new.
        
           | TehShrike wrote:
           | I was excited for that proposal, but it veered off course
           | some years ago - some TC39 members have stuck to the position
           | that without member property support or async/await support,
           | they will not let the feature move forward.
           | 
           | It seems like most people are just asking for the simple
           | function piping everyone expects from the |> syntax, but that
           | doesn't look likely to happen.
        
             | packetlost wrote:
             | I don't actually see why `|> await foo(bar)` wouldn't be
             | acceptable if you must support futures.
             | 
             | I'm not a JS dev so idk what member property support is.
        
               | cogman10 wrote:
               | Seems like it'd force the rest of the pipeline to be
               | peppered with `await` which might not be desirable
               | "bar"         |> await getFuture(%);         |> baz(await
               | %);         |> bat(await %);
               | 
               | My guess is the TC committee would want this to be more
               | seamless.
               | 
               | This also gets weird because if the `|>` is a special
               | function that sends in a magic `%` parameter, it'd have
               | to be context sensitive to whether or not an `async`
               | thing happens within the bounds. Whether or not it does
               | will determine if the subsequent pipes are dealing with a
               | future of % or just % directly.
        
               | packetlost wrote:
               | It wouldn't though? The first await would... await the
               | value out of the future. You still do the syntactic
               | transformation with the magic parameter. In your example
               | you're awaiting the future returned by getFuture twice
               | and improperly awaiting the output of baz (which isn't
               | async in the example).
               | 
               | In reality it would look like:                   "bar"
               | |> await getFuture()         |> baz()         |> await
               | bat()
               | 
               | (assuming getFuture and bat are both async). You _do_
               | need | > to be aware of the case where the await keyword
               | is present, but that's about it. The above would
               | effectively transform to:                   await
               | bat(baz(await getFuture("bar")));
               | 
               | I don't see the problem with this.
        
               | porridgeraisin wrote:
               | Correct me if I'm wrong, but if you use the below syntax
               | "bar"         |> await getFuture()
               | 
               | How would you disambiguate it from your intended meaning
               | and the below:                 "bar"         |> await
               | getFutureAsyncFactory()
               | 
               | Basically, an async function that returns a function
               | which is intended to be the pipeline processor.
               | 
               | Typically in JS you do this with parens like so:
               | 
               | (await getFutureAsyncFactory())("input")
               | 
               | But the use of parens doesn't transpose to the pipeline
               | setting well IMO
        
               | packetlost wrote:
               | I don't think |> really can support applying the result
               | of one of its composite applications in general, so it's
               | not ambiguous.
               | 
               | Given this example:                   (await
               | getFutureAsyncFactory("bar"))("input")
               | 
               | the getFutureAsyncFactory function is async, but the
               | function it returns is not (or it may be and we just
               | don't await it). Basically, using |> like you stated
               | above doesn't do what you want. If you wanted the same
               | semantics, you would have to do something like:
               | ("bar" |> await getFutureAsyncFactory())("input")
               | 
               | to invoke the returned function.
               | 
               | The whole pipeline takes on the value of the last
               | function specified.
        
           | chilmers wrote:
           | It also has barely seen any activity in years. It is going
           | nowhere. The TC39 committee is utterly dysfunctional and
           | anti-progress, and will not let any this or any other new
           | syntax into JavaScript. Records and tuples has just been
           | killed, despite being cited in surveys as a major missing
           | feature[1]. Pattern matching is stuck in stage 1 and hasn't
           | been presented since 2022. Ditto for type annotations and a
           | million other things.
           | 
           | Our only hope is if TypeScript finally gives up on the broken
           | TC39 process and starts to implement its own syntax
           | enhancements again.
           | 
           | [1] https://2024.stateofjs.com/en-
           | US/usage/#top_currently_missin...
        
             | johnny22 wrote:
             | Records and Tuples weren't stopped because of tc39, but
             | rather the engine developers. Read the notes.
        
             | tkcranny wrote:
             | I wouldn't hold your breath for TypeScript introducing any
             | new supra-JS features. In the old days they did a little
             | bit, but now those features (namely enums) are considered
             | harmful.
             | 
             | More specifically, with the (also ironically gummed up in
             | tc39) type syntax [1], and importantly node introducing the
             | --strip-types option [2], TS is only ever going to look
             | more and more like standards compliant JS.
             | 
             | [1] https://tc39.es/proposal-type-annotations/
             | 
             | [2] https://nodejs.org/en/blog/release/v22.6.0
        
           | hinkley wrote:
           | All of their examples are wordier than just function chaining
           | and I worry they've lost the plot somewhere.
           | 
           | They list this as a con of F# (also Elixir) pipes:
           | value |> x=> x.foo()
           | 
           | The insistence on an arrow function is pure hallucination
           | value |> x.foo()
           | 
           | Should be perfectly achievable as it is in these other
           | languages. What's more, doing so removes all of the
           | handwringing about await. And I'm frankly at a loss why you
           | would want to put yield in the middle of one of these chains
           | instead of after.
        
         | Symmetry wrote:
         | I feel like Haskell really missed a trick by having $ not go
         | the other way, though it's trivial to make your own symbol that
         | goes the other way.
        
           | jose_zap wrote:
           | Haskell has & which goes the other way:
           | users           & map validate           & catMaybes
           | & mapM persist
        
             | Symmetry wrote:
             | I guess I'm showing how long it's been since I was a
             | student of Haskell then. Glad to see the addition!
        
             | taolson wrote:
             | Yes, `&` (reverse apply) is equivalent to `|>`, but it is
             | interesting that there is no common operator for reversed
             | compose `.`, so function compositions are still read right-
             | to-left.
             | 
             | In my programming language, I added `.>` as a reverse-
             | compose operator, so pipelines of function compositions can
             | also be read uniformly left-to-right, e.g.
             | process = map validate .> catMaybes .> mapM persist
        
               | 1-more wrote:
               | Elm (written in Haskell) uses |> and <| for pipelining
               | forwards and backwards, and function composition is >>
               | and <<. These have made it into Haskell via nri-prelude
               | https://hackage.haskell.org/package/nri-prelude (written
               | by a company that uses a lot of Elm in order to make
               | writing Haskell look more like writing Elm).
               | 
               | There is also https://hackage.haskell.org/package/flow
               | which uses .> and <. for function composition.
               | 
               | EDIT: in no way do I want to claim the originality of
               | these things in Elm or the Haskell package inspired by
               | it. AFAIK |> came from F# but it could be miles earlier.
        
         | jasperry wrote:
         | Yes, a small feature set is important, and adding the
         | functional-style pipe to languages that already have chaining
         | with the dot seems to clutter up the design space. However,
         | dot-chaining has the severe limitation that you can only pass
         | to the first or "this" argument.
         | 
         | Is there any language with a single feature that gives the best
         | of both worlds?
        
           | bnchrch wrote:
           | FWIW you can pass to other arguments than first in this
           | syntax
           | 
           | ```
           | 
           | params
           | 
           | |> Map.get("user")
           | 
           | |> create_user()
           | 
           | |> (&notify_admin("signup", &1)).() ```
           | 
           | or
           | 
           | ```
           | 
           | params
           | 
           | |> Map.get("user")
           | 
           | |> create_user()
           | 
           | |> (fn user -> notify_admin("signup", user) end).() ```
        
             | Terr_ wrote:
             | BTW, there's a convenience macro of Kernel.then/2 [0] which
             | IMO looks a little cleaner:                   params
             | |> Map.get("user")         |> create_user()         |>
             | then(&notify_admin("signup", &1))              params
             | |> Map.get("user")         |> create_user()         |>
             | then(fn user -> notify_admin("signup", user) end)
             | 
             | [0] https://hexdocs.pm/elixir/1.18.3/Kernel.html#then/2
        
         | layer8 wrote:
         | It would be even better without the `>`, though. The `|>` is a
         | bit awkward to type, and more noisy visually.
        
           | MyOutfitIsVague wrote:
           | I disagree, because then it can be very ambiguous with an
           | existing `|` operator. The language has to be able to tell
           | that this is a pipeline and not doing a bitwise or operation
           | on the output of multiple functions.
        
             | layer8 wrote:
             | Yes, I'm talking about a language where `|` would be the
             | pipe operator and nothing else, like in a shell.
             | Retrofitting a new operator into an existing language tends
             | to be suboptimal.
        
         | hinkley wrote:
         | The pipe operator relies on the first argument being the
         | subject of the operation. A lot of languages have the arguments
         | in a different order, and OO languages sometimes use function
         | chaining to get a similar result.
        
           | Terr_ wrote:
           | IIRC the usual workaround in Elixir involves be small lambda
           | that rearranges things:                   "World"         |>
           | then(&concat("Hello ", &1))
           | 
           | I imagine a shorter syntax could someday be possible, where
           | some special placeholder expression could be used, ex:
           | "World"         |> concat("Hello ", &1)
           | 
           | However that creates a new problem: If the implicit-first-
           | argument form is still permitted (foo() instead of foo(&1))
           | then it becomes confusing which function-arity is being
           | called. A human could easily fail to notice the absence or
           | presence of the special placeholder on some lines, and invoke
           | the wrong thing.
        
             | hinkley wrote:
             | Yeah I really hate that syntax and I can't even explain why
             | so I kind of blot it out, but you're right.
             | 
             | My dislike does improve my test coverage though, since I
             | tend to pop out a real method instead.
        
           | BoingBoomTschak wrote:
           | Swiss arrows ftw!
           | 
           | https://github.com/rplevy/swiss-arrows
           | https://github.com/hipeta/arrow-macros
        
         | manmal wrote:
         | I wish there were a variation that can destructure more
         | ergonomically.
         | 
         | Instead of:
         | 
         | ```
         | 
         | fetch_data()
         | 
         | |> (fn                 {:ok, val, _meta} -> val
         | :error -> "default value"
         | 
         | end).()
         | 
         | |> String.upcase()
         | 
         | ```
         | 
         | Something like this:
         | 
         | ```
         | 
         | fetch_data()
         | 
         | |>? {:ok, val, _meta} -> val
         | 
         | |>? :error -> "default value"
         | 
         | |> String.upcase()
         | 
         | ```
        
         | bradford wrote:
         | I hate to be _that guy_ , but I believe the `|>` syntax started
         | with F# before Elixir picked it up.
         | 
         | (No disagreements with your post, just want to give credit
         | where it's due. I'm also a big fan of the syntax)
        
           | ghthor wrote:
           | I turn older then f#, it's been an ML language thing for a
           | while but not sure where it first appeared
        
       | chewbacha wrote:
       | Is this pipelining or the builder pattern?
        
         | meltyness wrote:
         | Pipes and filters are considered an architectural pattern,
         | whereas Builder is a GoF OOP pattern, so yes.
        
         | Mond_ wrote:
         | "These are the same picture." (Sort of.)
        
         | ivanjermakov wrote:
         | I usually call it method chaining. Where the builder pattern
         | use it.
        
       | cutler wrote:
       | Clojure has pipeline functions -> and ->> without resorting to OO
       | dot syntax.
        
         | jolt42 wrote:
         | As well as some-> (exit on null) and cond-> (with predicates)
         | that are often handy.
        
           | joeevans1000 wrote:
           | As well as a lot of flexibility on where the result of the
           | previous step feeds into the current one.
        
           | 3036e4 wrote:
           | Both Fennel and Janet has the Clojure threading macros, with
           | -?> and -?>> for false/null checks, but not any other
           | variants as far as I know.
        
       | amelius wrote:
       | Am I the only one who thinks yuck?
       | 
       | Instead of writing: a().b().c().d(), it's much nicer to write:
       | d(c(b(a()))), or perhaps (d [?] c [?] b [?] a)().
        
         | vinceguidry wrote:
         | Why, if you don't have to, would you write the functions in
         | reverse order of when they're applied?
        
           | gloxkiqcza wrote:
           | Presumably because they've been doing so for decades so it
           | seems logical and natural in their head while the new thing
           | is new and thus unintuitive.
        
           | dboreham wrote:
           | Because it makes more sense?
        
             | Mond_ wrote:
             | Does it actually make more sense, or is it just more
             | familiar?
        
             | kortex wrote:
             | I would wager within a rounding error, all humans have a
             | lifetime of experience in following directions of the form:
             | 
             | 1. do the first step in the process
             | 
             | 2. then do the next thing
             | 
             | 3. followed by a third action
             | 
             | I struggle to think of any context outside of programming,
             | retrosynthesis in chemistry, and some aspects of reverse-
             | Polish notation calculators, where you conceive of the
             | operations/arguments last-to-first. All of which are things
             | typically encountered pretty late in one's educational
             | journey.
        
               | amelius wrote:
               | Consistency is more important. If you ever wrote:
               | 
               | a(b())
               | 
               | then you're already breaking your left-to-right/first-to-
               | last rule.
        
               | ndriscoll wrote:
               | There are some math books out there that use (x)f. My
               | understanding is (some) algebraists tried to make it a
               | thing ~60 years ago but it never really caught on.
        
               | hollerith wrote:
               | There are, but they leave out the parens and use context
               | to distinguish function application from multiplication.
        
               | regular_trash wrote:
               | This is a foolish consistency, and a contrived
               | counterexample. Consistency is not an ideal unto itself.
        
           | nh23423fefe wrote:
           | function application is right associative?
        
           | tgv wrote:
           | It's a bit snarky, but would you rather write FORTH then? So
           | instead of                    draw(line.start, line.end);
           | print(i, round(a / b));
           | 
           | you'd write                   line.start, line.end |> draw;
           | i, a, b |> div |> round |> print;
        
         | queuebert wrote:
         | It probably wouldn't hurt for languages to steal more ideas
         | from APL.
        
         | duped wrote:
         | A subtlety that I think many people overlook is that putting
         | function application in lexicographical order means that tools
         | can provide significantly better autocomplete results without
         | needing to add a magic keybinding.
        
         | adzm wrote:
         | When a b c d are longer expressions, the pipeline version looks
         | more readable especially when split on multiple lines since it
         | only has one level of indentation and you don't have to think
         | about the number of parentheses at the end.
        
       | 1899-12-30 wrote:
       | You can somewhat achieve a pipelined like system in sql by
       | breaking down your steps into multiple CTEs. YMMV on the
       | performance though.
        
         | infogulch wrote:
         | Yeah, the way to get logical pipelining in SQL without CTEs is
         | nested subqueries in the FROM clause. Unfortunately, the
         | nesting is syntactically ugly and confusing to read which is
         | basically the whole idea behind pipeline syntax.
        
       | singularity2001 wrote:
       | I tried to convince the julia authors to make a.b(c) synonymous
       | to b(a,c) like in nim (for similar reasons as in the article).
       | They didn't like it.
        
         | queuebert wrote:
         | What were their reasons?
        
           | pansa2 wrote:
           | I suspect:
           | 
           | Julia's multiple dispatch means that all arguments to a
           | function are treated equally. The syntax `b(a, c)` makes this
           | clear, whereas `a.b(c)` makes it look like `a` is in some way
           | special.
        
       | dapperdrake wrote:
       | Pipelining in software is covered by Richard C. Waters (1989a,
       | 1989b). Wrangles this library to work with JavaScript. Incredibly
       | effective. Much faster at _writing_ and composing code. And this
       | code _executes_ much faster.
       | 
       | https://dspace.mit.edu/handle/1721.1/6035
       | 
       | https://dspace.mit.edu/handle/1721.1/6031
       | 
       | https://dapperdrake.neocities.org/faster-loops-javascript.ht...
        
       | blindseer wrote:
       | This article is great, and really distills why the ergonomics of
       | Rust is so great and why languages like Julia are so awful in
       | practice.
        
         | jakobnissen wrote:
         | You mean tab completion in Rust? Otherwise, let me introduce
         | you to:                   imap(f) = x -> Iterators.map(f, x)
         | ifilter(f) = x -> Iterators.filter(f, x)         v = things |>
         | ifilter(isodd) |>             imap(do_process) |>
         | collect
        
       | hliyan wrote:
       | I always wondered how programming would be if we hadn't designed
       | the assignment operator to be consistent with mathematics, and
       | instead had it go LHS -> RHS, i.e. you perform the operation and
       | _then_ decide its destination, much like Unix pipes.
        
         | RodgerTheGreat wrote:
         | Plenty of LTR languages to choose from, especially
         | concatenative languages like Forth, Joy, or Factor.
         | 
         | The APL family is similarly consistent, except RTL.
        
         | donatj wrote:
         | TI-BASIC is like this with its store operator -. I always liked
         | it.                   10-A         A+10-C
        
         | remram wrote:
         | For function calls too? List the arguments then the function's
         | name?
        
       | TrianguloY wrote:
       | Kotlin sort of have it with let (and run)
       | a().let{ b(it) }.let{ c(it) }
        
         | hombre_fatal wrote:
         | Yeah, Kotlin's solution is nice because it's so general: you
         | can chain on to anything instead of needing everyone to
         | implement a builder pattern.
         | 
         | And it's already idiomatic unlike bolting a pipeline operator
         | onto a language that didn't start with it.
        
           | jillesvangurp wrote:
           | If you see somebody using a builder in Kotlin, they're
           | basically doing it wrong. You can usually get rid of that
           | stuff with a 1 line extension function (for example if it's
           | some Java API that's being called).                 //
           | extension function on Foo.Companion (similar to static class
           | function in Java)       fun Foo.Companion.create(block:
           | FooBuilder.() -> Unit): Foo =
           | FooBuilder().apply(block).build()            // example usage
           | val myFoo = Foo.create {         setSomeproperty("foo")
           | setAnotherProperty("bar")       }
           | 
           | Works for any Java/Kotlin API that forces you into method
           | chaining and calling build() manually. Also works without
           | extension functions. You can just call it fun createAFoo(..)
           | or whatever. Looking around in the Kotlin stdlib code base is
           | instructive. Lots of little 1/2 liners like this.
        
       | wslh wrote:
       | I also like a syntax that includes pipelining parallelization,
       | for example:
       | 
       | A
       | 
       | .B
       | 
       | .C                 || D            || E
        
         | regular_trash wrote:
         | Wouldn't this complicate variable binding? I'm unsure how to
         | think about this kinda of syntax if either D or E are expected
         | to return some kind of data instead of "fire and forget"
         | processes.
        
       | tantalor wrote:
       | > allows you to omit a single argument from your parameter list,
       | by instead passing the previous value
       | 
       | I have no idea what this is trying to say, or what it has to do
       | with the rest of the article.
        
         | delusional wrote:
         | It's getting at the essential truth that for all(?) mainstream
         | languages since object orientation and the dot syntax became a
         | thing `a.b()` implicitly includes `a` as the first argument to
         | the actual method `b(a self)`. Different languages have
         | different constructs on top of that, C++ for example includes a
         | virtual dispatch mechanism, but the one common idea of the
         | _method call_ is that the `self` pointer is passed as the first
         | argument.
        
       | 0xf00ff00f wrote:
       | First example doesn't look bad in C++23:                   auto
       | get_ids(std::span<const Widget> data)         {
       | return data                 | filter(&Widget::alive)
       | | transform(&Widget::id)                 | to<std::vector>();
       | }
        
         | inetknght wrote:
         | This is not functionally different from operator<< which
         | std::cout has taught us is a neat trick but generally a bad
         | idea.
        
           | senderista wrote:
           | Unlike the iostreams shift operators, the ranges pipe
           | operator isn't stateful.
        
         | Shorel wrote:
         | This looks awesome!
         | 
         | I'm really want to start playing with some C++23 in the future.
        
           | 0xf00ff00f wrote:
           | I cheated a bit, I omitted the namespaces. Here's a working
           | version: https://godbolt.org/z/1rE9o3Y95
        
       | RHSeeger wrote:
       | I feel like, at least in some cases, the article is going out of
       | its way to make the "undesired" look worse than it needs to be.
       | Compairing                   fn get_ids(data: Vec<Widget>) ->
       | Vec<Id> {             collect(map(filter(map(iter(data), |w|
       | w.toWingding()), |w| w.alive), |w| w.id))         }
       | 
       | to                   fn get_ids(data: Vec<Widget>) -> Vec<Id> {
       | data.iter()                 .map(|w| w.toWingding())
       | .filter(|w| w.alive)                 .map(|w| w.id)
       | .collect()         }
       | 
       | The first one would read more easily (and, since it called out,
       | diff better)                   fn get_ids(data: Vec<Widget>) ->
       | Vec<Id> {             collect(                 map(
       | filter(                         map(iter(data), |w|
       | w.toWingding()), |w| w.alive), |w| w.id))         }
       | 
       | Admittedly, the chaining is still better. But a fair number of
       | the article's complaints are about the lack of newlines being
       | used; not about chaining itself.
        
         | the_sleaze_ wrote:
         | In my eyes newlines don't solve what I feel to be the issue.
         | Reader needs to recognize reading from left->right to
         | right->left.
         | 
         | Of course this really only matters when you're 25 minutes into
         | critical downtime and a bug is hiding somewhere in these method
         | chains. Anything that is surprising needs to go.
         | 
         | IMHO it would be better to set intermediate variables with dead
         | simple names instead of newlines.
         | 
         | fn get_ids(data: Vec<Widget>) -> Vec<Id> {
         | let iter = iter(data);              let wingdings = map(iter,
         | |w| w.toWingding());              let alive_wingdings =
         | filter(wingdings, |w| w.alive);              let ids =
         | map(alive_wingdings, |w| w.id);              let collected =
         | collect(ids);              collected          }
        
           | trealira wrote:
           | > Reader needs to recognize reading from left->right to
           | right->left.
           | 
           | Yeah, I agree. The problem is that you have to keep track of
           | nesting in the middle of the expression and then unnest it at
           | the end, which is taxing.
           | 
           | So, I also think it could also read better written like this,
           | with the arguments reversed, so you don't have to read it
           | both ways:                 fn get_ids(data: Vec<Widget>) ->
           | Vec<Id> {           collect(              map(|w| w.id,
           | filter |w| w.alive,                    (map(|w|
           | w.toWingding(), iter(data)))))       }
           | 
           | That's also what they do in Haskell. The first argument to
           | map is the mapping function, the first argument to filter is
           | the predicate function, and so on. People will often just
           | write the equivalent of:                 getIDs = map getID .
           | filter alive . map toWingDing
           | 
           | as their function definitions, with the argument omitted
           | because using the function composition operator looks neater
           | than using a bunch of dollar signs or parentheses.
           | 
           | Making it the second argument only makes sense when functions
           | are written after their first argument, not before, to
           | facilitate writing "foo.map(f).filter(y)".
        
         | TOGoS wrote:
         | They did touch on that.
         | 
         | > You might think that this issue is just about trying to cram
         | everything onto a single line, but frankly, trying to move away
         | from that doesn't help much. It will still mess up your git
         | diffs and the blame layer.
         | 
         | Diff will still be terrible because adding a step will change
         | the indentation of everything 'before it' (which, somewhat
         | confusingly, are below it syntactically) in the chain.
        
           | RHSeeger wrote:
           | Diff can ignore whitespace, so not really an issue. Not _as_
           | nice, but not really a problem.
        
         | tasuki wrote:
         | Oh wow, are we living in the same universe? To me the one-line
         | example and your example with line breaks... they just... look
         | about the same?
         | 
         | See how adding line breaks still keeps the `|w| w.alive` _very
         | far_ from the `filter` call? And the `|w| w.id` _very far_ from
         | the `map` call?
         | 
         | If you don't have the pipeline operator, please at least format
         | it something like this:                   fn get_ids(data:
         | Vec<Widget>) -> Vec<Id> {             collect(
         | map(                     filter(                         map(
         | iter(data),                             |w| w.toWingding()
         | ),                         |w| w.alive                     ),
         | |w| w.id                 )             )         }
         | 
         | ...which is still absolutely atrocious _both_ to write _and_ to
         | read!
         | 
         | Also see how this still reads fine despite being one line:
         | fn get_ids(data: Vec<Widget>) -> Vec<Id> {
         | data.iter().map(|w| w.toWingding()).filter(|w| w.alive).map(|w|
         | w.id).collect()         }
         | 
         | It's not about line breaks, it's about the order of applying
         | the operations, and about the parameters to the operations
         | you're performing.
        
           | RHSeeger wrote:
           | > It's not about line breaks, it's about the order of
           | applying the operations
           | 
           | For me, it's both. Honestly, I find it much less readable the
           | way you're split it up. The way I had it makes it very easy
           | for me to read it in reverse; map, filter, map, collect
           | 
           | > Also see how this still reads fine despite being one line
           | 
           | It doesn't read fine, to me. I have to spend mental effort
           | figuring out what the various "steps" are. Effort that I
           | don't need to spend when they're split across lines.
           | 
           | For me, it's a "forest for the trees" kind of thing. I like
           | being able to look at the code casually and see what it's
           | doing at a high level. Then, if I want to see the details, I
           | can look more closely at the code.
        
       | guerrilla wrote:
       | This is just super basic functional programming. Seems like we're
       | taking the long way around...
        
         | Mond_ wrote:
         | Have you read the article? This isn't about functional vs.
         | imperative programming, it's (if anything) about two different
         | ways to write functional code.
        
           | guerrilla wrote:
           | Keywords "super basic". You learn this in a "my first
           | Haskell" tutorials. Seems tortured in whatever language that
           | is though.
        
       | duped wrote:
       | A pipeline operator is just partial application with less power.
       | You should be able to bind any number of arguments to any places
       | in order to create a new function and "pipe" its output(s) to any
       | other number of functions.
       | 
       | One day, we'll (re)discover that partial application is actually
       | incredibly useful for writing programs and (non-Haskell)
       | languages will start with it as the primitive for composing
       | programs instead of finding out that it would be nice later, and
       | bolting on a restricted subset of the feature.
        
         | choult wrote:
         | ... and then recreate the scripting language...
        
           | stogot wrote:
           | I was just thinking does this not sound like a shell
           | language? Using | instead of .function()
        
         | dayvigo wrote:
         | Sure. But how do you write that in a way that is expressive,
         | terse, and readable all at once? Nothing beats x | y | z or (->
         | x y z). The speed of both writing and reading (and
         | comprehending), the sheer simplicity, is what makes pipelining
         | useful in the first place.
        
       | weinzierl wrote:
       | I suffer from (what I call) bracket claustrophobia. Whenever
       | brackets get nested too deep I makes me uncomfortable. But I
       | fully realize that there are people who are the complete
       | opposite. Lisp programmers are apparently as claustrophil as cats
       | and spelunkers.
        
         | monsieurbanana wrote:
         | Forget the parenthesis, embrace the automatic indentation and
         | code source manipulations that only perfectly balanced
         | homoiconic expressions can give you.
        
       | pxc wrote:
       | Maybe it's because I love the Unix shell environment so much, but
       | I also really love this style. I try to make good use of it in
       | every language I write code in, and I think it helps make my
       | control flow very simple. With lots of pipelines, and few
       | conditionals or loops, everything becomes very easy to follow.
        
       | taeric wrote:
       | A thing I really like about pipelines in shell scripts, is all of
       | the buffering and threading implied by them. Semantically, you
       | can see what command is producing output, and what command is
       | consuming it. With some idea of how the CPU will be split by
       | them.
       | 
       | This is far different than the pattern described in the article,
       | though. Small shame they have come to have the same name. I can
       | see how both work with the metaphor; such that I can't really
       | complain. The "pass a single parameter" along is far less
       | attractive to me, though.
        
       | tpoacher wrote:
       | pipelines are great IF you can easily debug them as easily as
       | temp variable assignments
       | 
       | ... looking at you R and tidyverse hell.
        
       | layer8 wrote:
       | The one thing that I don't like about pipelining (whether using a
       | pipe operator or method chaining), is that assigning the result
       | to a variable goes in the wrong direction, so to speak. There
       | should be an equivalent of the shell's `>` for piping into a
       | variable as the final step. Of course, if the variable is being
       | declared at the same time, whatever the concrete syntax is would
       | still require some getting used to, being "backwards" compared to
       | regular assignment/initialization.
        
         | jiggunjer wrote:
         | Exists in R: Mydata %>% myfun -> myresult
        
       | bluSCALE4 wrote:
       | Same. The sad part is that pipelining seems to be something AI is
       | really good at so I'm finding myself writing less of it.
        
       | joeevans1000 wrote:
       | Clojure threading, of course.
        
       | flakiness wrote:
       | After seeing LangChain abusing the "|" operator overload for
       | pipeline-like DSL, I followed the suite at work and I loved it.
       | It's especially good when you use it in a notebook environment
       | where you literally build the pipeline incrementally through
       | repl.
        
       | true_blue wrote:
       | That new Rhombus language that was featured here recently has an
       | interesting feature where you can use `_` in a function call to
       | act as a "placeholder" for an argument. Essentially it's an easy
       | way to partially apply a function. This works very well with
       | piping because it allows you to pipe into any argument of a
       | function (including optional arguments iirc) rather than just the
       | first like many pipe implementations have. It seems really cool!
        
         | gdw2 wrote:
         | Sounds like Clojure's as-> macro
         | (https://clojuredocs.org/clojure.core/as-%3E).
        
       | otsukare wrote:
       | I wish more languages would aim for infix functions (like Haskell
       | and Kotlin), rather than specifically the pipe operator.
        
       | Straw wrote:
       | Lisp macros allow a general solution to this that doesn't just
       | handle chained collection operators but allows you to decide the
       | order in which you write any chain of calls.
       | 
       | For example, we can write: (foo (bar (baz x))) as (-> x baz bar
       | foo)
       | 
       | If there are additional arguments, we can accommodate those too:
       | (sin (* x pi) as (-> x (* pi) sin)
       | 
       | Where expression so far gets inserted as the first argument to
       | any form. If you want it inserted as the last argument, you can
       | use ->> instead:
       | 
       | (filter positive? (map sin x)) as (->> x (map sin) (filter
       | positive?))
       | 
       | You can also get full control of where to place the previous
       | expression using as->.
       | 
       | Full details at https://clojure.org/guides/threading_macros
        
         | gleenn wrote:
         | I find the threading operators in Clojure bring much joy and
         | increase readability. I think it's interesting because it makes
         | me actually consider function argument order much more because
         | I want to increase opportunities to use them.
        
       | wavemode wrote:
       | > At this point you might wonder if Haskell has some sort of
       | pipelining operator, and yes, it turns out that one was added in
       | 2014! That's pretty late considering that Haskell exists since
       | 1990.
       | 
       | The tone of this (and the entire Haskell section of the article,
       | tbh) is rather strange. Operators aren't special syntax and they
       | aren't "added" to the language. Operators are just functions that
       | by default use infix position. (In fact, any function can be
       | called in infix position. And operators can be called in prefix
       | position.)
       | 
       | The commit in question added & to the prelude. But if you wanted
       | & (or any other character) to represent pipelining you have
       | always been able to define that yourself.
       | 
       | Some people find this horrifying, which is a perfectly valid
       | opinion (though in practice, when working in Haskell it isn't
       | much of a big deal if you aren't foolish with it). But at least
       | get the facts correct.
        
       | relaxing wrote:
       | These articles never explain what's wrong with calling each
       | function separately and storing each return value in an
       | intermediate variable.
       | 
       | Being able to inspect the results of each step right at the point
       | you've written it is pretty convenient. It's readable. And the
       | compiler will optimize it out.
        
       | amai wrote:
       | Pipelining looks nice until you have to debug it. And exception
       | handling is also very difficult, because that means to add forks
       | into your pipelines. Pipelines are only good for programming the
       | happy path.
        
         | mpalmer wrote:
         | At the risk of over generalized pronouncements, ease of
         | debugging is usually down to how well-designed your tooling
         | happens to be. Most of the time the framework/language does
         | that for you, but it's not the only option.
         | 
         | And for exceptions, why not solve it in the data model, and
         | reify failures? Push it further downstream, let your pipeline's
         | nodes handle "monadic" result values.
         | 
         | Point being, it's always a tradeoff, but you can usually lessen
         | the pain more than you think.
         | 
         | And that's without mentioning that a lot of "pipelining" is
         | pure sugar over the same code we're already writing.
        
         | bergen wrote:
         | Depends on the context - in a scripting language where you have
         | some kind of console you just don't copy all lines, and see
         | what each pipe does one after another. This is pretty straight
         | forward. (Not talking about compiled code though)
        
         | bsder wrote:
         | Pipelining is also nice until you _have_ to use it for
         | everything because you can 't do alternatives (like default
         | function arguments) properly.
         | 
         | Rust chains _everything_ because of this. It 's often
         | unpleasant (see: all the Rust GUI toolkits).
        
         | w4rh4wk5 wrote:
         | Yes, certainly!
         | 
         | I've encountered and used this pattern in Python, Ruby,
         | Haskell, Rust, C#, and maybe some other languages. It often
         | feels nice to write, but reading can easily become difficult --
         | especially in Haskell where obscure operators can contain a lot
         | of magic.
         | 
         | Debugging them interactively can be equally problematic,
         | depending on the tooling. I'd argue, it's commonly harder to
         | debug a pipeline than the equivalent imperative code and, that
         | in the best case it's equally hard.
        
         | eikenberry wrote:
         | Pipelining simplifies debugging. Each step is obvious and it is
         | trivial to insert logging between pipeline elements. It is
         | easier to debug than the patterns compared in the article.
         | 
         | Exception handing is only a problem in languages that use
         | exceptions. Fortunately there are many modern alternatives in
         | wide use that don't use exceptions.
        
         | rusk wrote:
         | Established debugging tools and logging rubric are not suitable
         | for debugging heavily pipelined code. Stack traces, debuggers
         | rely heavily on line based references which are less useful in
         | this style and can make diagnostic practices feel a little
         | clumsy.
         | 
         | The old adage of not writing code so smart you can't debug it
         | applies here.
         | 
         | Pipelining runs contrary enough to standard imperative
         | patterns. You don't just need a new mindset to write code this
         | way. You need to think differently about how you structure your
         | code overall and you need different tools.
         | 
         | That's not to say that doing things a different way isn't
         | great, but it does come with baggage that you need to be in a
         | position to carry.
        
       | dpc_01234 wrote:
       | I think there's a language syntax to be invented that would make
       | everything suffix/pipeline-based. Stack based languages are kind
       | of there, but I don't think exactly the same thing.
       | 
       | BTW. For people complaining about debug-ability of it:
       | https://doc.rust-lang.org/std/iter/trait.Iterator.html#metho...
       | etc.
        
       | huyegn wrote:
       | I liked the pipelining syntax so much from pyspark and linq that
       | I ended up implementing my own mini linq-like library for python
       | to use in local development. It's mainly used in quick data
       | processing scripts that I run locally. The syntax just makes
       | everything much nicer to work with.
       | 
       | https://datapad.readthedocs.io/en/latest/quickstart.html#ove...
        
         | michalsustr wrote:
         | Looks really neat, might use that in my work!
        
       | neuroelectron wrote:
       | I really like the website layout. I'm guessing that they're
       | optimizing for Kindle or other e-paper readers.
        
         | pixelmeister wrote:
         | I recognized this site layout from a past HN post about a solar
         | powered website. Check out their about page. It links to the
         | source for the style that explains why it looks the way it
         | does. Not to spoil it, but it's not for e-readers :)
        
       | okayishdefaults wrote:
       | Surprised that the term "tacit programming" wasn't mentioned once
       | in the article.
       | 
       | Point-free style and pipelining were meant for each other.
       | https://en.m.wikipedia.org/wiki/Tacit_programming
        
       | vitus wrote:
       | I think the biggest win for pipelining in SQL is the fact that we
       | no longer have to explain that SQL execution order has nothing to
       | do with query order, and we no longer have to pretend that we're
       | mimicking natural language. (That last point stops being the case
       | when you go beyond "SELECT foo FROM table WHERE bar LIMIT 10".)
       | 
       | No longer do we have to explain that expressions are evaluated in
       | the order of FROM -> JOIN -> ON -> SELECT -> WHERE -> GROUP BY ->
       | HAVING -> ORDER BY -> LIMIT (and yes, I know I'm missing several
       | other steps). We can simply just express how our data flows from
       | one statement to the next.
       | 
       | (I'm also stating this as someone who has yet to play around with
       | the pipelining syntax, but honestly anything is better than the
       | status quo.)
        
         | _dark_matter_ wrote:
         | You flipped SELECT and WHERE, which probably just solidifies
         | your point. I can't count the number if times I've seen this
         | trip up analysts.
        
       | ZYbCRq22HbJ2y7 wrote:
       | Its nice sugar, but pretty much any modern widely used language
       | supports "pipelining", just not of the SML flavor.
        
       | jongjong wrote:
       | Pipelining is great. Currying is horrible. Though currying
       | superficially looks similar to pipelining.
       | 
       | One difference is that currying returns an incomplete result
       | (another function) which must be called again at a later time. On
       | the other hand, pipelining usually returns raw values. Currying
       | returns functions until the last step. The main philosophical
       | failure of currying is that it treats logic/functions as if they
       | were state which should be passed around. This is bad. Components
       | should be responsible for their own state and should just talk to
       | each other to pass plain information. State moves, logic doesn't
       | move. A module shouldn't have awareness of what tools/logic other
       | modules need to do their jobs. This completely breaks the
       | separation of concerns principle.
       | 
       | When you call a plumber to fix your drain, do you need to provide
       | them with a toolbox? Do you even need to know what's inside their
       | toolbox? The plumber knows what tools they need. You just show
       | them what the problem is. Passing functions to another module is
       | like giving a plumber a toolbox which you put together by
       | guessing what tools they might need. You're not a plumber, why
       | should you decide what tools the plumber needs?
       | 
       | Currying encourages spaghetti code which is difficult to follow
       | when functions are passed between different modules to complete
       | the currying. In practice, if one can design code which gathers
       | all the info it needs before calling the function once; this
       | leads to much cleaner and much more readable code.
        
       | raggi wrote:
       | > (This is not real Rust code. Quick challenge for the curious
       | Rustacean, can you explain why we cannot rewrite the above code
       | like this, even if we import all of the symbols?)
       | 
       | Um, you can:
       | #![feature(import_trait_associated_functions)]             use
       | Iterator::{collect, map, filter};                          fn
       | get_ids2(data: Vec<usize>) -> Vec<usize> {
       | collect(map(filter(<[_]>::iter(&data), |v| ...), |v| ...))
       | }
       | 
       | and you can because it's lazy, which is also the same reason you
       | can write it the other way.. in rust. I think the author was
       | getting at an ownership trap, but that trap is avoided the same
       | way for both arrangements, the call order is the same in both
       | arrangements. If the calls were actually a pipeline (if collect
       | didn't exist and didn't need to be called) then other
       | considerations show up.
        
       | XorNot wrote:
       | Every example of why this is meant to be good is contrived.
       | 
       | You have a create_user function that doesn't error? Has no
       | branches based on type of error?
       | 
       | We're having arguments over the best way break these over
       | multiple lines?
       | 
       | Like.. why not just store intermediate results in variables?
       | Where our branch logic can just be written inline? And then the
       | flow of data can be very simply determined by reading top to
       | bottom?
        
       ___________________________________________________________________
       (page generated 2025-04-21 23:00 UTC)