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.