Lesson 19. The Maybe type: dealing with missing values

After reading lesson 19, you’ll be able to

Just as type classes can often be much more abstract than interfaces in OOP, parameterized types play a much larger role than generics do in most languages. This lesson introduces an important parameterized type: Maybe. Unlike List or Map, which represent containers for values, Maybe is the first of many types you’ll see that represents a context for a value. Maybe types represent values that might be missing. In most languages, a missing value is represented by the null value. By using a context representing a value that might be missing, the Maybe type allows you to write much safer code. Because of the power of the Maybe type, errors related to null values are systematically removed from Haskell programs.

Consider this

Suppose you have a simple Map that contains grocery items and indicates the number of them that you need to purchase:

groceries :: Map.Map String Int
groceries = Map.fromList [("Milk",1),("Candy bars",10),
("Cheese blocks",2)]

You accidentally look up MILK instead of Milk. What behavior should you expect from your Map, and how can you handle this type of mistake so that your programs can be sure to run safely even in the presence of missing values in your Map?

19.1. Introducing Maybe: solving missing values with types

At the end of lesson 18, you were working for a mad scientist organizing a collection of human organs. You used the Map type to store a list of organs for easy lookup. Let’s continue exploring this exercise in a new file named Lesson19.hs. Here’s the important code from the preceding lesson:

import qualified Data.Map as Map

data Organ = Heart | Brain | Kidney | Spleen deriving (Show, Eq)

organs :: [Organ]
organs = [Heart,Heart,Brain,Spleen,Spleen,Kidney]

ids :: [Int]
ids = [2,7,13,14,21,24]

organPairs :: [(Int,Organ)]
organPairs = zip ids organs

organCatalog :: Map.Map Int Organ
organCatalog = Map.fromList organPairs

Everything was going fine until you decided to use Map.lookup to look up an Organ in your Map. When doing this, you came across a strange new type, Maybe.

Maybe is a simple but powerful type. So far, all of our parameterized types have been viewed as containers. Maybe is different. Maybe is best understood as a type in a context. The context in this case is that the type contained might be missing. Here’s its definition.

Listing 19.1. Definition of Maybe
data Maybe a = Nothing | Just a

Something of a Maybe type can be either Nothing, or Just something of type a. What in the world could this mean? Let’s open up GHCi and see what happens:

GHCi> Map.lookup 13 organCatalog
Just Brain

When you look up an ID that’s in the catalog, you get the data constructor Just and the value you expect for that ID. If you look up the type of this value, you get this:

Map.lookup 13 organCatalog :: Maybe Organ

In the definition of lookup, the return type is Maybe a. Now that you’ve used lookup, the return type is made concrete and the type is Maybe Organ. The Maybe Organ type means pretty much what it says: this data might be an instance of Organ. When would it not be? Let’s see what happens when you ask for the value of an ID that you know has nothing in it:

GHCi> Map.lookup 6 organCatalog
Nothing
Quick check 19.1

Q1:

What’s the type of Nothing in the preceding example?

QC 19.1 answer

1:

The type is Maybe Organ.

 

19.2. The problem with null

The organCatalog has no value at 6. In most programming languages, one of two things happens if you ask for a value that isn’t in the dictionary. Either you get an error, or you get back a null value. Both of these responses have major issues.

19.2.1. Handling missing values with errors

In the case of throwing an error, many languages don’t require you to catch errors that might be thrown. If a program requests an ID not in the dictionary, the programmer must remember to catch the error, or the whole program could crash. Additionally, the error must be handled at the time the exception is thrown. This might not seem like a big issue, because it might be wise to always stop the error at its source. But suppose that you want to handle the case of a missing Spleen differently from a missing Heart. When the missing ID error is thrown, you might not have enough information to properly handle the different cases of having a missing value.

19.2.2. Returning null values

Returning null has arguably more problems. The biggest issue is that the programmer once again has to remember to check for null values whenever a value that can be null is going to be used. There’s no way for the program to force the programmer to remember to check. Null values are also extremely prone to causing errors because they don’t typically behave like the value your program is expecting. A simple call of toString can easily cause a null value to throw an error in a part of the program. If you’re a Java or C# developer, the mere phrase null pointer exception should be argument enough that null values are tricky.

19.2.3. Using Maybe as a solution to missing values

Maybe solves all of these problems in a clever way. When a function returns a value of the Maybe type, the program can’t use that value without dealing with the fact that the value is wrapped in a Maybe. Missing values can never cause an error in Haskell because Maybe makes it impossible to forget that a value might be null. At the same time, the programmer never has to worry about this until absolutely necessary. Maybe is used in all the typical places that Null values pop up, including these:

The best way to illustrate the magic of Maybe is with code. Let’s say you’re the assistant of the mad scientist. Periodically you need to do inventory to figure out what new body parts must be harvested. You can never remember which drawers have what in them, or even which have anything in them. The only way you can query all the drawers is to use every ID in the range of 1 to 50.

