Learn You a Haskell for Great Good! - Miran Lipovaca [58]
So far, you’ve seen Maybe a mostly used to represent the results of computations that could have failed. But sometimes, Maybe a isn’t good enough, because Nothing doesn’t convey much information other than that something has failed. That’s fine for functions that can fail in only one way, or if we’re not interested in how or why they failed. For instance, a Data.Map lookup fails only if the key wasn’t in the map, so we know exactly what happened.
However, when we’re interested in how or why some function failed, we usually use the result type of Either a b, where a is a type that can tell us something about the possible failure, and b is the type of a successful computation. Hence, errors use the Left value constructor, and results use Right.
As an example, suppose that a high school has lockers so that students have some place to put their Guns N’ Roses posters. Each locker has a code combination. When students need to be assigned a locker, they tell the locker supervisor which locker number they want, and he gives them the code. However, if someone is already using that locker, the student needs to pick a different one. We’ll use a map from Data.Map to represent the lockers. It will map from locker numbers to a pair that indicates whether the locker is in use and the locker code.
import qualified Data.Map as Map
data LockerState = Taken | Free deriving (Show, Eq)
type Code = String
type LockerMap = Map.Map Int (LockerState, Code)
We introduce a new data type to represent whether a locker is taken or free, and we make a type synonym for the locker code. We also make a type synonym for the type that maps from integers to pairs of locker state and code.
Next, we’ll make a function that searches for the code in a locker map. We’ll use an Either String Code type to represent our result, because our lookup can fail in two ways: The locker can be taken, in which case we can’t tell the code, or the locker number might not exist. If the lookup fails, we’re just going to use a String to indicate what happened.
lockerLookup :: Int -> LockerMap -> Either String Code
lockerLookup lockerNumber map = case Map.lookup lockerNumber map of
Nothing -> Left $ "Locker " ++ show lockerNumber ++ " doesn't exist!"
Just (state, code) -> if state /= Taken
then Right code
else Left $ "Locker " ++ show lockerNumber
++ " is already taken!"
We do a normal lookup in the map. If we get a Nothing, we return a value of type Left String, saying that the locker doesn’t exist. If we do find it, then we do an additional check to see if the locker is in use. If it is, we return a Left saying that it’s already taken. If it isn’t, we return a value of type Right Code, in which we give the student the correct code for the locker. It’s actually a Right String (which is a Right [Char]), but we added that type synonym to introduce some additional documentation into the type declaration.
Here’s an example map:
lockers :: LockerMap
lockers = Map.fromList
[(100,(Taken, "ZD39I"))
,(101,(Free, "JAH3I"))
,(103,(Free, "IQSA9"))
,(105,(Free, "QOTSA"))
,(109,(Taken, "893JJ"))
,(110,(Taken, "99292"))
]
Now let’s try looking up some locker codes.
ghci> lockerLookup 101 lockers
Right "JAH3I"
ghci> lockerLookup 100 lockers
Left "Locker 100 is already taken!"
ghci> lockerLookup 102 lockers
Left "Locker number 102 doesn't exist!"
ghci> lockerLookup 110 lockers
Left "Locker 110 is already taken!"
ghci> lockerLookup 105 lockers
Right "QOTSA"
We could have used a Maybe a to represent the result, but then we wouldn’t know why we couldn’t get the code. But now we have information about the failure in our result type.
Recursive Data Structures
As you’ve seen, a constructor in an algebraic data type can have several fields (or none at all), and each field must be of some concrete type. So we can make types that have themselves as types in their fields! And that means we can create recursive data types, where one value of some type