← Fundamentals
Guided lesson 7 questions · Haskell only

Haskell Maybe, Either, and partial functions

Some Prelude functions stop the program on input their type accepts. Replace them with functions that give a Maybe, and combine many steps that can fail.

A partial function such as head, read, or fromJust has a type that promises a result it cannot always give, and the program stops when that missing result is needed. A function that gives a Maybe or an Either instead puts the failure in its type, so every caller must handle it. A do block combines such steps and ends at the first failure.

Written for Haskell.

Before you begin

Why this matters

A Haskell program that type-checks can still stop with a one-line message such as Prelude.read: no parse, which does not say where. If you know which functions can do this, and what to write instead, you can keep these failures in the types, where GHC checks every use.

What this helps you reason about

  • Recognize head, tail, read, fromJust, !!, and div by zero in code, and predict the line where the program stops.
  • Replace each one with a function that gives a Maybe, and combine several such steps in one do block.
  • Spot a list where one bad element is dropped without a word, and a list where it stops everything.

In Python, int("nine") fails at the line that calls it, and Java’s Integer.parseInt does the same. Haskell’s read fails too, but at the line that first needs the number, which can be far from the call. Errors as values or exceptions showed both Haskell answers to failure: Maybe and Either values for failures a caller should expect, and exceptions from partial functions such as head. This lesson goes further on both: which Prelude functions stop a program, what to write instead, and how to combine many steps that can fail.

Already know this? The short version
  • head, tail, read, fromJust, !!, div by zero, maximum on an empty list, and patterns that miss a case all type-check and raise an exception when their missing result is needed; GHC’s message names the function, and for most of them not the line.
  • The total replacements give a Maybe: readMaybe from Text.Read, listToMaybe from Data.Maybe, lookup, Map.lookup, and a case expression that names Nothing and [] itself.
  • A do block for Maybe ends at the first Nothing, like a do block for Either at the first Left; a helper such as note reason, or maybe (Left reason) Right, turns each Maybe step into an Either step that says what failed, because one do block cannot mix the two types.
  • mapM (traverse) gives all results or the first failure, and never looks at the elements after it; mapMaybe keeps the successes and drops the failures. The section “What goes wrong with Maybe and partial functions in real programs?” covers Map.! and !!, a list that a filter can empty, fromJust after a check, and what readMaybe accepts.

Go to the final check· Go to the practice tickets

Words in this lesson (8)
partial function
A function that has no result for some inputs of its type, such as head on an empty list, or !! with a position past the end. The type checker accepts every call; the failure appears only when the missing result is needed.
exception
An error raised while a program runs, such as the one !! raises for a position past the end. Unless the program handles it, the program stops and prints its message.
non-exhaustive patterns
Patterns that do not cover every possible value. GHC accepts them; a value that matches none stops the program with Non-exhaustive patterns when it is needed.
total function
A function that gives a result for every input of its type. readMaybe is total: for text that is not a number it gives Nothing instead of stopping the program.
Maybe
A type whose values are Just x, holding a value, or Nothing, holding none. Maybe Int, for example, is a number that may be missing.
>>=
The operator a do block is written with. For Either, Left e >>= f is Left e without using f, and Right x >>= f is f x.
Either
A type whose values are Left e or Right x. By convention Left holds an error and Right holds a successful result.
mapM
A function that applies a step that can fail to each element of a list. For Maybe, mapM f xs gives Just the list of results when every step succeeds, and Nothing at the first step that fails. traverse does the same.

Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.

01 / Partial functions

Which functions stop the program on a value their type allows?

Errors as values or exceptions called head a partial function: its type, [a] -> a, promises an element that an empty list does not have. head is one of a family. Each function below type-checks with the input shown, has no result for it, and raises an exception. These are the messages GHC prints:

  • head [] and tail []: Prelude.head: empty list, and Prelude.tail: empty list
  • read "12x" :: Int: Prelude.read: no parse
  • [1, 2] !! 5: Prelude.!!: index too large
  • div 7 0: divide by zero
  • fromJust Nothing, from Data.Maybe: Maybe.fromJust: Nothing
  • shelfName 3, for the shelfName below, whose equations miss a case: Main.hs:(2,1)-(3,22): Non-exhaustive patterns in function shelfName

