What's Weird with Maybe List

Joël Quenneville

When modeling data in a typed language like Elm or Haskell, some combinations of types are a bit strange. Take Maybe (List a). It has two empty states: Nothing and Just []. Does your code distinguish between these two? Or do they mean the same thing?

Same thing

Lists already have an empty state: []. If you only need to model “list of values” and “no values” then you can rely on List alone.

In the following code, we see that Nothing and Just [] are semantically the same: they represent the absence of results.

view : Maybe (List Int) -> Html a
view calculation =
  case calculation of
    Nothing -> text "No Results"
    Just [] -> text "No Results"
    Just numbers -> viewResults numbers

Wrapping in Maybe doesn’t add any extra information so we can simplify our case statement by dropping the Maybe entirely.

view : List Int -> Html a
view calculation =
  case calculation of
    [] -> text "No Results"
    _ -> viewResults calculation

Different things

Sometimes, Nothing and Just [] are being used to represent different states.

view : Maybe (List Int) -> Html a
view calculation =
  case calculation of
    Nothing -> text "In Progress"
    Just [] -> text "No Results"
    Just numbers -> viewResults numbers

A custom union type will do a better job at communicating the semantic differences. Now you can see at a glance what possible states a calculation can have.

type Calculation
  = InProgress
  | NoResults
  | Results (List Int)

Getting fancy

Observant readers might have noticed that it’s still possible to end up with the weird state Results [] using the union type defined above. Since we’ve separated out the empty state from the list, we might want to get fancy and guarantee a non-empty list for Results.

type Calculation
  = InProgress
  | NoResults
  | Results (List.NonEmpty Int)

Legitimate uses of Maybe (List a)

Does this mean that we should avoid Maybe (List a) at all cost? No. Consider getting the head of a doubly-nested list:

[[1,2], [3,4]]
  |> List.head -- returns Just [1,2]

Maybe (List a) is a perfectly valid return type here. It represents a list that may or may not be present. It does not carry any extra implicit semantic meaning.

Returning an optional list often makes sense when querying another data structure. Not only does it correctly model the uncertainty in the querying, it also allows functions to be chained nicely with other Maybe helpers.

firstChild : Int -> MyTree a -> Maybe (MyTree a)
firstChild id myTree =
  myTree
    |> MyTree.childrenOfNode 10
    |> Maybe.andThen List.head

Conclusion

The Maybe (List a) is a type that introduces ambiguity when data modeling. Depending on what you’re trying to represent, a simple List a or a custom union type may better model your system. Don’t discount it entirely though. It can often make sense as the return type of a query function.