Polyglot
Languages Haskell conditionals
Haskell § conditionals

Conditionals

Haskell’s conditional constructs are expressions, not statements: every form yields a value. The principal forms are if … then … else, the case expression, and guards in function definitions. Pattern matching is integrated throughout — every conditional admits patterns alongside boolean tests. The expression-orientation has the consequence that if must have an else branch (the language has no statement form that lets the conditional yield no value), and the choice between if/then/else, case, and guards is largely stylistic. Modern code tends to favour case and guards for non-trivial discriminations and if only for simple boolean conditions.

This page covers the principal selection constructs; the Pattern matching page covers the pattern grammar in detail.

if / then / else

The simplest form:

abs :: Int -> Int
abs n = if n < 0 then negate n else n

classify :: Int -> String
classify n =
    if n > 0
        then "positive"
        else if n < 0
            then "negative"
            else "zero"

The if expression has three parts:

  • The condition — a Bool-valued expression.
  • The then-branch — the value if the condition is True.
  • The else-branch — the value if the condition is False.

Both branches are required and must have the same type; the result type is that common type.

The cascading form if … then … else if … is admissible but rarely the conventional choice; case or guards (treated below) are typically clearer for multi-way conditionals.

Boolean-only condition

The condition must be of type Bool. Haskell does not admit C-style truthy values: 0, null, "" are not false:

if 0 then "yes" else "no"     -- ERROR: 0 is not a Bool

n :: Int
if n then "yes" else "no"     -- ERROR: n is Int, not Bool

if n /= 0 then "yes" else "no"  -- OK

The strict typing eliminates an entire class of errors that C-family programmers learn to guard against.

Guards

A guard is a boolean condition attached to a function equation:

abs :: Int -> Int
abs n
    | n < 0     = negate n
    | otherwise = n

classify :: Int -> String
classify n
    | n > 0     = "positive"
    | n < 0     = "negative"
    | otherwise = "zero"

Each guard begins with | and ends with =. The guards are evaluated top-to-bottom; the first one that evaluates to True selects the corresponding right-hand side.

otherwise is just True — a name in the standard library:

otherwise :: Bool
otherwise = True

The convention is to use otherwise for the catch-all case, which reads more naturally than True.

Guards may include multiple conditions:

classify :: Int -> Int -> String
classify x y
    | x == y && x > 0  = "both positive equal"
    | x == y           = "equal"
    | x > y            = "first greater"
    | otherwise        = "second greater"

If no guard matches, the equation is considered to fail, and the next equation (if any) is tried. If no equation in the function matches, a runtime error is produced — Non-exhaustive patterns in function f.

case expressions

The case expression admits arbitrary pattern matching against an expression’s value:

classify :: Int -> String
classify n = case n of
    0 -> "zero"
    1 -> "one"
    _ -> "many"

shapeName :: Shape -> String
shapeName s = case s of
    Circle _      -> "circle"
    Square _      -> "square"
    Rectangle _ _ -> "rectangle"

The form: case <expr> of { <pattern> -> <result>; ... }. The <expr> is evaluated; the patterns are tried in order; the first matching pattern’s right-hand side is the result.

The case expression is the conventional Haskell mechanism for value-driven selection. Function-equation patterns desugar to case:

-- These are equivalent:
factorial 0 = 1
factorial n = n * factorial (n - 1)

factorial n = case n of
    0 -> 1
    _ -> n * factorial (n - 1)

The function-equation form is shorter; the case form is necessary when the discrimination is on something other than the function’s argument (e.g., on a derived value).

Patterns and guards together

A case arm may have guards:

classify :: Int -> String
classify n = case n of
    0 -> "zero"
    n
        | n > 0 -> "positive"
        | otherwise -> "negative"

The n pattern matches anything; the guards then refine. The form is the conventional way to combine type/constructor matching with value comparisons.

LambdaCase

The LambdaCase extension admits a shorthand for \x -> case x of …:

