Maybe Data

Dear Computer

Chapter 7: Immutability and I/O

Maybe Data

The getInt function has a problem: it doesn't validate the user's input. If the user enters text that is not an integer, the program crashes with an IOError. Haskell does provide mechanisms for catching exceptions, but we will not discuss them. Instead, we'll favor a different solution for handling exceptional situations: Maybe.

A Maybe type is effectively a union that can either hold a value or be marked empty. It maintains a type tag that has two possible values. Nothing means there is no value. Just means there is. The compiler forces us to examine this tag before processing the value that may be inside.

headMaybe

The builtin head function does not use a Maybe type. It's type signature is head :: [a] -> a. If we pass it an empty list, it throws an exception:

ghci
> head []
*** Exception: Prelude.head: empty list

We can write a safer version using Maybe. If the list is empty, we return Nothing. Otherwise, we safely call head and wrap the element up in a Just:

Haskell
headMaybe :: [a] -> Maybe a
headMaybe items =
  case items of
    [] -> Nothing
    _ -> Just (head items)

Note that the function doesn't return an a. It returns a Maybe a. We can call headMaybe with any list and it won't generate an exception:

ghci
> headMaybe []
Nothing
> headMaybe "Trouble"
Just 'T'
> headMaybe [5, 6]
Just 5

To operate on the returned Maybe, we must check if it's Nothing or Just, which means we need a conditional expression. This function uses a case expression:

Haskell
printFirst list = do
  let firstMaybe = headMaybe list
  case firstMaybe of
    Nothing -> putStrLn "There is no first element."
    Just first -> putStrLn ("First: " ++ show first)

There's no exception when we call this function with an empty list:

GHCI
> showFirst ""
There is no first element.
> showFirst "shambles"
First: 's'

This Maybe type forces developers to anticipate invalid results. The compiler rejects code that doesn't take into account the possibility of Nothing. We simply don't get null pointer exceptions in Haskell like we do in Java and many other languages that don't build null awareness into their type system.

readMaybe

The Text.Read module includes the function readMaybe that returns Nothing on a parsing error. We'll use it to make our getInt function resilient to bad input. First, we import the module and call readMaybe much as we would call read:

Haskell
import Text.Read

getInt :: IO Int
getInt = do
  intString <- getLine
  let intMaybe = readMaybe intString :: Maybe Int
  -- ...

If intMaybe is a valid integer, then we package it back up as an IO Int with return:

Haskell
import Text.Read

getInt :: IO Int
getInt = do
  intString <- getLine
  let intMaybe = readMaybe intString :: Maybe Int
  case intMaybe of
    Just int -> return int
    -- ...

If the user entered input that couldn't be parsed as integer, then readMaybe returns Nothing. We want to display an error message and prompt the user again. This might have to happen many times. Haskell doesn't have loops to repeat the prompting, but it does have recursion. We recursively call getInt in the Nothing case:

Haskell
import Text.Read

getInt :: IO Int
getInt = do
  intString <- getLine
  let intMaybe = readMaybe intString :: Maybe Int
  case intMaybe of
    Just int -> return int
    Nothing -> do
      putStr "That wasn't an integer. Try again: "
      getInt

This function won't crash on bad user input, and we can be assured that when it finally returns, we will have a valid integer in our hands.

← Helper FunctionsIteration →