loading...

Invoices 2: functors and monads in action

drbearhands profile image DrBearhands ・4 min read

Considering my last post in this series didn't attract a lot of attention, I've decided to keep it a bit short. If you're interested in writing invoices in Elm, I've made a public repository. I'm using it myself, but at the time of writing it certainly still requires some polishing up.

So let's instead focus on what everybody seems to care about the most, functors and monads.

type ColumnSize
  = [...]

columnSizeToString : ColumnSize -> String
columnSizeToString gs = [...]


columns : List (Html msg, ColumnSize) -> Html msg
columns cs =
  let
    columnSizes : String
    columnSizes =
      List.map ( columnSizeToString << Tuple.second ) cs
      |> List.intersperse " "
      |> String.concat

    inDiv : Html msg -> Html msg
    inDiv element = div [] [element]
  in
    div
      [ style "display" "grid"
      , style "grid-template-columns" columnSizes
      ]
      ( List.map ( inDiv << Tuple.first ) cs )

What's going on here?

The function columns puts multiple elements into columns, using display: grid;.

I decided I wanted to have every column dictate its own width. So rather than passing a list of Html msg, we pass a list of (Html msg, ColumnSize). By coupling elements with their widths into tuples, we ensure that we have exactly as many widths as columns.

ColumnSize is a type I have declared previously denoting how wide the column should be (e.g. 1fr or 50px). I'm using ColumnSize rather than String so the correctness of the argument is checked at compile time. If I'd used String, I could have made a column that is "7 kg" long, which would result in a silent error at runtime rather than a loud and obnoxious one at compile time.


Now let's look at columnSizes. It constructs a string denoting the grid's column template, using the list of tuples given as input.

This is where functors come into play. As we've seen previously, a functor is any unary type constructor which has a map function. This is the case for lists: a lists by itself is not a type, but e.g. a list of strings is, and it has a map function that is applies a function to all elements of the list, returning a new list.

That is exactly what happens here:

List.map ( columnSizeToString << Tuple.second ) cs

we create a function by composing columnSizeToString after Tuple.second, this function is passed to List.map, and the resulting function is applied over cs, the list of tuples that was input of the parent function.

So, for each element of cs (a tuple (Html msg, ColumnSize)), we first apply Tuple.second, the first part of the composition. This returns the second element of the tuple (a ColumnSize). We feed that to the second element of the composition, columnSizeToString, which returns a string. The result of the whole expression is therefore a List String.

In the next line:

|> List.intersperse " "

we pipe the result of the previous expression (out list of strings) into List.intersperse " ", which intersperses space elements into our list.

Finally:

|> String.concat

we pipe into String.concat, which turns a list of strings into a single string.


Now, let's look at some code were we use the monadic properties of a list. This is a snippet from inside the keyValueLayout.

entryLayout : List (String, String) -> Html msg
    entryLayout entry =
      case entry of
        [] -> div [] []
        (headerKey, headerValue) :: entryRemainder ->
          entryHeaderLayout headerKey headerValue
            :: List.map entryBodyLayout entryRemainder
            |> List.map (\(a, b) -> [a, b])
            |> List.concat
            |> div
              [ style "display" "grid"
              , style "grid-template-columns" "2fr 5fr"
              , style "grid-column-gap" "10px"
              ]

entryHeaderLayout : String -> String -> (Html msg, Html msg)
entryHeaderLayout k v = [...]
entryBodyLayout : ( String, String ) -> (Html msg, Html msg)
entryBodyLayout (k, v) = [...]

This code turns pairs of strings into a grid element with 2 neatly aligned columns.

The following lines:

entryHeaderLayout headerKey headerValue
  :: List.map entryBodyLayout entryRemainder

create a list with type List (Html msg, Html msg). They are not particularly interesting so I won't delve into them further.

Rather than a list of tuples, we need a list that looks like [key1, value1, key2, value2,...], as that is what a grid element needs as children. We can turn a tuple into a list using \(a, b) -> [a, b], and we could use the map function, but that would leave us with a list of lists of elements, which is not what we want.

Because a list is a monad, we know that we can 'bind' a function.
So a function of type type1 -> List type2, which is just the type of \(a, b) -> [a, b], can be 'bound' so that a List type1 can be turned into a List type2. Essentially, a flat map. This is what we want! In our case, type1 is (Html msg, Html msg) and type2 is Html msg.

List gives us monadic properties with its concat function, which flattens a List (List a) into List a.

Because we already have map, we can simply flatten after mapping, which is what we do here:

|> List.map (\(a, b) -> [a, b])
|> List.concat

That's it! All it takes to be a monad: a map and a flat map function.

Okay technically you also need a singleton a -> F a function but that one is usually trivial.

Discussion

pic
Editor guide