Two things make these failures hard to find. First, most messages name the function, not your line: Prelude.read: no parse does not say which read failed. fromJust is different: its message ends with a CallStack, a short list that names the line that called fromJust. The message for patterns that miss a case names the lines of the function, as (2,1)-(3,22): from line 2, column 1, to line 3, column 22. Second, the program stops where the result is first needed, not where the call is written. A list built with read can be counted and passed along, and the program stops later, at the line that needs the bad number.

Main.hs
shelfName :: Int -> String
shelfName 1 = "atlases"
shelfName 2 = "poetry"

firstCount :: [String] -> Int
firstCount typed = read (head typed)

-- Each of these type-checks and stops the program:
-- shelfName 3, firstCount [], firstCount ["ten"]

head, tail, read, fromJust, !!, div by zero, and patterns that miss a case all type-check and stop the program when their missing result is needed.

Coming from Python: int("nine") raises at the line that calls it. read "nine" raises later, at the line that first needs the number, and a list of read results can be built and counted without any error.

Precisely: the messages in GHC 9.0

The messages above are those of GHC 9.0.2, the stepper’s compiler, and other versions word them differently. In base for GHC 9.0, fromJust has a HasCallStack constraint, so its message names the caller; head, tail, read, and !! do not. GHC 9.0 does not warn about head or tail; GHC 9.8 added the -Wx-partial warning for them. GHC warns about patterns that miss a case with -Wincomplete-patterns, which -Wall turns on, but the stepper does not show warnings.

Experiment

Find the line where a bad read stops the program

map read typed builds a list whose elements are not computed yet. length needs only the shape of the list, and head needs only the first element, so neither one runs read on "nine". sum needs every element, so read "nine" runs at line 10, finds no number, and raises the exception there.

Why it matters A partial function fails where its result is first needed. Look for the cause where the value was made, not only at the line where the program stopped.

counts is built with read from text that volunteers typed. Predict the three lines. Then change "9" on line 3 to "nine", and predict which lines still print.

-- Book counts typed by volunteers, one per shelf.
typed :: [String]
typed = ["14", "9", "12"]

main :: IO ()
main = do
  let counts = map read typed :: [Int]
  putStrLn ("shelves counted: " ++ show (length counts))
  putStrLn ("first shelf: " ++ show (head counts))
  putStrLn ("all shelves: " ++ show (sum counts))
What to look for

Each stop shows one place, such as Main.main, not a list of frames. At line 8, counts shows _ (unevaluated): nothing has been read yet. The output shows shelves counted: before the 3, because show computes the number only when putStrLn needs it. At line 9, counts shows [_ (unevaluated),_ (unevaluated),_ (unevaluated)]: length counted three elements and read none of them. At line 10, counts shows [14,_ (unevaluated),_ (unevaluated)], and the run prints shelves counted: 3, first shelf: 14, and all shelves: 35. After the edit, the stops are the same. length still gives 3 and head still gives 14. At line 10, all shelves: is printed, and then the run ends with the error *** Exception: Prelude.read: no parse. The message names read, but not line 7, where read is written, and not line 10, where its result was needed.

Expected output

shelves counted: 3
first shelf: 14
all shelves: 35

02 / Total replacements

What can you write instead of a partial function?

A total function gives a result for every input of its type. Each partial function has a total replacement that gives a Maybe, so the type says that the answer can be missing:

  • readMaybe, from Text.Read, in place of read: Just the number, or Nothing for text that is not one
  • listToMaybe, from Data.Maybe, in place of head: Just the first element, or Nothing for []
  • a case expression in place of fromJust and tail: it names Nothing and [] and says what to do for them
  • lookup, which Absence, zero, and empty values introduced, in place of a search that assumes the key is there
  • a test of the divisor before div, and an equation for every case

The caller then has to say what Nothing means, and GHC refuses a Maybe Int where an Int is needed. firstCount below reads the first typed count. It has two steps that can fail: the list can be empty, and the first text can be something other than a number.

