What is the best way to write code that involves long chains of .map
and .andThen
with anonymous lambdas in Elm? Such code might arise when you are doing a lot of IO with tasks, or composing lots of functions that produce a Result
or Maybe
.
Elm does not have ‘do’ notation like Haskell, so I look instead for a good idiomatic Elm style of coding that is explicit and readable.
There is a previous discussion on this topic to be found here:
The OP there gives this as an example of code that is starting to get hard to read in Elm:
extractTag : String -> Result Parser.Problem ( Tag, String )
extractTag str =
Parser.run openTagParser str
|> Result.andThen
(\( name, attributes, afterOpenTag ) ->
Parser.run (insideStringToParser name) ("<" ++ name ++ ">" ++ afterOpenTag)
|> Result.map (\( inside, rest ) ->
( { name = name, attributes = attributes, inside = inside }, rest )
)
)
The problems with this code is that since arguments to the lambda functions are needed deeper within inner lambda functions, the .andThen
and .map
calls end up nested inside each other. When elm-format is applied to this, it creates a deeply nested structure that pushes everything further to the right.
Code I am working with currently is pushing stuff right off the RHS of page! and that is just unworkable with Elm.
Here is my attempt to write this more cleanly:
{-| Extracts the first tag after the opening tag.
-}
extractTag : String -> Result (List Parser.DeadEnd) ( Tag, String )
extractTag str =
Parser.run openTagParser str
|> Result.andThen extractTagInner
|> Result.map toTagAndRemainder
{-| Extracts a tag and attributes from inside a set of tag markers.
-}
extractTagInner :
{ name : String, attributes : String, afterOpenTag : String }
-> Result (List Parser.DeadEnd) { name : String, attributes : String, inside : Bool, rest : String }
extractTagInner { name, attributes, afterOpenTag } =
Parser.run (insideStringToParser name) ("<" ++ name ++ ">" ++ afterOpenTag)
|> Result.map
(\{ inside, rest } ->
{ name = name
, attributes = attributes
, inside = inside
, rest = rest
}
)
{-| Converts the results of parsing a tag into a tuple of tag and remaining unparsed text.
-}
toTagAndRemainder :
{ name : String, attributes : String, inside : Bool, rest : String }
-> ( Tag, String )
toTagAndRemainder { name, attributes, inside, rest } =
( { name = name
, attributes = attributes
, inside = inside
}
, rest
)
It is more work to do it this way, but things I like about it are:
- There is a single pipeline which does not nest to ever deeper levels at the top.
- The functions being pipelined have descriptive names, making the entire pipeline readable as a sequence - what does this do? Oh, it opens a tag, extracts the inner tag, and yields a tag and remainder, just like the pipeline says.
- When params must pass through, I pass them through as record fields, so that they are named.
In general, when I have something like this:
thing
|> Thing.andThen (\outer -> f outer |> ThingCons
|> Thing.andThen (\inner -> g outer inner |> ThingCons)
)
I am turning it into something like this:
thing
|> Thing.andThen (\outer -> { inner = f outer, outer = outer} |> ThingCons)
|> Thing.andThen (\{inner, outer} -> g outer inner |> ThingCons)
And then breaking out the anonymous lambdas into named top-level functions, so that I can have good names and somewhere to put a comment. It is quite a verbose style, so I wonder what anyone thinks about that?
There is this package, which lets you collect the passthrough variables into nested tuples. It seems aimed at letting you continue to work with anonymous lambda functions but break up the nesting into a flatter pipeline:
For example:
computeAnswer : Maybe Int
computeAnswer =
getA
|> Maybe.andCollect getB
|> Maybe.andCollect (\( a, b ) -> getC a b)
|> Maybe.andThen (\( ( a, b ), c ) -> solve a b c)
What is your approach to doing this?