पाठ 9 / 25

Maybe and Either

Represent missing values and errors with Maybe and Either.

No null, no surprise exceptions

Haskell has no null. A value that may be absent has type Maybe a, which is either Just x or Nothing, and the compiler forces you to handle both cases. Functions such as lookup, Data.Map.lookup, find and Text.Read.readMaybe return Maybe. Either e a represents a computation that may fail with an error: Left err or Right value, by convention with the error on the left. Useful helpers: maybe default f m and fromMaybe, either onLeft onRight, mapMaybe and catMaybes to drop missing values, and traverse to validate a whole list. Because Maybe and Either are monads, you can chain several fallible steps with do notation, and the chain stops at the first Nothing or Left (the next section explains why). Use Maybe when the reason for absence is obvious, and Either with a descriptive error type (often a sum type of error cases) when callers need to know what went wrong. Reserve exceptions for truly exceptional situations in IO.

Validating a signup with Either

Each step can fail; do notation stops at the first Left.

import Text.Read (readMaybe)
import Data.Maybe (mapMaybe, fromMaybe)
import Data.Char (isSpace)

data SignupError = MissingName | InvalidEmail String | InvalidAge String
  deriving (Show, Eq)

data Signup = Signup { name :: String, email :: String, age :: Int }
  deriving Show

trim :: String -> String
trim = reverse . dropWhile isSpace . reverse . dropWhile isSpace

validName :: String -> Either SignupError String
validName n = if null (trim n) then Left MissingName else Right (trim n)

validEmail :: String -> Either SignupError String
validEmail e = if '@' `elem` e then Right e else Left (InvalidEmail e)

validAge :: String -> Either SignupError Int
validAge raw = case readMaybe raw of
  Just a | a >= 13 && a <= 120 -> Right a
  _                            -> Left (InvalidAge raw)

signup :: String -> String -> String -> Either SignupError Signup
signup n e a = do
  n' <- validName n
  e' <- validEmail e
  a' <- validAge a
  pure (Signup n' e' a')

main :: IO ()
main = do
  print (signup "Asha" "asha@example.com" "29")   -- Right (Signup {...})
  print (signup "Ravi" "ravi" "x")                -- Left (InvalidEmail "ravi")
  print (mapMaybe readMaybe ["1", "two", "3"] :: [Int])   -- [1,3]
  print (fromMaybe 0 (lookup "ink" [("pen", 120), ("ink", 40 :: Int)]))   -- 40

Avoid fromJust

fromJust Nothing crashes, reintroducing the null-pointer problem Maybe was meant to solve. Use pattern matching, maybe, fromMaybe or do notation instead.

त्वरित जाँच: In `do { a <- step1; b <- step2 a; pure (a, b) }` with Either, what happens if step1 returns Left err?

  • step2 still runs
  • It throws an exception
  • The whole expression is Left err and later steps do not run
  • It returns Right ()
Answer

The whole expression is Left err and later steps do not run — Either's monad short-circuits on the first Left.