Listing 19.2. List of possibleDrawers in your organCatalog
possibleDrawers :: [Int]
possibleDrawers = [1 .. 50]

Next you need a function to get the contents of each drawer. The following maps this list of possible drawers with the lookup function.

Listing 19.3. Definition of getDrawers
getDrawerContents :: [Int] -> Map.Map Int Organ -> [Maybe Organ]
getDrawerContents ids catalog = map getContents ids
 where getContents = \id -> Map.lookup id catalog

With getDrawerContents, you’re ready to search the catalog.

Listing 19.4. A list of availableOrgans that can contain missing values
availableOrgans :: [Maybe Organ]
availableOrgans = getDrawerContents possibleDrawers organCatalog

Had this been a programming language that threw exceptions on nulls, your program would already have blown up. Notice that your type is still a List of Maybe Organ. You’ve also avoided the issue with returning a special null value. No matter what you do with this list, until you deal explicitly with this possibility of missing values, you must keep this data a Maybe type.

The final thing that you need is to be able to get a count of a particular organ you’re interested in. At this point, you do need to deal with the Maybe.

Listing 19.5. countOrgan function counts instances of an Organ
countOrgan :: Organ -> [Maybe Organ] -> Int
countOrgan organ available = length (filter
                                      (\x -> x == Just organ)
                                      available)

The interesting thing here is that you didn’t even have to remove the organ from the Maybe context. Maybe implements Eq, so you can just compare two Maybe Organs. You not only didn’t have to handle any errors, but because your computation never explicitly dealt with values that didn’t exist, you also never had to worry about handling that case! Here’s the final result in GHCi:

GHCi> countOrgan Brain availableOrgans
1
GHCi> countOrgan Heart availableOrgans
2

19.3. Computing with Maybe

It would be useful to be able to print your list of availableOrgans so you could at least see what you have. Both your Organ type and Maybe support Show so you can print it out in GHCi:

