Discussion: How much to pipeline

I really like |> pipelines. My brain now recognizes the a (b |> c) pattern and is tempted to convert it into b |> c |> a.

But sometimes I’m not sure if that’s actually better. For example:

Element.column [ Element.spacing 8 ]
    (items
        |> List.map viewItem
    )

Refactoring that to:

items
    |> List.map viewItem
    |> Element.column [ Element.spacing 8 ]

… while beautiful, it feels a bit like writing HTML like this (invalid HTML, I know):

<div>
  {{ loop viewItem items }}
</div class="spacing-8">

I kind of like when going through a view function is like going through the HTML inspector in the browser. Pipelines stray away from that a bit.

|> keeps execution order, but reverses structure order.
<| keeps structure order, but reverses execution order.

Anyone else thought about this?

3 Likes

Personally, I love to use the |> pipe as much as possible, even in the view.
Something like

items
    |> List.map viewItem
    |> Element.column [ Element.spacing 8 ]

Seems very normal to me.

I also use <| but rather rarely, trying to keep it at a minimum. I try to use it only for math or for type conversion:

Element.spacing <| toInt <| 17 / 4

6 Likes

I tend to follow your first snippet. I share your sentiment about wanting view functions to feel more like the resulting HTML. In general, my opinion is to use |> for data transformations and use parentheses and the occasional <| for view logic. It might feel even better if you just pull the items |> ... part into its own value so that you don’t have to use the parens at all. I’ve found that writing view functions that way helps me and my team understand the HTML hierarchy better and visualize the results.

2 Likes

I follow a few rules since it’s so easy to get overzealous with pipelining. I’m gonna call these pizza rules here for fun.

  1. I only pizza if there are two or more values in the chain
  2. If the code is short enough to be subjectively fine on one line, I get suspicious and probably will refactor it to use parens
  3. I try to never pizza in HTML code[1]
  4. I never use left pizza except when I’m avoiding parens around a function (in, say, elm-test tests)

[1]: I gave a talk at Elm in the Spring where I recommended people do that and now I kinda regret making such a strong recommendation! I think it’s OK but the HTML-like pattern is also fine and has well-understood syntax and semantics such that it’s maybe a better pattern. Still depends highly on the team.

5 Likes

I only pizza if there are two or more values in the chain

I think error messages are better when you do someList |> List.map (\item -> whatever) rather than List.map (\item -> whatever) someList. It’s almost always the lambda that is wrong so I like when it is marked as the error rather than someList.

-- TYPE MISMATCH - src/Example.elm

This function cannot handle the argument sent through the (|>) pipe:

182|     someList |> List.map (\( _, x ) -> x + 1)
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The argument is:

    List number

But (|>) is piping it to a function that expects:

    List ( a, number )

Hint: Only Int and Float values work as numbers.

-- TYPE MISMATCH - src/Example.elm

The 2nd argument to `map` is not what I expect:

186|     List.map (\( _, x ) -> x + 1) someList
                                       ^^^^^^^^
This `someList` value is a:

    List number

But `map` needs the 2nd argument to be:

    List ( a, number )

Hint: I always figure out the argument types from left to right. If an argument
is acceptable, I assume it is “correct” and move on. So the problem may actually
be in one of the previous arguments!

Hint: Only Int and Float values work as numbers.
3 Likes

My rule a thumb is

For non view code:

  • Use <| or parens for one liners
  • Use |> when more than 1 function is applied to a value

For view code:

  • Favour <| for building html hierarchies (I like the code to fit the html structure as far as possible)
  • Only use |> when assigning values in let blocks, never in the body of the let or any other expressions. Put another way, I only use |> to calculate values which are then used to express the html.
4 Likes

Another thing to think about … do you pipeline the first call in a chain?

Do you use |> only to avoid parens:

Dict.get "content-length" metadata.headers
    |> Maybe.andThen String.toInt
    |> Result.fromMaybe (BadHeader "Content-Length header not found or invalid")

Or do you go “all the way”?

metadata.headers
    |> Dict.get "content-length"
    |> Maybe.andThen String.toInt
    |> Result.fromMaybe (BadHeader "Content-Length header not found or invalid")
2 Likes

Since pipes and parentheses are substitutes, it seems like teams have to agree on one or the other to avoid having different ways of doing the same thing. And, I think parentheses usually win because everyone is familiar with them, while only a smaller and more hardcore group is addicted to the pizza operators.

So, I have wondered, what would that look like if a team went 100% pizza. And “No parentheses” was their style. What if Elm removed parentheses? Could it work?

1 Like

If there were no parenthesis, let statements would be used more. Might be a good thing but things like map3 wouldn’t work well with this

1 Like

To me, choosing between <| and |> is the crux of it all, and not so much whether or not I use them.

favoring <| means that I read function application in the same direction whether I use parenthesis or pipes. This is very important for me, as I won’t have to mentally switch reading-direction when looking at function-applications.

I get that |> feels natural, but I don’t like the trade-off.

I don’t think it could work because even with pizzas, you still need parenthesis sometimes. For example, I recently wrote a parsing package, and as I cannot export |. and |=, I have to use |> ignore or |> keep, leading to code like this:

PR.succeed identity
    |> PR.ignore PR.spaces
    |> PR.keep (PR.int ExpectingInt InvalidNumber |> PR.map Just)
    |> PR.ignore PR.spaces
    |> PR.ignore (PR.symbol "," ExpectingComma |> PR.optional ())
    |> PR.ignore PR.spaces
    |> PR.forwardOrSkip Nothing [ "," ] ExpectingSpace Discarded

to work around having no parenthesis, I would need to use more let..in blocks:

let 
    parseInt = 
        PR.int ExpectingInt InvalidNumber
            |> PR.map Just

    parseComma = 
        PR.symbol "," ExpectingComma 
            |> PR.optional ()
in
PR.succeed identity
    |> PR.ignore PR.spaces
    |> PR.keep parseInt
    |> PR.ignore PR.spaces
    |> PR.ignore parseComma
    |> PR.ignore PR.spaces
    |> PR.forwardOrSkip Nothing [ "," ] ExpectingSpace Discarded

Maybe that is actually more readable, and perhaps if I was tidying up some code I might even do that. But I could see it being very annoying at the time when you are writing a pipeline, as usually at that point you are just trying to sequence a load of stuff together in order, and don’t want the cognitive interruption of having to create another let..in block (and then delete it again, when you realize you didn’t need it, and then put it back again when you realize you really did…).

I do agree pipelines can be harder to read sometimes, but so handy when you are just trying to figure out something complicated that needs to combine sequencing and transformation.

1 Like

@rupert Off topic question – do you miss |. and |= in your parsing example? Since you can’t define your own operators I see you use |> PR.ignore and |> PR.keep instead. (See Chat about elm/parser operators |. and |= for my previous topic about this.)

1 Like

More use of let definitions is probably a better thing much of the time. How well it works, however, depends on how well one names the local definitions. That, of course, then touches on one of the two hard problems in computer programming: cache invalidation, naming things, and off-by-one errors.

Mark

1 Like

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.