{-# LANGUAGE LambdaCase #-}

classify :: Int -> String
classify = \case
    0 -> "zero"
    n | n > 0 -> "positive"
      | otherwise -> "negative"

The form is the conventional Haskell idiom for a single-argument function that immediately pattern-matches.

The MultiWayIf extension (rarely used) admits if-expressions with multiple guard arms without an explicit case:

{-# LANGUAGE MultiWayIf #-}

classify n = if
    | n > 0     -> "positive"
    | n < 0     -> "negative"
    | otherwise -> "zero"

The form is occasionally clearer than the cascading if … then … else if … chain.

The conditional operator

Haskell does not have a dedicated ternary ?: operator. The if/then/else is itself the equivalent:

greater = if x > y then x else y

The if-expression form is conventional and reads naturally. Some libraries provide a bool function that takes the false-branch first:

import Data.Bool (bool)

bool :: a -> a -> Bool -> a
bool f t b = if b then t else f

The function admits bool x y cond as an alternative to if cond then y else x; it is mostly used for point-free style.

Pattern matching as conditional

Pattern matching is itself a discrimination mechanism. Function definitions with multiple equations are conventionally the conditional form:

factorial :: Int -> Int
factorial 0 = 1
factorial n = n * factorial (n - 1)

length :: [a] -> Int
length []     = 0
length (_:xs) = 1 + length xs

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

The mechanism subsumes much of what other languages express with if/switch. The full treatment is in Pattern matching.

Selection idioms

Several patterns recur in idiomatic Haskell.

Early return through pattern matching

parse :: String -> Either String Int
parse "" = Left "empty input"
parse s = case reads s of
    [(n, "")] -> Right n
    _         -> Left ("could not parse: " ++ s)

The pattern matching admits handling each case directly; there is no if/else cascade.

Validate-and-process via guards

process :: Int -> String -> Result
process n s
    | n < 0          = errorResult "negative count"
    | length s == 0  = errorResult "empty string"
    | length s > 100 = errorResult "string too long"
    | otherwise      = computeResult n s

Guards admit discriminating on multiple conditions concisely. The conventional contemporary form for “try several things and produce the first matching error”.

Maybe-driven branching

greet :: Maybe String -> String
greet Nothing     = "Hello, stranger."
greet (Just name) = "Hello, " ++ name ++ "."

Pattern matching on Maybe is the conventional Haskell idiom for “value or absence” — substantially clearer than C-family null checks.

case on a derived value

process :: User -> Action
process user = case userStatus user of
    Active   -> normalProcess user
    Banned   -> rejectProcess user
    Pending  -> queueProcess user

The case discriminates on the result of userStatus, not on the function’s argument directly.

Folding Maybe and Either

The standard library provides several “fold-like” functions on Maybe and Either:

maybe :: b -> (a -> b) -> Maybe a -> b
maybe d _ Nothing  = d
maybe _ f (Just x) = f x

either :: (a -> c) -> (b -> c) -> Either a b -> c
either f _ (Left x)  = f x
either _ g (Right x) = g x

fromMaybe :: a -> Maybe a -> a
fromMaybe d Nothing  = d
fromMaybe _ (Just x) = x

The functions admit handling Maybe/Either without an explicit case:

maybe "stranger" (\n -> "Hello, " ++ n) name
fromMaybe 0 maybeCount
either (const 0) id maybeError

The pattern is the conventional Haskell idiom for the cases where the explicit case would be unnecessarily verbose.

A note on the absence of statements

Haskell does not have if as a statement (the if x then y without an else form does not exist). Every if is an expression that yields a value, so both branches are required. The implication: discriminations that don’t naturally fit into the expression form (e.g., “do this if the condition is true; otherwise do nothing”) are typically expressed as monadic when:

import Control.Monad (when, unless)

when :: Monad m => Bool -> m () -> m ()
when True  action = action
when False _      = return ()

unless :: Monad m => Bool -> m () -> m ()
unless = when . not

In an IO context:

main = do
    n <- readInt
    when (n > 0) $ do
        putStrLn "positive"
    unless (n == 0) $ do
        putStrLn "non-zero"

The when and unless functions are the conventional Haskell idiom for conditional effects.

For pure code, the case and if expressions are the principal conditional mechanism; for effectful code, when and unless admit the conditional-effect pattern. The choice is determined by whether the conditional should yield a value.