Skip to content
AgileLogics
6 min read

Maybe and Either

Prerequisites: Algebraic data types

Haskell has no null. When a value might be missing, the type says so.

Maybe

-- illustrative: this is how the standard library defines it
data Maybe a = Nothing | Just a
import Data.Maybe (fromMaybe, mapMaybe, catMaybes)
import Text.Read (readMaybe)

safeDiv :: Int -> Int -> Maybe Int
safeDiv _ 0 = Nothing
safeDiv a b = Just (a `div` b)

describe :: Maybe Int -> String
describe Nothing = "no result"
describe (Just n) = "result: " ++ show n

main :: IO ()
main = do
  putStrLn (describe (safeDiv 10 2))
  putStrLn (describe (safeDiv 1 0))
  print (fromMaybe 0 (safeDiv 1 0))
  print (maybe "none" show (Just 42))
  print (mapMaybe (\s -> readMaybe s :: Maybe Int) ["1", "x", "3"])
  print (catMaybes [Just 1, Nothing, Just 3])
  print (lookup "b" [("a", 1), ("b", 2)])
result: 5
no result
0
"42"
[1,3]
[1,3]
Just 2
  • A function returning Maybe Int must be handled by the caller: you can't accidentally use a Maybe Int as an Int. That's how Haskell eliminates null-pointer errors.
  • fromMaybe default m and maybe default f m unwrap with a fallback.
  • mapMaybe maps and drops the Nothings in one step.

Either

Maybe says that something failed; Either says why:

-- illustrative: the standard definition
data Either a b = Left a | Right b

By convention Left holds an error and Right holds the result ("right" as in correct):

data AgeError = NotANumber String | Negative Int | TooOld Int
  deriving (Show)

parseAge :: String -> Either AgeError Int
parseAge s = case reads s of
  [(n, "")]
    | n < 0 -> Left (Negative n)
    | n > 150 -> Left (TooOld n)
    | otherwise -> Right n
  _ -> Left (NotANumber s)

main :: IO ()
main = do
  print (parseAge "36")
  print (parseAge "-3")
  print (parseAge "200")
  print (parseAge "abc")
  print (either show (\n -> "ok: " ++ show n) (parseAge "36"))
Right 36
Left (Negative (-3))
Left (TooOld 200)
Left (NotANumber "abc")
"ok: 36"

Using your own error type (AgeError) instead of a String means callers can pattern match on exactly what went wrong.

Chaining without nesting

Handling each Nothing with a case quickly nests. The next lessons show how Functor, Applicative, and Monad chain these computations cleanly — for now, a preview:

import Text.Read (readMaybe)

addStrings :: String -> String -> Maybe Int
addStrings a b = do
  x <- readMaybe a
  y <- readMaybe b
  return (x + y)

main :: IO ()
main = do
  print (addStrings "2" "40")
  print (addStrings "2" "forty")
  print (fmap (* 2) (Just 21))
Just 42
Nothing
Just 42

The do block stops at the first Nothing — no explicit checks needed.

Practice

Write safeRoot :: Double -> Either String Double that fails with a message for negative numbers, and print it for 16 and -4.

Show solution
safeRoot :: Double -> Either String Double
safeRoot x
  | x < 0 = Left ("cannot take the root of " ++ show x)
  | otherwise = Right (sqrt x)

main :: IO ()
main = do
  print (safeRoot 16)
  print (safeRoot (-4))
Right 4.0
Left "cannot take the root of -4.0"