Hacker News new | ask | show | jobs
by dnautics 1980 days ago
The pipe operator seems silly until you learn to pipe into IO.inspect. for vscoders I use this snippet (https://slickb.it/bits/70) which also labels with the line numbers, meaning you can multiline select a bunch of lines in your pipeline and when you're done ninja them out with another multiline select (usually line numbers have the same number of characters around each other). In combination with triggering single tests, it's invaluable for isolating errors in code.

In action: https://youtu.be/WMYc3VzOSpg&t=23m20s

I suppose the problem with this snippet is that it keeps you from using more powerful tools like tracer and recon.

3 comments

I think OP's point about building huge systems with pipes is well taken. They can become confusing when the break and over-use can be a code smell.

Their power, as you point it, is in the ability to jump into any statement and add another action with minimal syntax. It's extremely useful while iterating on development because you can quickly check that something works (or check on what is happening).

I think it also encourages a declarative style of programming, so that's a win if you ask me, especially for large systems, though I can't measure this.
If you’re chaining more than three or four things together I think you’re definitely doing something wrong but I’m not sure I’ve ever seen someone do that in Elixir.
I disagree. I think a common antipattern is piping only one thing.

This is good and readable:

    nuclear_missile
    |> open_hatch()
    |> open_fuel_lines()
    |> fire_thrusters(:all)
    |> configure_gps(%Coordinates{...})
This is unnecessary:

    word_count = 
      words
      |> Enum.count()
This is better:

    word_count = Enum.count(words)
Pipes read like a list of bullet points. We can easily deal with up to a dozen bullet points, but a single bullet point is bad grammar style.
the entire ecto api is built upon chaining statements together. I have a few queries that are 8-10 statements long.
Good point, I don’t use Ecto so I wasn’t aware of this.
Not an antipattern for nimble_parsec: https://github.com/ityonemo/zigler/blob/fe845a9fbbfef92da8ab...

Plus think of how much easier that pipe makes it for you to understand what is going on.

That’s a very beautiful usage of it. I hadn’t thought of it as a way to construct DSLs, but it makes a lot of sense here.
Looks like Haskell-like do notation would be helpful.
do you feel like the code presented is broken? I don't. Could it be better? Maybe... But how much? Is it worth a whole new pattern to learn?
My longest pipe chain in Elixir so far (with ~1 year of usage) is this[1], where I build a GraphQL context from an HTTP session. I personally think it's clean enough, but your comment made me wonder if it can be better.

[1] https://github.com/RodrigoLeiteF/craftup/blob/main/backend/l...

Well I believe that no pipe is better than a a handicapped pipe operator.

The Elixir implementation inverts the classical order of piping last in functional languages to the detriment of it. IMO a language should either support pipe last AND currying by default or supply a multitude of thread operators like Clojure does (->, ->>, as->, etc). Elixir's is just middle-of-the-road-weird.

Is it to its detriment? What's really at stake here? Elixir's pipe operator takes a stance, and I love that. It enforces consistency. I feel it's a lot better than having the "sometimes first, sometimes last" that Erlang and Clojure have.
IME it is. Many Elixir apis end up forcing an unnatural parameter order just so that the entire body can be piped through.

Clojure is way more consistent in this regard:

  - thread first (->) when operating on maps.
  - thread last (->>) when operating on sequences.
  - as-> "choose your own adventure".
Huh? Almost always the parameter order is "the type of the module first" (except builders bla bla). This is super easy to remember, and this convention also encourages good code organization and module naming.

The only place where pipe order bristles is Enum.reduce, but I'd bet if I counted I'd have wanted it the normal way more often than the backwards way.

I just gave reduce as an example in my response without reading yours first. Can you explain why it would be more desirable to have it last in reduce? I have never felt this pain though, again, don't come from a functional background.
The easiest way to say it is:

often you want to apply a list of actions to an object.

This is a "backward reduce".

I don't feel it often, but when I do, it's such a bummer.

I don't have a functional background so I can only empathize that only being allowed to pipe first is not what folks are used to. I am having trouble understanding your argument, however, that includes both "consistent" and "choose your own adventure". I also don't buy that, other than simply being used to it, that piping last to sequences is more "natural". "reduce a into b with c" or "reduce value a into value b using function c" reads perfectly natural to me. I'm happy to hear arguments otherwise.
You’re missing the crux of the argument: auto-currying. In languages like Elm and Ocaml all functions are single arity which means a multi-arity function is just a partially-applied single arity function thus you can treat all functions as being single arity. This also means the pipe operator can be implemented as a higher order function being, as such, first class. Languages without auto-currying have to resort to macros.

Clojure also doesn’t have auto currrying, but makes up for it by giving you 6 variants and creating a consistent default library for sequences and associative structures.

So, the pipe first operator is just a dirty hack.

I'm familiar with auto-currying from Haskell I've never worked seriously in a language that has it. But yes, that makes sense! I never thought about that.

I'm a bit stuck on your Clojure example still, though. If a language doesn't have auto-currying (or even currying at all in Elixir's case), why does the argument order matter? Whether it's a List or a Map, what does it matter if it's passed first instead of last?

I dislike uppercasing.

I wished Elixir would’ve used “io” instead of “IO”.