Main.hs
import Data.Maybe (listToMaybe)
import Text.Read (readMaybe)

firstCount :: [String] -> Maybe Int
firstCount typed =
  case listToMaybe typed of
    Nothing -> Nothing
    Just text -> readMaybe text
-- firstCount [] and firstCount ["ten"] are Nothing

Each partial function has a total replacement that gives a Maybe, so GHC makes every caller handle the missing result.

Coming from Java: Integer.parseInt throws when the text is not a number. readMaybe is like a parse method that returns an empty Optional instead of throwing.

Precisely: telling readMaybe the type

readMaybe, like read, needs to know which type to read. Here the signature of firstCount fixes it as Int. Without such a signature, write readMaybe text :: Maybe Int. A position can be read without !!: listToMaybe (drop n xs) gives Nothing past the end, and needs a separate test for a negative n, because drop with a negative count drops nothing.

03 / Chains of Maybe

How do several steps that can fail work together?

In firstCount, the second step uses the result of the first: readMaybe needs the text that listToMaybe found. Steps like these form a chain, and each new step adds one more level of case. A do block writes the same chain with one step per line. Propagation and unwinding showed that the first Left ends a do block for Either. In a do block for Maybe, the first Nothing ends the block in the same way and becomes its result, and the lines after it are never evaluated.

This works because a do block is written with >>=. For Maybe, Nothing >>= f is Nothing without using f, and Just x >>= f is f x. So listToMaybe typed >>= readMaybe is firstCount without do.

freeSpace below has three steps: find the shelf, read the count, and compare. Its two Nothing results look the same. The caller cannot tell an unknown shelf from a count that is not a number.

Main.hs
-- Books each shelf can hold.
shelves :: [(String, Int)]
shelves = [("A1", 40), ("B2", 25)]

freeSpace :: String -> String -> Maybe Int
freeSpace code typed = do
  size <- lookup code shelves
  used <- readMaybe typed
  if used > size then Nothing else Just (size - used)
-- freeSpace "A1" "14" is Just 26
-- freeSpace "C3" "14" and freeSpace "A1" "lots" are Nothing

In a do block for Maybe, the first Nothing ends the block and becomes its result; the lines after it are never evaluated.

Precisely: one notation for Maybe, Either, and IO

The Haskell 2010 Report translates do { x <- e; rest } into e >>= \x -> do { rest }. Maybe, Either e, and IO are all instances of the Monad type class, which provides >>=, so the same notation means “stop at Nothing”, “stop at Left”, or “run the next action”, depending on the type.

04 / Adding a reason

How do you tell which step failed?

Either keeps a reason. To use a Maybe step in a do block for Either, give the step its reason: note below turns Nothing into Left reason and Just x into Right x. maybe (Left reason) Right does the same in one expression, as Errors as values or exceptions showed.

A do block has one type for all its lines. A line that gives a Maybe inside a do block for Either does not compile: GHC reports Couldn’t match type ‘Maybe’ with ‘Either String’. So each Maybe step needs its own note. That is also the right place for the reason: the shelf lookup knows the shelf code, and the read knows the text.

Main.hs
note :: String -> Maybe a -> Either String a
note reason Nothing = Left reason
note _ (Just x) = Right x

freeSpace :: String -> String -> Either String Int
freeSpace code typed = do
  size <- note ("no shelf " ++ code) (lookup code shelves)
  used <- note ("not a number: " ++ typed) (readMaybe typed)
  if used > size then Left "too many books" else Right (size - used)

Give each Maybe step its own reason, at the step that knows it; then the Left that reaches the caller says which step failed.

Coming from Rust: note reason m is m.ok_or(reason), and each <- line in a do block for Either works like ? on a Result.

Precisely: where note comes from

base has no function called note. The name comes from libraries such as errors, and many projects define it themselves, as above. Going the other way, either (const Nothing) Just drops the reason and gives a Maybe.

Experiment

Give each failing step its own reason

note turns Nothing into a Left with the reason given at that step, and Just x into Right x. Every line of the do block is then an Either String value, and the first Left ends the block. A do block has one type for all its lines, so a bare lookup, a Maybe, cannot be a line of it.

