Module design: When to use a pipeline and when to fold a list?

From the package documentation of List:

foldl : (a -> b -> b) -> b -> List a -> b

Reduce a list from the left.

foldl (+)  0  [1,2,3] == 6
foldl (::) [] [1,2,3] == [3,2,1]

So foldl step state [1,2,3] is like saying:

state
  |> step 1
  |> step 2
  |> step 3

foldr : (a -> b -> b) -> b -> List a -> b

Reduce a list from the right.

foldr (+)  0  [1,2,3] == 6
foldr (::) [] [1,2,3] == [1,2,3]

So foldr step state [1,2,3] is like saying:

state
  |> step 3
  |> step 2
  |> step 1

  

So I was wondering, from a design perspective, when is it better to have the user of an API plug a list into some wrapper for foldl, and when is it better to have them use a pipeline like json-decode-pipeline?

Thanks

With a foldl/foldr the step function is a -> b -> b.
With the pipeline the step function is a -> b -> c.

In other words, in the fold case the type of the last argument is the type of the return. In the pipe approach, it can be something else. Usually it is something that can take the first argument and produce the return.

Thanks for the reply!

Sorry for not being clearer—the main thing I wonder is, in the case that either implementation is equivalent, which design would a user prefer?

If I can express something as a fold I will always express it as a fold.

Maybe there is something I miss here. If you can give a realistic example I (or someone else) could provide more insight.

Something like this? https://ellie-app.com/4TV3pTrBjZYa1

Would you prefer:

test : Something
test =
    default
        |> field1 "foo"
        |> field2 5
        |> field3 0.5

or:

test2 : Something
test2 =
    build
        [ field1 "foo"
        , field2 5
        , field3 0.5
        ]

This is a very good example as it illustrates the conflict very well.

I have a slight preference for the pipeline version in this case because the “things” are heterogeneous but this is only a slight preference as my mind cannot ignore the fact that Something is a record and that I would always write that using Elm syntax.

test : Something
test =
    { default
        | field1 = "foo"
        , field2 = 5
        , field3 = 0.5
    }

Later edit, if you want to mimic the elm/html API, you could go for the list version.


type alias Options = 
    { foo : String 
    , bar : Int 
    }

type alias Attr = Options -> Options 

foo : String -> Attr 
foo theFoo options = 
     { options | foo = theFoo } 


apiEndpoint : List (Attr) -> SomeResult 
apiEndpoint attrs = 
    let 
        options = List.fold identity defaultOptions attrs 
    in 
    ...

Thanks for the example @Herteby, it seems like exactly what I’m interested in.

One thing I notice is that the pipeline exposes the default, which might be e g. an empty structure. The fields build it up in incremental steps. This approach seems pretty intuitive to me.

In the List implementation, the user offers an order, but that may not be the actual order the operations are applied, in case some sequences are less efficient or illegal, they can be rearranged behind the scenes. Additionally, the syntax seems more convenient if all the steps are the same. And the user can do convenient operations to this List, like mapping, before it’s sent through the api endpoint build.

Does anyone else have any thoughts on this or other things they notice?

If I can express something as a fold I will always express it as a fold

Why is that? Are there more alternatives, other than folding and a pipeline, that I haven’t considered?

Code is a liability and the semantics of fold are very clear to me. If I can get by writing less function code and reuse one of the already defined functions, I will do that. In any case, this is a heuristic. Of course I break my own rules when I see better alternatives but these better alternatives arise from the specifics of the types involved.

Just look at the API of the libraries hosted on the elm organization. Read Evan’s Design Process.

1 Like

Great response, really appreciate the references! I’m definitely persuaded with the principle of consistent semantics.

Can you give an example of something I should look for in those links?

Cheers

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