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:
> 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
:
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:
> 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:
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:
> 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
:
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
:
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:
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.