Why it matters Give each step that can fail its own reason, at the step that knows it. Then the Left that reaches the caller says which step failed.

pickup reads a typed card number, finds the member, and finds the shelf where a reserved book waits. Predict the three lines, and on which lines of pickup the stepper stops for "4x". Then replace line 16 with name <- lookup card members, and predict what GHC says.

import Text.Read (readMaybe)

members :: [(Int, String)]
members = [(17, "Ines"), (42, "Tomas")]

shelfOf :: [(String, String)]
shelfOf = [("Ines", "A1")]

note :: String -> Maybe a -> Either String a
note reason Nothing = Left reason
note _ (Just x) = Right x

pickup :: String -> Either String String
pickup typed = do
  card <- note ("not a card number: " ++ typed) (readMaybe typed)
  name <- note ("no member " ++ show card) (lookup card members)
  note (name ++ " has nothing reserved") (lookup name shelfOf)

main :: IO ()
main = do
  print (pickup "17")
  print (pickup "42")
  print (pickup "4x")
What to look for

For "17", the stepper stops on line 14, then on lines 15, 16, and 17, each followed by a stop in note on line 11; members and shelfOf get a stop when they are first needed. The run prints Right "A1". For "42", the stops reach line 17, then note on line 10, and the output shows Left " before the reason: the text is computed while it is printed. name shows 'T' : _ (unevaluated) at that point, a text whose first letter is known and whose rest is not computed yet. For "4x", the stepper stops on lines 14 and 15, then in note on line 10, then on line 15 again while the reason is printed. It never stops on line 16 or 17. The run prints Left "Tomas has nothing reserved" and Left "not a card number: 4x". After the edit, nothing runs. GHC reports Main.hs:16:11: error: • Couldn’t match type ‘Maybe’ with ‘Either String’ Expected: Either String String Actual: Maybe String.

Expected output

Right "A1"
Left "Tomas has nothing reserved"
Left "not a card number: 4x"

05 / Lists of results

What happens when one element of a list fails?

mapM applies a step that can fail to each element of a list. For Maybe, it gives Just the list of results when every step succeeds, and Nothing at the first step that fails; the elements after it are never looked at. For Either, it gives the first Left. traverse does the same. mapM_, from Haskell recursion and collection transformations, is the IO relative that keeps no results.

mapMaybe, from Data.Maybe, does something else. It applies the step to every element, keeps each Just result, and drops each Nothing.

Choose by what one bad element means. If one bad count makes the whole stock-take wrong, use mapM: the caller gets Nothing and must deal with it. If the good elements are useful on their own, use mapMaybe, and compare the lengths of the two lists so that the caller can see how many were dropped.

Main.hs
readCount :: String -> Maybe Int
readCount typed = readMaybe typed

allCounts :: [String] -> Maybe [Int]
allCounts typed = mapM readCount typed

goodCounts :: [String] -> [Int]
goodCounts typed = mapMaybe readCount typed
-- for ["14", "nine", "12"]: Nothing, and [14, 12]

mapM gives every result or the first failure; mapMaybe keeps the successes and drops the failures.

Coming from Rust: mapM readCount typed is typed.iter().map(read_count).collect::<Option<Vec<_>>>(), and mapMaybe is filter_map.

Precisely: sequence and Traversable

mapM f xs is sequence (map f xs): sequence turns a list of Maybe values into a Maybe of a list. Both mapM and traverse work for any Traversable container, such as a Maybe or a Data.Map, not only lists. For a Data.Map, mapM keeps the keys and gives Nothing if any value fails.

Experiment

Count how many texts mapM reads

mapM applies parseCount to one text after another. When parseCount gives Nothing, mapM gives Nothing at once, and the texts after it are never read. mapMaybe applies parseCount to every text, keeps each Just result, and drops each Nothing.

Why it matters mapM gives every result or the first failure. mapMaybe gives the results that worked and drops the failures without a word, so use it only where a failure may be left out.

Predict both lines, and how many times the stepper stops in parseCount during the first print. Then change "nine" on line 9 to "9" and predict again.