GHCi> show availableOrgans [Nothing,Just Heart,Nothing,Nothing,Nothing,
Nothing,Just Heart,Nothing,Nothing,Nothing...

Although you do get printing for free, this is ugly. The first thing you want to do is remove all the Nothing values. You can use filter and pattern matching to achieve this.

Listing 19.6. Definition of isSomething
isSomething :: Maybe Organ -> Bool
isSomething Nothing = False
isSomething (Just _) = True

And now you can filter your list to the organs that aren’t missing.

Listing 19.7. Using isSomething with filter to clean [Maybe Organ]
justTheOrgans :: [Maybe Organ]
justTheOrgans = filter isSomething availableOrgans

In GHCi, you can see that you’ve made quite an improvement:

GHCi>justTheOrgans
[Just Heart,Just Heart,Just Brain,Just Spleen,Just Spleen,Just Kidney]

The problem is you still have these Just data constructors in front of everything. You can clean this up with pattern matching as well. You’ll make the showOrgan function that will turn a Maybe Organ into a String. You’ll add the Nothing pattern even though you won’t need it because it’s a good habit to always match all patterns just in case.

isJust and isNothing

The Data.Maybe module contains two functions, isJust and isNothing, that solve the general case of handling Just values. isJust is identical to the isSomething function but works on all Maybe types. With Data.Maybe imported, you could’ve solved this problem as follows:

justTheOrgans = filter isJust availableOrgans
Listing 19.8. Definition of showOrgan
showOrgan :: Maybe Organ -> String
showOrgan (Just organ) = show organ
showOrgan Nothing = ""

Here are a couple of examples in GHCi to get a feel for how this works:

GHCi> showOrgan (Just Heart)
"Heart"
GHCi> showOrgan Nothing
""

Now you can map your showOrgan function on justTheOrgans.

Listing 19.9. Using showOrgan with map
organList :: [String]
organList = map showOrgan justTheOrgans

As a final touch, you’ll insert commas to make the list prettier. You can use the intercalate (a fancy word for insert) function in the Data.List module (so you’ll need to add import Data.List to the top of your file):

cleanList :: String
cleanList = intercalate ", " organList

GHCi> cleanList
"Heart, Heart, Brain, Spleen, Spleen, Kidney"

Quick check 19.2

Q1:

Write a function numOrZero that takes a Maybe Int and returns 0 if it’s nothing, and otherwise returns the value.

QC 19.2 answer

1:

numOrZero :: Maybe Int -> Int
numOrZero Nothing = 0
numOrZero (Just n) = n

 

19.4. Back to the lab! More-complex computation with Maybe

Suppose you need to do several things to a value in a Maybe. The mad scientist has a more interesting project. You’ll be given a drawer ID. You need to retrieve an item from the drawer. Then you’ll put the organ in the appropriate container (a vat, a cooler, or a bag). Finally, you’ll put the container in the correct location. Here are the rules for containers and locations:

For containers:

For locations:

You’ll start by writing this out, assuming everything goes well and you don’t have to worry about Maybe at all.

Listing 19.10. Defining key functions and data types for mad scientist request
data Container = Vat Organ | Cooler Organ | Bag Organ

instance Show Container where
   show (Vat organ) = show organ ++ " in a vat"
   show (Cooler organ) = show organ ++ " in a cooler"
   show (Bag organ) = show organ ++ " in a bag"
data Location = Lab | Kitchen | Bathroom deriving Show

organToContainer :: Organ -> Container
organToContainer Brain = Vat Brain
organToContainer Heart = Cooler Heart
organToContainer organ = Bag organ

placeInLocation :: Container -> (Location,Container)
placeInLocation (Vat a) = (Lab, Vat a)
placeInLocation (Cooler a) = (Lab, Cooler a)
placeInLocation (Bag a) = (Kitchen, Bag a)

A function, process, will handle taking an Organ and putting it in the proper container and location. Then a report function will take your container and location, and output a report for the mad scientist.

Listing 19.11. The core functions process and report
process :: Organ -> (Location, Container)
process organ =  placeInLocation (organToContainer organ)

report ::(Location,Container) -> String
report (location,container) = show container ++
                              " in the " ++
                              show location

These two functions are written assuming that no organs are missing. You can test how they work before you worry about working with the catalog:

GHCi> process Brain
(Lab,Brain in a vat)
GHCi> process Heart
(Lab,Heart in a cooler)
GHCi> process Spleen
(Kitchen,Spleen in a bag)
GHCi> process Kidney
(Kitchen,Kidney in a bag)
GHCi> report (process Brain)
"Brain in a vat in the Lab"
GHCi> report (process Spleen)
"Spleen in a bag in the Kitchen"

You still haven’t handled getting your Maybe Organ out of the catalog. In Haskell, other types such as Maybe handle the many cases in software where things could go wrong. What you’ve done here with your process function is a common pattern in Haskell: you separate the parts of the code for which you need to worry about a problem (for example, missing values) from the ones that you don’t. Unlike in most other languages, it’s impossible for Maybe values to accidentally find their way into process. Imagine that you could write code that couldn’t possibly have null values in it!

Now let’s put this together so you can get data out of your catalog. What you want is something like the following function, except you still need to handle the case of Maybe.

Listing 19.12. Ideal definition of processRequest (won’t compile)
processRequest :: Int -> Map.Map Int Organ -> String
processRequest id catalog = report (process organ)
 where organ = Map.lookup id catalog

The trouble is that your organ value is a Maybe Organ type and that process takes an Organ. To solve this given the tools you have now, you’ll have to combine report and process into a function that handles the Maybe Organ.

Listing 19.13. processAndReport to handle the Maybe Organ data
processAndReport :: (Maybe Organ) -> String
processAndReport (Just organ) = report (process organ)
processAndReport  Nothing = "error, id not found"

You can now use this function to process the request.

Listing 19.14. processRequest with support for Maybe Organ
processRequest :: Int -> Map.Map Int Organ -> String
processRequest id catalog = processAndReport organ
 where organ = Map.lookup id catalog

This solution works out well, as you can see in GHCi the function handles both null and existing organs:

GHCi> processRequest 13 organCatalog
"Brain in a vat in the Lab"
GHCi> processRequest 12 organCatalog
"error, id not found"

There’s one minor issue from a design perspective. Right now your processRequest function handles reporting when there’s an error. Ideally, you’d like the report function to handle this. But to do that given your knowledge so far, you’d have to rewrite process to accept a Maybe. You’d be in a worse situation, because you’d no longer have the advantage of writing a processing function that you can guarantee doesn’t have to worry about a missing value.

Quick check 19.3

Q1:

How would you rewrite report so that it works with Maybe (Location, Container) and handles the case of the missing Organ?

QC 19.3 answer

1:

report :: Maybe (Location,Container) -> String
report Nothing = "container not found"
report (Just (location,container)) = show container ++
                                     " in the " ++
                                     show location

 

Summary

In this lesson, our objective was to introduce you to one of Haskell’s more interesting parameterized types: Maybe. The Maybe type allows you to model values that may be missing. Maybe achieves this by using two data constructors, Just and Nothing. Values represented by the Nothing data constructor are missing. Values represented by the Just a constructor can be safely accessed through pattern matching. Maybe is a great example of how powerful types make your code less erro- prone. Because of the Maybe type, the entire class of errors related to having null values is completely eliminated. Let’s see if you got this.

Q19.1

Write a function emptyDrawers that takes the output of getDrawerContents and tells you the number of drawers that are empty.

Q19.2

Write a version of map that works for Maybe types, called maybeMap.