Elm List.map Fun
Let’s play with List.map in Elm. As a reminder, List.map’s annotation is
map : (a -> b) -> List a -> List b
It takes a function (that takes something and returns something) and a list of something and returns a list of something. (Sorry to be so technical.)
TL;DR
foo bar baz
in Elm is not foo(bar, baz)
in JS, it is foo(bar)(baz)
.
Basic Example
Here, we take our Model
, a list of strings, and use List.map to convert it
into a list of li
nodes.
type alias Model =
List String
renderItem : String -> Html msg
renderItem item =
li [] [ text item ]
view : Model -> Html msg
view model =
ul [] (List.map renderItem model)
renderItem
takes something and returns something. It’s annotation is
renderItem : String -> Html msg
which could also be expressed as (a -> b)
. That just happens to be the first
part of List.map’s annotation, so List.map can use it to transform our list
of strings (List String
) into a List (Html msg)
. Looking at the annotation
for Html.div
| this part |
div : List (Attribute msg) -> List (Html msg) -> Html msg
that works perfectly as our 2nd arg. All that this makes sense (eventually).
With the Index
If we want to include the index, we can use List.indexedMap instead. It’s annotation is
indexedMap : (Int -> a -> b) -> List a -> List b
It takes a
- function that takes an int and something and returns something
- a list of something
and returns a list of something.
After a few minor changes, we’ve got
renderItem : Int -> String -> Html msg
renderItem index item =
li [] [ text (String.fromInt index ++ ": " ++ item) ]
view : Model -> Html msg
view model =
ul [] (List.indexedMap renderItem model)
This is pretty simple. We’ve just added another Int
to renderItem
that gets
inserted automatically.
A Custom Prefix
But what if we’re in a different situation? Like List.indexedMap, we want to include another piece of data, but something aside from the index.
renderItem : String -> String -> Html msg
renderItem prefix item =
li [] [ text (prefix ++ item) ]
That seems to make sense. However, when we try to call it, including the additional value…
view model =
ul [] (List.map "Gruffalo: " renderItem model)
We get an error.
The `map` function expects 2 arguments, but it got 3 instead.
42| ul [] (List.map "Gruffalo: " renderItem model)
^^^^^^^^
Are there any missing commas? Or missing parentheses?
How should we handle this?
Let’s combine the two and see if that works. That way List.map is only getting two args again.
view model =
ul [] (List.map ("Gruffalo: " renderItem) model)
This value is not a function, but it was given 1 argument.
42| ul [] (List.map ("Gruffalo: " renderItem) model)
^^^^^^^^^^^^
Are there any missing commas? Or missing parentheses?
Nope. This forces Elm to try and evaluate the (…) stuff, but it reads it as
“execute the
"Gruffalo: "
function and pass itrenderItem
”
That’s all wrong.
Helper Function
We need a function that gets something and returns something according to the
List.map
annotation. (a -> b)
What if we write a function to return the function we want to use for
List.map
? That seems pretty functional, right?
getItemRenderer : String -> (String -> Html msg)
getItemRenderer prefix =
renderItem prefix
view : Model -> Html msg
view model =
ul [] (List.map (getItemRenderer "Gruffalo: ") model)
Hey, it works! Now, when we see List.map (getItemRenderer "Gruffalo: ") model
,
Elm runs getItemRenderer "Gruffalo: "
first again, but this time, the first
value is a function. It, in turn, returns another function (String -> Html
msg)
, which is fed into List.map
as the first arg.
How does it work?
So conceptually, every function accepts one argument. It may return another function that accepts one argument. Etc. At some point it will stop returning functions.
Everything can be curried in Elm! That allows us to call renderItem
with just
the prefix, before we know what values we want. Doing so preloads the first
value and returns a function that’s only needs one more value in order to glue
it all together.
Is there a better way to write this?
view model =
let
itemRenderer =
renderItem "Gruffalo: "
in
ul [] (List.map itemRenderer model)
If we do it like this, it’s a bit less code. It means this trick can’t be used elsewhere, though. Does that really matter in this case? It’s a pretty lame little function, more annotation than anything else. It’s worth thinking about, though, if it’s a non-trivial bit of logic used in the helper function.
If we do inline it like this, though, it makes another option pretty obvious.
Inline the Sucker
view model =
ul [] (List.map (renderItem "Gruffalo: ") model)
There’s the eureka moment. 🤦
If we have a function with arity of 2 - hold tight… We don’t. Stop thinking like that. When we see
renderItem : String -> String -> Html msg
we may think of this as an arity of 2, but it’s not. As stated before, this is
a function that accepts Int
and returns a function that accepts String
that
returns Html msg
The problem we ran into is that List.map
wanted only two args:
- A function that takes in something and returns something
(a -> b)
- A list of something
List a
We couldn’t figure out how to use renderItem
with our prefix, because we
thought we had to call it with the prefix and the value at the same time!
Maybe it’s because of the notation.
renderItem "Gruffalo: " "red"
would be written like this in JS
renderItem("Gruffalo: ")("red")
not like this
renderItem("Gruffalo: ", "red")
That’s a pretty significant difference. Once we arrive at this conslusion, it seems obvious. Before that, though, it can be really confusing. I hope this helps. Once it clicks, it’s seems really nice.