Maybe and Either
Now that we've seen the data
command, let's revisit a type that we saw earlier: Maybe
. Recall that Maybe
is used to represent data that may be null or undefined. It's a builtin type in Haskell, but we could have written it ourselves like this:
data Maybe a = Nothing | Just a
When a computation cannot produce a value, we return Nothing
. Otherwise, we return the value wrapped up in a Just
. Since we don't know what the type of the wrapped value is going to be, we target generic type a
.
Earlier we discussed headMaybe
, divMaybe
, and readMaybe
. Let's look at a new example. Suppose we want to find the first element in a list that meets some criteria. Our function takes in a boolean predicate and a list, and it returns one of the items. This seems like a reasonable type signature:
find :: (a -> Bool) -> [a] -> a
But what if no item satisfies the predicate? We'll get to the end of the list and have no a
to return. We need to return a Maybe
since the search may fail:
find :: (a -> Bool) -> [a] -> Maybe a
When our search reaches the empty list, there's no item that meets the criteria, and we give back Nothing
:
find _ [] = Nothing
In the general case, the list is not empty. We check the first item to see if it satisfies the predicate. It it does, we return it wrapped up in a Just
. Otherwise we recurse on the tail:
find predicate (first : rest)
| predicate first = Just first
| otherwise = find predicate rest
Thanks to the Maybe
, anyone who calls find
is forced to contend with the possibility of a failed search. The type system demands that the caller check if the return value is Nothing
or Just
. In a language like Java, programmers often do not check if a returned reference is null
before trying to call a method on it or access its data. These programmers are usually operating in haste or under the naive optimism that errors don't occur. Their programs inevitably crash with a NullPointerException
. Programmers who use maybe types won't trigger such exceptions. They can only get at the data if they've matched it against Just
.
When we receive Nothing
back from a function, we know that something went wrong, but not what. Haskell has another builtin type called Either
that carries an explanation. In its most abstract sense, it is just a tagged union that holds either a Left
value or a Right
value. But in the conventional context of error handling, a Left
value is the error message that explains why the data couldn't be computed, and the Right
value is the data that was successfully computed. Its definition looks something like this:
data Either a b = Left a | Right b
This alternative definition of find
gives back an Either
:
find :: (a -> Bool) -> [a] -> Either String a
find _ [] = Left "No element matched the predicate"
find predicate (first : rest)
| predicate first = Right first
| otherwise = find predicate rest
When no item is found, it gives back a Left
with the error message. When a matching item is found, it is returned wrapped up in a Right
. As with Maybe
, the caller must pattern match against the variants before it can operate on the result. The type system forces the caller to acknowledge that errors occur.
We will see types similar to Maybe
and Either
used frequently in Rust, a language whose designers value software safety.