import Data.Maybe (mapMaybe)
import Text.Read (readMaybe)

parseCount :: String -> Maybe Int
parseCount typed = readMaybe typed

main :: IO ()
main = do
  let shelves = ["14", "nine", "12"]
  print (mapM parseCount shelves)
  print (mapMaybe parseCount shelves)
What to look for

During the first print, the stepper stops in parseCount twice. At each stop, typed shows _ (unevaluated), because the stop comes before the text is needed. The run prints Nothing. At the next stop, on line 11, shelves shows ["14","nine",_ (unevaluated)]: mapM read two texts and never looked at "12". During the second print, the stepper stops in parseCount three times, with typed = "14", then "nine", then _ (unevaluated) for "12". The output shows [14 before ,12]. The run prints [14,12]. After the edit, the first print stops in parseCount three times, and the run prints Just [14,9,12] and [14,9,12].

Expected output

Nothing
[14,12]

06 / In production

What goes wrong with Maybe and partial functions in real programs?

Four mistakes with partial functions and Maybe are common in real Haskell code.

A lookup that cannot fail, until it does. Map.! from Data.Map, and !! on a list, stop the program when the key or the position is missing: Map.!: given key is not an element in the map. A key that comes from input, a file, or another module can always be missing. Use Map.lookup, and handle Nothing where the key came from.

A list that a filter can empty. maximum, minimum, head, and a div by the length all fail on an empty list, with Prelude.maximum: empty list or divide by zero. In the test data the list always had elements; after a filter on real data, it may not. Look at the list with a case expression first, and give Nothing for [].

fromJust after a check. if isJust found then fromJust found else …, where isJust from Data.Maybe is True for a Just, works on the day it is written. Later someone changes the test or moves the fromJust, and nothing warns that the crash is back. A case expression takes the value out in the same place where it checks it, so the two cannot drift apart.

readMaybe accepts more than digits. For an Int, " 12 ", "(12)", "0x0C", and "0o14" all give Just 12, while "+3" and "12.0" give Nothing. If a form wants plain digits, check that first, for example with all isDigit text, where isDigit comes from Data.Char.

Main.hs
busiest :: [Int] -> Maybe Int
busiest counts =
  case filter (> 0) counts of
    [] -> Nothing
    found -> Just (maximum found)

Use lookup in place of Map.! and !!, look at a list before maximum or div, take a value out of a Maybe with a case expression, and check what readMaybe accepts.

07 / Try it

Can you predict where a chain of steps stops?

Predict each line. Then say which typed text the third line never reads.

Main.hs
import Data.Maybe (mapMaybe)
import Text.Read (readMaybe)

-- Days each kind of item may be borrowed.
loanDays :: [(String, Int)]
loanDays = [("book", 21), ("dvd", 7)]

daysLeft :: String -> String -> Maybe Int
daysLeft kind typed = do
  allowed <- lookup kind loanDays
  used <- readMaybe typed
  if used > allowed then Nothing else Just (allowed - used)

main :: IO ()
main = do
  print (daysLeft "dvd" " 3 ")
  print (daysLeft "map" "two")
  print (mapM (daysLeft "book") ["5", "30", "x"])
  print (mapMaybe (daysLeft "book") ["5", "30", "x"])

What does the program print, and which text does mapM never read?

Reveal the reasoning

Just 4, Nothing, Nothing, and [16]. readMaybe accepts the spaces around 3, and 7 - 3 is 4. "map" is not in loanDays, so the first line of the do block gives Nothing and "two" is never read. mapM gives Just 16 for "5", then Nothing for "30", because 30 is more than 21, and stops there: it never reads "x". mapMaybe tries all three, keeps 16, and drops the Nothing for "30" and for "x".

Practice tickets

  1. HS-801 · The print kiosk stops when a visitor types a word → Free
  2. HS-802 · Inter-library boxes leave with books missing → Sign in · Pro
  3. HS-803 · A new gallery with no ratings stops the staff screen → Sign in · Pro

Keep exploring

I/O, buffering, and external state →

Primary references