[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()
|
| |> (¬ify_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(¬ify_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)