In functional programming, computation is expressed through expressions rather than statements, which is a fundamental difference from imperative languages.
(.) :: (b -> c) -> (a -> b) -> (a -> c)f . g = \x -> f (g x)
This enables point-free style programming:
-- With explicit parameter\x -> show (add10 x)-- Point-free styleshow . add10
Equations are not Assignments
In imperative languages, we might write:
x = x + 1 // Modify x by adding 1
In Haskell, an equation like x = x + 1 is not an assignment; it’s a recursive definition that tries to define x as its successor, which would lead to an infinite loop.
area :: Shape -> Floatarea (Circle r) = pi * r * rarea (Rectangle w h) = w * harea (Triangle a b c) = sqrt (s * (s - a) * (s - b) * (s - c)) where s = (a + b + c) / 2 -- Semi-perimeter
Pattern matching and recursion are core techniques in functional programming that work hand-in-hand to process data structures and implement algorithms.
data Shape = Circle Float | Rectangle Float Floatarea :: Shape -> Floatarea (Circle r) = pi * r * rarea (Rectangle w h) = w * hdata Tree a = Leaf | Node a (Tree a) (Tree a)treeDepth :: Tree a -> InttreeDepth Leaf = 0treeDepth (Node _ left right) = 1 + max (treeDepth left) (treeDepth right)
Pattern Guards
Pattern guards add conditions to patterns:
absoluteValue :: Int -> IntabsoluteValue n | n < 0 = -n | otherwise = ngrade :: Int -> Stringgrade score | score >= 90 = "A" | score >= 80 = "B" | score >= 70 = "C" | score >= 60 = "D" | otherwise = "F"
case Expressions
Pattern matching can also be done within expressions using case:
describePair :: (Int, Int) -> StringdescribePair pair = case pair of (0, 0) -> "Origin" (0, _) -> "Y-axis" (_, 0) -> "X-axis" (x, y) -> "Point at (" ++ show x ++ "," ++ show y ++ ")"
length' :: [a] -> Intlength' [] = 0 -- Base caselength' (_:xs) = 1 + length' xs -- Recursive casemap' :: (a -> b) -> [a] -> [b]map' _ [] = [] -- Base casemap' f (x:xs) = f x : map' f xs -- Recursive casefilter' :: (a -> Bool) -> [a] -> [a]filter' _ [] = [] -- Base casefilter' p (x:xs) -- Recursive case | p x = x : filter' p xs | otherwise = filter' p xs
Recursive Numerical Functions
-- Factorialfactorial :: Integer -> Integerfactorial 0 = 1 -- Base casefactorial n = n * factorial (n-1) -- Recursive case-- Fibonaccifibonacci :: Int -> Integerfibonacci 0 = 0 -- Base case 1fibonacci 1 = 1 -- Base case 2fibonacci n = fibonacci (n-1) + fibonacci (n-2) -- Recursive case
-- Sum all values in a treetreeSum :: Tree Int -> InttreeSum Leaf = 0treeSum (Node value left right) = value + treeSum left + treeSum right-- Map a function over all values in a treetreeMap :: (a -> b) -> Tree a -> Tree btreeMap _ Leaf = LeaftreeMap f (Node value left right) = Node (f value) (treeMap f left) (treeMap f right)
Higher-order functions (HOFs) are functions that take other functions as arguments or return functions as results. They are a powerful abstraction tool in functional programming.
Property-based testing is an approach that verifies that properties of a program hold true for a wide range of inputs. In Haskell, the primary tool for property-based testing is the QuickCheck library. This approach is especially useful for testing functions that operate on lists (Lists and List Comprehensions) or that involve algebraic laws (Equational Reasoning).
Core Concepts
What is Property-Based Testing?
Property-based testing:
Tests that certain properties of your code hold across a range of inputs
Generates random test data automatically
Tries to find the simplest counterexample if a property fails
Provides a systematic way to test code without writing individual test cases
Advantages over Unit Testing
Compared to traditional unit testing:
Tests behavior rather than specific cases
Explores edge cases you might not have considered
Often requires less code to test more thoroughly
Finds bugs that manual test cases might miss
QuickCheck Basics
Installation
QuickCheck can be installed using Cabal:
cabal updatecabal install --lib QuickCheck
Importing QuickCheck
import Test.QuickCheck
Defining Properties
A property in QuickCheck is a function that returns a Bool or a Property value:
Sometimes we need to limit the property to certain kinds of inputs:
Using Conditional Properties
prop_divisionInverse :: Int -> Int -> Propertyprop_divisionInverse x y = y /= 0 ==> (x `div` y) * y + (x `mod` y) == x
Using Generators
prop_divisionInverse2 :: Int -> NonZero Int -> Boolprop_divisionInverse2 x (NonZero y) = (x `div` y) * y + (x `mod` y) == x
Custom Generators
For more control, we can define our own generators:
genEven :: Gen IntgenEven = do n <- arbitrary return (n * 2)prop_evenNumberIsEven :: Propertyprop_evenNumberIsEven = forAll genEven (\n -> even n)
Testing Example: A Sorting Function
Let’s verify that a sorting function behaves correctly:
-- Properties for a sorting functionprop_sortOrders :: [Int] -> Boolprop_sortOrders xs = ordered (sort xs) where ordered [] = True ordered [_] = True ordered (x:y:zs) = x <= y && ordered (y:zs)prop_sortPreservesLength :: [Int] -> Boolprop_sortPreservesLength xs = length (sort xs) == length xsprop_sortPreservesElements :: [Int] -> Boolprop_sortPreservesElements xs = all (`elem` xs) (sort xs) && all (`elem` sort xs) xs
Finding Bugs with QuickCheck
When a property fails, QuickCheck attempts to shrink the counterexample to a minimal case, which is useful for debugging recursive functions (Pattern Matching and Recursion):
Define simple, specific properties that can be checked independently
Think about edge cases and invariants in your functions
Test algebraic laws when appropriate (associativity, commutativity, etc.)
Use appropriate generators for your data types
Combine property-based testing with unit testing for critical code paths
Key Points to Remember
QuickCheck generates random test cases to verify properties of your code
Properties are expressed as functions that return Bool or Property values
When a property fails, QuickCheck tries to find the simplest counterexample
Custom generators can be defined for specific test data requirements
Property-based testing is especially powerful for functional code, where properties and invariants are well-defined
Common properties to test include identity properties (e.g., reverse (reverse xs) == xs), which relate to the concept of pure functions (Functions and Equations), and algebraic properties such as associativity of list append (++), which is a key law for monads (Monads Basics) and functors/applicatives (Functors and Applicatives).
When testing functions that manipulate lists, such as map, filter, or sort, you are often indirectly testing higher-order functions (Higher-Order Functions).
Best practices for property-based testing include thinking about invariants and algebraic laws, which are also central to Equational Reasoning and Monad Laws.
Evaluation strategies determine when expressions are evaluated during program execution. Haskell uses lazy evaluation by default, which has significant implications for how programs behave.
Lazy vs. Eager Evaluation
Eager (Strict) Evaluation
In eager (strict) evaluation:
Arguments to functions are evaluated before the function is called
Expressions are evaluated as soon as they are bound to variables
Most common programming languages (C, Java, Python, etc.) use eager evaluation
Example in Python (eager):
def f(x, y): return x + 2# Both arguments are evaluated, even though y is never usedresult = f(3, expensive_computation())
Lazy (Non-strict) Evaluation
In lazy (non-strict) evaluation:
Expressions are only evaluated when their values are needed
Arguments to functions are not evaluated until they are used inside the function
Evaluation is delayed until the last possible moment (“call by need”)
Example in Haskell (lazy):
f :: Int -> Int -> Intf x y = x + 2-- y is never evaluated because it's not used in the function bodyresult = f 3 (expensive_computation)
Benefits of Lazy Evaluation
Infinite Data Structures
Lazy evaluation allows for infinite data structures:
-- Infinite list of natural numbersnaturals = [0..]-- We can operate on infinite liststake 5 naturals -- [0,1,2,3,4]take 10 (filter even naturals) -- [0,2,4,6,8,10,12,14,16,18]
Short-Circuit Evaluation
Functions can terminate early without evaluating all arguments:
-- Returns first element of the list that satisfies the predicatefind :: (a -> Bool) -> [a] -> Maybe afind p [] = Nothingfind p (x:xs) | p x = Just x -- Terminates here if predicate is satisfied | otherwise = find p xs-- Only computes until it finds the first prime number greater than 100find (> 100) primes
Performance Optimization
Lazy evaluation can lead to performance optimizations:
Avoiding unnecessary computations:
if condition then expensiveComputation1 else expensiveComputation2
Only one of the expensive computations will be evaluated, depending on the condition.
Fusion transformations:
sum (map (*2) [1..1000000])
Can be optimized to avoid creating the intermediate list.
Better Modularity
Lazy evaluation allows for better separation of concerns:
-- Producer generates all possible solutionssolutions = generateAllPossibleSolutions problem-- Consumer takes only what it needsfirstSolution = head solutionsbestSolution = minimumBy compareCost solutions
Drawbacks of Lazy Evaluation
Unpredictable Space Usage
Lazy evaluation can lead to space leaks if thunks (unevaluated expressions) build up:
sum [1..1000000] -- Can use a lot of memory due to thunk accumulation
Harder to Reason About Performance
It can be difficult to predict exactly when expressions will be evaluated.
Not Always More Efficient
For code that needs to evaluate everything anyway, the overhead of tracking thunks can make lazy evaluation less efficient.
Thunks
A thunk is a suspended computation - a promise to compute a value when needed.
Thunks are created when expressions are not immediately evaluated
They store all data needed to evaluate the expression later
When evaluation is needed, the thunk is forced and replaced with the result
Forcing Evaluation
Bang Patterns
Bang patterns (!) can be used to force evaluation:
-- This version of sum is strict in its accumulatorsum' :: [Int] -> Intsum' = go 0 where go !acc [] = acc go !acc (x:xs) = go (acc + x) xs
Seq Function
The seq function forces evaluation of its first argument before returning the second:
seq :: a -> b -> b-- Forces evaluation of x before returning yx `seq` y
Example usage:
sum' :: [Int] -> Intsum' xs = go 0 xs where go acc [] = acc go acc (x:xs) = let acc' = acc + x in acc' `seq` go acc' xs
Strictness Annotations
Data types can be made strict with strictness annotations:
data Pair a b = Pair !a !b
This forces both fields to be evaluated when the constructor is evaluated.
Common Patterns
Lazy Functions vs. Strict Data
A common pattern in Haskell is to write functions that are lazy in their arguments but strict in their data fields:
-- Infinite list of onesones = 1 : ones-- Infinite list of Fibonacci numbersfibs = 0 : 1 : zipWith (+) fibs (tail fibs)-- Primes using the Sieve of Eratosthenesprimes = sieve [2..] where sieve (p:xs) = p : sieve [x | x <- xs, x `mod` p /= 0]
Infinite Trees
data Tree a = Node a (Tree a) (Tree a)-- Infinite binary tree where each node contains its pathpaths = build [] where build path = Node path (build (False:path)) (build (True:path))
Key Points to Remember
Haskell uses lazy evaluation by default: expressions are only evaluated when needed
Lazy evaluation enables infinite data structures and certain programming patterns
Thunks are delayed computations that are evaluated on demand
Strict evaluation can be enforced using bang patterns, seq, or strictness annotations
Space leaks are a common issue with lazy evaluation, requiring careful attention to performance
Polymorphism (meaning “many forms”) allows code to work with values of different types. Haskell supports two main kinds of polymorphism: parametric polymorphism and ad-hoc polymorphism (via typeclasses).
Parametric Polymorphism
Parametric polymorphism allows functions to operate on values of any type, without knowing or caring what that type is.
Type Variables
Type variables (usually lowercase letters like a, b, c) represent arbitrary types:
id :: a -> a -- Identity function works on any typelength :: [a] -> Int -- Length works on lists of any typereverse :: [a] -> [a] -- Reverse works on lists of any typefst :: (a, b) -> a -- First works on any pair of types
Examples of Parametrically Polymorphic Functions
-- Identity functionid :: a -> aid x = x-- Apply a function twicetwice :: (a -> a) -> a -> atwice f x = f (f x)-- Swap elements in a pairswap :: (a, b) -> (b, a)swap (x, y) = (y, x)
Parametrically Polymorphic Data Types
Data types can also be parametrically polymorphic:
-- List of elements of type adata List a = Empty | Cons a (List a)-- Binary tree with values of type adata Tree a = Leaf | Node a (Tree a) (Tree a)-- Maybe type (optional value)data Maybe a = Nothing | Just a-- Either type (value of one type or another)data Either a b = Left a | Right b
Parametricity
Parametricity is a property of parametrically polymorphic functions: the behavior of a function is determined solely by its type signature, not by the types it operates on.
For example, a function with type [a] -> Int can only compute properties like length - it cannot inspect the elements themselves because it knows nothing about type a.
Consequences of Parametricity
Free theorems: We get certain properties “for free” based on the type signature
Limited operations: Functions can only perform operations consistent with all possible types
Predictable behavior: Functions behave uniformly across all types
Example: Functions of Type a -> a
How many functions can have the type a -> a?
With parametricity, there’s only one possible function: the identity function id x = x.
This is because any function of type a -> a must work the same way for all types, and the only thing it can do is return its input unchanged.
Ad-hoc Polymorphism via Typeclasses
Ad-hoc polymorphism allows functions to behave differently depending on the type of their arguments. In Haskell, this is accomplished with typeclasses.
What are Typeclasses?
A typeclass defines a set of functions that must be implemented by any type that wants to be a member of that class.
class Show a where show :: a -> String
This says: “For a type a to be a member of the Show class, it must implement the show function.”
Typeclass Instances
Types become members of a typeclass by providing implementations of the required functions:
data Color = Red | Green | Blueinstance Show Color where show Red = "Red" show Green = "Green" show Blue = "Blue"
Typeclass Constraints
Functions can require that their type parameters belong to specific typeclasses:
-- A function that works on any type that can be shownprintTwice :: Show a => a -> IO ()printTwice x = do putStrLn (show x) putStrLn (show x)
The Show a => part is a typeclass constraint, saying “this works for any type a that is a member of the Show class.”
Common Typeclasses
Eq - Equality Testing
class Eq a where (==) :: a -> a -> Bool (/=) :: a -> a -> Bool -- Default implementations x /= y = not (x == y) x == y = not (x /= y)
Example instance:
instance Eq Color where Red == Red = True Green == Green = True Blue == Blue = True _ == _ = False
Ord - Ordering
class Eq a => Ord a where compare :: a -> a -> Ordering (<), (<=), (>), (>=) :: a -> a -> Bool max, min :: a -> a -> a -- Default implementations provided
Ord has Eq as a superclass, meaning any type that is Ord must also be Eq.
Num - Numeric Types
class Num a where (+), (-), (*) :: a -> a -> a negate, abs, signum :: a -> a fromInteger :: Integer -> a
This allows numeric operations on various types (Int, Float, Double, etc.).
Show - String Conversion
class Show a where show :: a -> String
Read - Parsing from Strings
class Read a where read :: String -> a
deriving Mechanism
For common typeclasses, Haskell can automatically generate instances:
data Color = Red | Green | Blue deriving (Show, Eq, Ord, Enum)
This automatically implements:
show for Show
== and /= for Eq
compare, <, etc. for Ord
succ, pred, etc. for Enum
Custom Typeclasses
You can define your own typeclasses:
class Drawable a where draw :: a -> IO () clear :: a -> IO ()data Shape = Circle Float | Rectangle Float Floatinstance Drawable Shape where draw (Circle r) = putStrLn $ "Drawing circle with radius " ++ show r draw (Rectangle w h) = putStrLn $ "Drawing rectangle " ++ show w ++ "x" ++ show h clear _ = putStrLn "Clearing shape"
Comparing Parametric and Ad-hoc Polymorphism
Parametric Polymorphism
Ad-hoc Polymorphism (Typeclasses)
One implementation works for all types
Different implementations for different types
Functions operate on the structure, not the content
Functions can inspect and operate on values
”Uniform” behavior across types
Type-specific behavior
Examples: id, map, reverse
Examples: show, (+), (==)
Key Points to Remember
Parametric polymorphism uses type variables to create functions that work on any type
Parametricity restricts what polymorphic functions can do, leading to predictable behavior
Typeclasses enable ad-hoc polymorphism, allowing different implementations for different types
Common typeclasses include Eq, Ord, Show, Read, and Num
The deriving mechanism automates the creation of typeclass instances
Input/Output (IO) operations present a unique challenge in a pure functional language like Haskell. This page explains how Haskell manages side effects while maintaining purity.
The Problem of Side Effects
Pure Functions
A pure function:
Always returns the same result when given the same arguments
Has no observable side effects
Does not depend on external state
In mathematical terms, pure functions are referentially transparent: any expression can be replaced with its value without changing the program’s behavior.
Side Effects
Side effects include:
Reading/writing files
Network operations
Getting user input
Printing to the console
Getting the current time
Generating random numbers
Modifying mutable data structures
These operations depend on or modify external state, breaking referential transparency.
The Challenge
How can a pure functional language perform impure operations like IO while maintaining its mathematical foundation?
The IO Type
Haskell solves this problem using the IO type, which represents computations that might perform side effects.
The IO type constructor wraps the type of the value that the IO action will produce:
IO () - An IO action that produces no useful result (just the unit value ())
IO String - An IO action that produces a String
IO Int - An IO action that produces an Int
Key Insight
An IO a value doesn’t represent a value of type a; it represents a recipe or description for obtaining a value of type a (potentially with side effects).
The Haskell runtime system executes these recipes when the program runs.
Basic IO Operations
Printing to the Console
putStr :: String -> IO () -- Print without a newlineputStrLn :: String -> IO () -- Print with a newlineprint :: Show a => a -> IO () -- Print any showable value
Reading from the Console
getChar :: IO Char -- Read a single charactergetLine :: IO String -- Read a line of text
do notation allows us to sequence IO actions and use their results:
greeting :: IO ()greeting = do putStrLn "What's your name?" name <- getLine putStrLn $ "Hello, " ++ name ++ "!"
This code:
Prints a question
Waits for user input and binds the result to name
Prints a greeting using the name
Key Components of do Notation
Action Sequencing: Actions are executed in order from top to bottom
Result Binding: name <- getLine binds the result of getLine to the variable name
Let Bindings: let x = expression defines a pure value within the do block
Return: return value creates an IO action that produces value without any side effects
example :: IO Intexample = do x <- readFile "number.txt" let n = read x :: Int let doubled = n * 2 putStrLn $ "Doubled number: " ++ show doubled return doubled -- The final result of the IO action
The main Function
Every Haskell program has a main function:
main :: IO ()main = do putStrLn "Hello, world!"
The Haskell runtime system executes the IO actions described by main.
Combining Pure and Impure Code
Pure functions cannot directly use IO results, and IO actions cannot directly use pure functions. Instead, we:
Extract values from IO actions using <- in a do block
Process them with pure functions
Wrap results back in IO actions if needed
processFile :: FilePath -> IO StringprocessFile path = do content <- readFile path -- Impure: read file let processed = map toUpper content -- Pure: process with function return processed -- Wrap result in IO
The return Function
return lifts a pure value into an IO action:
return :: a -> IO a
return x creates an IO action that produces x without performing any actual IO.
Common Patterns
Reading and Processing Input
readAndProcess :: IO ()readAndProcess = do input <- getLine let processed = process input -- Pure function putStrLn processed
Interactive Loops
loop :: IO ()loop = do input <- getLine if input == "quit" then return () -- End the loop else do putStrLn $ "You entered: " ++ input loop -- Recursive call for the next iteration
Error Handling
import System.IO.ErrorsafeReadFile :: FilePath -> IO (Either IOError String)safeReadFile path = do result <- try (readFile path) return result
State - Computations that maintain state (Common Monads)
And many more…
The key insight is that monads provide a consistent interface for working with these contexts. See Monad Laws for the rules that all monads must follow.
The Monad Typeclass
The Monad typeclass is defined as:
class Applicative m => Monad m where return :: a -> m a (>>=) :: m a -> (a -> m b) -> m b -- pronounced "bind" (>>) :: m a -> m b -> m b -- pronounced "then" -- Default implementations m >> n = m >>= \_ -> n
Note: The Applicative constraint is part of the modern definition of the Monad typeclass. For this course, you can focus on return and (>>=). See Functors and Applicatives for more on the relationship between these abstractions.
Core Functions
return - Takes a value and wraps it in the monad
(>>=) (bind) - Sequences two monadic operations, passing the result of the first to the second
(>>) (then) - Sequences two monadic operations, discarding the result of the first
Understanding Bind (>>=)
The bind operator is the heart of monads:
(>>=) :: m a -> (a -> m b) -> m b
It allows us to:
Take a monadic value (m a)
Apply a function that takes a normal value and returns a monadic value (a -> m b)
Get a new monadic value (m b)
Think of it as “extracting” a value from a context, applying a function, and putting the result back into the context.
instance Monad Maybe where return x = Just x Nothing >>= _ = Nothing (Just x) >>= f = f x
This captures the idea that if any computation fails (returns Nothing), the entire sequence fails.
Example with Maybe
-- Safe divisionsafeDiv :: Int -> Int -> Maybe IntsafeDiv _ 0 = NothingsafeDiv x y = Just (x `div` y)-- Using bind to chain operationscomputation :: Int -> Int -> Int -> Maybe Intcomputation x y z = safeDiv x y >>= \result1 -> safeDiv result1 z >>= \result2 -> return (result2 * 2)
If either division fails, the entire computation returns Nothing.
Example: The List Monad
The list monad represents non-deterministic computations (Common Monads):
instance Monad [] where return x = [x] xs >>= f = concat (map f xs)
This captures the idea of exploring all possible outcomes.
Haskell provides syntactic sugar called do notation to make working with monads more readable. See Monad Laws for how do notation is translated to uses of >>= and return.
computation :: Int -> Int -> Int -> Maybe Intcomputation x y z = do result1 <- safeDiv x y result2 <- safeDiv result1 z return (result2 * 2)
Translation Rules
do notation is translated to uses of >>= and return:
do notation
Translation
do { x <- mx; rest }
mx >>= \x -> do { rest }
do { let x = v; rest }
let x = v in do { rest }
do { mx; rest }
mx >> do { rest }
do { mx }
mx
The Identity Monad
The simplest monad is the Identity monad, which adds no computational effects. See Monad Transformers for how this is used as a base for more complex monads.
newtype Identity a = Identity { runIdentity :: a }instance Monad Identity where return x = Identity x (Identity x) >>= f = f x
It’s useful primarily for understanding monadic concepts and as a base for monad transformers.
Pattern: Building Computations with Monads
Monads allow us to build complex computations by sequencing smaller ones. This works for any monad, whether Maybe, List, IO, etc. (Common Monads, Introduction to IO)
complexComputation :: Monad m => m a -> m b -> (a -> b -> m c) -> m ccomplexComputation ma mb combine = do a <- ma b <- mb combine a b
This page explores several common monads in Haskell and their practical applications. For a general introduction, see Monads Basics. For laws and properties, see Monad Laws. For combining monads, see Monad Transformers.
-- Safe lookup in a listsafeLookup :: Int -> [a] -> Maybe asafeLookup _ [] = NothingsafeLookup 0 (x:_) = Just xsafeLookup n (_:xs) = safeLookup (n-1) xs-- Chain computations with potential failureslookupAndProcess :: [Int] -> Int -> Maybe IntlookupAndProcess list idx = do value <- safeLookup idx list if value > 0 then Just (value * 2) else Nothing
-- Generate all possible dice rollsdiceRolls :: Int -> [Int]diceRolls n = do -- Roll n dice rolls <- replicateM n [1..6] -- Return the sum return (sum rolls)-- All pythagorean triples with components less than npythagoreanTriples :: Int -> [(Int, Int, Int)]pythagoreanTriples n = do a <- [1..n] b <- [a..n] -- Ensure b >= a c <- [b..n] -- Ensure c >= b guard (a*a + b*b == c*c) -- Only keep results that satisfy the equation return (a, b, c)
Reader Monad
The Reader monad represents computations that can read values from a shared environment. See also: Higher-Order Functions for the use of functions as first-class values.
Definition
newtype Reader r a = Reader { runReader :: r -> a }instance Monad (Reader r) where return x = Reader (\_ -> x) (Reader f) >>= g = Reader $ \r -> let a = f r Reader h = g a in h r
Use Cases
Dependency injection
Configuration management
Functions sharing an immutable context
Example
import Control.Monad.Reader-- Configuration datadata Config = Config { baseUrl :: String, timeout :: Int, maxRetries :: Int}-- Functions using configurationgetResource :: String -> Reader Config StringgetResource path = do config <- ask -- Get the environment return $ baseUrl config ++ "/" ++ pathfetchWithRetry :: String -> Reader Config StringfetchWithRetry resource = do path <- getResource resource config <- ask return $ "Fetching " ++ path ++ " with timeout " ++ show (timeout config) ++ " and " ++ show (maxRetries config) ++ " retries"-- Main program using these functionsprogram :: Reader Config Stringprogram = do result1 <- getResource "users" result2 <- fetchWithRetry "data" return (result1 ++ "\n" ++ result2)-- Run the program with a specific configrunProgram :: StringrunProgram = runReader program (Config "https://api.example.com" 1000 3)
Writer Monad
The Writer monad represents computations that can produce a secondary stream of data (e.g., a log). See also: Pattern Matching and Recursion for recursive logging examples.
Definition
newtype Writer w a = Writer { runWriter :: (a, w) }instance (Monoid w) => Monad (Writer w) where return a = Writer (a, mempty) (Writer (a, w)) >>= f = let (b, w') = runWriter (f a) in Writer (b, w `mappend` w')
Use Cases
Logging
Collecting statistics
Building up strings or other monoid values
Example
import Control.Monad.Writer-- Log messages during computationfactorial :: Int -> Writer [String] Intfactorial n = do tell ["Computing factorial of " ++ show n] if n <= 1 then do tell ["Factorial of " ++ show n ++ " is 1"] return 1 else do res <- factorial (n-1) let result = n * res tell ["Factorial of " ++ show n ++ " is " ++ show result] return result-- Run the computation and get result with logscomputeFactorial :: Int -> (Int, [String])computeFactorial n = runWriter (factorial n)
State Monad
The State monad represents computations that can maintain and modify state. See also: Pattern Matching and Recursion for recursive stateful algorithms.
Definition
newtype State s a = State { runState :: s -> (a, s) }instance Monad (State s) where return a = State $ \s -> (a, s) (State h) >>= f = State $ \s -> let (a, s') = h s State g = f a in g s'
Use Cases
Stateful algorithms
Passing mutable state through a computation
Random number generation
Example
import Control.Monad.State-- Simple random number generator using statetype RandomState = State Int-- Linear congruential generator parametersa, c, m :: Inta = 1103515245c = 12345m = 2^31-- Generate a random number and update the seednextRandom :: RandomState IntnextRandom = do seed <- get let newSeed = (a * seed + c) `mod` m put newSeed return newSeed-- Generate n random numbersrandomList :: Int -> RandomState [Int]randomList n = replicateM n nextRandom-- Run with an initial seedgenerateRandoms :: Int -> Int -> [Int]generateRandoms seed count = evalState (randomList count) seed
Either Monad
The Either monad represents computations that might fail with an error value.
Definition
data Either e a = Left e | Right ainstance Monad (Either e) where return = Right Left e >>= _ = Left e Right a >>= f = f a
Use Cases
Error handling with specific error information
Validation with detailed error messages
Railway-oriented programming
Example
import Control.Monad.Trans.Either-- Error typesdata ValidationError = EmptyField String | InvalidFormat String | OutOfRange String Int Int Int deriving Show-- Validation functionsvalidateName :: String -> Either ValidationError StringvalidateName "" = Left (EmptyField "Name")validateName name = Right namevalidateAge :: Int -> Either ValidationError IntvalidateAge age | age < 0 = Left (OutOfRange "Age" age 0 120) | age > 120 = Left (OutOfRange "Age" age 0 120) | otherwise = Right age-- Combined validationvalidatePerson :: String -> Int -> Either ValidationError (String, Int)validatePerson name age = do validName <- validateName name validAge <- validateAge age return (validName, validAge)
IO Monad
The IO monad represents computations that perform input/output operations.
Definition
The implementation is built into the Haskell runtime system.
Use Cases
Reading/writing files
Network operations
Console input/output
Any interaction with the external world
Example
-- Interactive programinteractiveCalculator :: IO ()interactiveCalculator = do putStrLn "Enter the first number:" input1 <- getLine putStrLn "Enter the second number:" input2 <- getLine putStrLn "Enter operation (+, -, *, /):" op <- getLine let num1 = read input1 :: Double num2 = read input2 :: Double result = case op of "+" -> num1 + num2 "-" -> num1 - num2 "*" -> num1 * num2 "/" -> num1 / num2 _ -> error "Invalid operation" putStrLn $ "Result: " ++ show result putStrLn "Continue? (y/n)" cont <- getLine if cont == "y" then interactiveCalculator else putStrLn "Goodbye!"
Identity Monad
The Identity monad is the simplest monad, with no additional effects.
Definition
newtype Identity a = Identity { runIdentity :: a }instance Monad Identity where return = Identity Identity x >>= f = f x
Use Cases
Understanding monadic concepts
Base case for monad transformers
Pure computations requiring a monadic interface
Example
import Control.Monad.Identity-- Pure computation in a monadic stylefactorial :: Int -> Identity Intfactorial n = do if n <= 1 then return 1 else do res <- factorial (n-1) return (n * res)-- Run the computationcomputeFactorial :: Int -> IntcomputeFactorial n = runIdentity (factorial n)
Combining Monads
Different monads solve different problems, but we often need to combine their capabilities. This is where monad transformers come in (covered in a separate page).
Key Points to Remember
Maybe represents computations that might fail
List represents non-deterministic computations with multiple results
Reader provides read-only access to shared environment
Writer allows accumulating a secondary value (like a log)
State enables maintaining and modifying state throughout a computation
Either is like Maybe but with detailed error information
IO handles side effects and interaction with the external world
Identity is the simplest monad with no additional effects
For a type to be a proper monad, it must satisfy three laws that ensure it behaves consistently and predictably. These laws are mathematical properties that should hold for any monad implementation. See also: Monads Basics, Common Monads, Monad Transformers, Equational Reasoning.
The Three Monad Laws
1. Left Identity
return a >>= f = f a
This law states that if we take a value, wrap it in a monad using return, and then feed it to a function using >>=, the result should be the same as just applying the function directly to the value. For more on return and >>=, see Monads Basics.
Intuition
return is a “neutral” way to inject a value into a monad. It shouldn’t add any computational effect or change the value. So using return and then immediately extracting the value with >>= should be equivalent to not using the monad at all.
Example with Maybe
return 5 >>= (\x -> Just (x + 3))-- is the same as(\x -> Just (x + 3)) 5-- Both evaluate to Just 8
2. Right Identity
m >>= return = m
This law states that if we have a monadic value and we feed its result to return using >>=, we should get back the original monadic value.
Intuition
Since return simply injects a value into the monad without adding any effects, taking a monadic value, extracting its core value, and then reinjecting it with return should give us back what we started with.
Example with List
[1, 2, 3] >>= return-- is the same as[1, 2, 3]
3. Associativity
(m >>= f) >>= g = m >>= (\x -> f x >>= g)
This law states that the order of application doesn’t matter when chaining monadic operations. Whether we first apply f to m and then apply g to the result, or we define a new function that applies f and then g, and apply that to m, the result should be the same. See Monads Basics for how this relates to do notation.
Intuition
This ensures that when we chain monadic operations, we can group them however we want without affecting the result. This is essential for building complex computations out of simpler ones in a modular way.
Example with IO
-- These two expressions are equivalent:(getLine >>= readFile) >>= putStrLngetLine >>= (\filename -> readFile filename >>= putStrLn)
Verifying the Laws for Specific Monads
Let’s verify the monad laws for a few common monads to better understand them. See Common Monads for more on these types.
Maybe Monad
The Maybe monad is defined as:
instance Monad Maybe where return = Just Nothing >>= _ = Nothing (Just x) >>= f = f x
Left Identity
return a >>= f= Just a >>= f= f a
Right Identity
m >>= return
For m = Nothing:
Nothing >>= return= Nothing
For m = Just a:
Just a >>= return= return a= Just a
Associativity
(m >>= f) >>= g
For m = Nothing:
(Nothing >>= f) >>= g= Nothing >>= g= NothingNothing >>= (\x -> f x >>= g)= Nothing
For m = Just a:
(Just a >>= f) >>= g= f a >>= gJust a >>= (\x -> f x >>= g)= (\x -> f x >>= g) a= f a >>= g
List Monad
The List monad is defined as:
instance Monad [] where return x = [x] xs >>= f = concat (map f xs)
Left Identity
return a >>= f= [a] >>= f= concat (map f [a])= concat [f a]= f a
The proof is more involved, but it relies on the associativity of concat and the distributivity of map over concat.
Why the Monad Laws Matter
The monad laws ensure that:
Consistency: Monads behave in a consistent, predictable way
Equational Reasoning: We can reason about monadic code using substitution (Equational Reasoning)
Refactoring: Code can be refactored without changing behavior
Composability: Monadic operations can be composed in any order (Monads Basics)
Do-Notation: The translation of do notation relies on these laws (Monads Basics)
Consequences of Breaking the Laws
If a monad implementation breaks these laws:
Code using that monad might behave unexpectedly
Refactoring could change the behavior of the code
The do notation might not work as expected
Composition with other monadic operations might be inconsistent
Practical Implications
For do Notation
The translation of do notation to >>= relies on the monad laws. For example:
do a <- ma b <- mb return (a, b)
This translates to:
ma >>= (\a -> mb >>= (\b -> return (a, b)))
The associativity law ensures that this is equivalent to:
(ma >>= (\a -> mb)) >>= (\b -> return (a, b))
For Monad Transformers
Monad transformers combine the effects of multiple monads. The monad laws ensure that these combinations behave reasonably.
For Library Design
When designing a library that uses monads, adhering to the monad laws ensures that users can reason about your library’s behavior and combine it with other monadic code.
Testing the Monad Laws
You can use property-based testing with QuickCheck to verify that your monad implementation satisfies the laws:
import Test.QuickCheck-- For a custom monad MyMonadprop_leftIdentity :: (Eq (m b), Monad m) => a -> (a -> m b) -> Boolprop_leftIdentity a f = (return a >>= f) == f aprop_rightIdentity :: (Eq (m a), Monad m) => m a -> Boolprop_rightIdentity m = (m >>= return) == mprop_associativity :: (Eq (m c), Monad m) => m a -> (a -> m b) -> (b -> m c) -> Boolprop_associativity m f g = ((m >>= f) >>= g) == (m >>= (\x -> f x >>= g))
Key Points to Remember
The three monad laws are left identity, right identity, and associativity
These laws ensure consistent and predictable behavior when using monads
All proper monad implementations must satisfy these laws
The laws are not enforced by the type system - it’s the developer’s responsibility
The laws are essential for reasoning about monadic code and for the correct functioning of do notation
Parser combinators are a functional approach to parsing where complex parsers are built by combining simpler ones. They’re a powerful application of monads in Haskell (Monads Basics, Common Monads, Monad Laws).
What are Parser Combinators?
A parser is a function that takes some input (typically a string) and produces a structured result. Parser combinators are higher-order functions (Higher-Order Functions) that take parsers as input and return new parsers.
The key insight is that we can:
Create primitive parsers for basic elements
Combine them to create more complex parsers
Use the monadic structure to sequence parsing operations (Monads Basics)
The Parsec Library
Parsec is the most common parser combinator library in Haskell. Let’s look at its core concepts.
Installation
cabal install --lib parsec
Basic Types
In Parsec, the primary type is:
type Parser a = Parsec String () a
which means a parser that consumes a String input, uses () as a custom state, and produces a value of type a.
Simple Parsers
Parsec provides many primitive parsers:
import Text.Parsecimport Text.Parsec.String (Parser)-- Parse a single charactercharParser :: Parser CharcharParser = char 'c'-- Parse any digitdigitParser :: Parser ChardigitParser = digit-- Parse a specific stringhelloParser :: Parser StringhelloParser = string "hello"
Running Parsers
To run a parser on an input string:
parse :: Parsec s u a -> SourceName -> s -> Either ParseError a
Examples:
parse charParser "input" "cat" -- Right 'c'parse charParser "input" "dog" -- Left "unexpected 'd'"parseTest :: Show a => Parsec s u a -> s -> IO ()parseTest charParser "cat" -- 'c'
Combining Parsers
The power of parser combinators comes from combining simpler parsers into more complex ones. This is a direct application of higher-order functions (Higher-Order Functions) and monadic sequencing (Monads Basics).
Sequencing Parsers
To parse one thing followed by another, we use monadic do notation (Monads Basics):
-- Parse "hello" followed by "world"helloWorldParser :: Parser (String, String)helloWorldParser = do hello <- string "hello" space world <- string "world" return (hello, world)
Alternatives
To try one parser, and if it fails, try another, we use the <|> operator from the Alternative typeclass (Functors and Applicatives):
-- Parse either "cat" or "dog"animalParser :: Parser StringanimalParser = try (string "cat") <|> string "dog"
The <|> operator comes from the Alternative typeclass and represents choice.
Repetition
To parse something multiple times:
-- Parse zero or more digitsdigitsParser :: Parser StringdigitsParser = many digit-- Parse one or more digitsdigitsParser1 :: Parser StringdigitsParser1 = many1 digit
Often, we want to build a structured representation of the input rather than evaluating it directly. This is a classic use of algebraic data types (Algebraic Data Types) and recursion (Pattern Matching and Recursion).
-- Data type for arithmetic expressionsdata Expr = Lit Integer | Add Expr Expr | Sub Expr Expr | Mul Expr Expr | Div Expr Expr deriving (Show)-- Parser for a parse treeexprTree :: Parser ExprexprTree = E.buildExpressionParser table term where term = parens exprTree <|> (Lit <$> integer) table = [ [binary "*" Mul E.AssocLeft, binary "/" Div E.AssocLeft] , [binary "+" Add E.AssocLeft, binary "-" Sub E.AssocLeft] ] binary name constructor assoc = E.Infix (reservedOp name >> return constructor) assoc-- After parsing, we can evaluate or transform the parse treeeval :: Expr -> Integereval (Lit n) = neval (Add e1 e2) = eval e1 + eval e2eval (Sub e1 e2) = eval e1 - eval e2eval (Mul e1 e2) = eval e1 * eval e2eval (Div e1 e2) = eval e1 `div` eval e2
Common Parser Combinators
Basic Parsers
char :: Char -> Parser Char -- Parse a specific characterstring :: String -> Parser String -- Parse a specific stringanyChar :: Parser Char -- Parse any characterletter :: Parser Char -- Parse a letterdigit :: Parser Char -- Parse a digitalphaNum :: Parser Char -- Parse a letter or digitspace :: Parser Char -- Parse a space characterspaces :: Parser () -- Parse zero or more spaces
Combinators
(<|>) :: Parser a -> Parser a -> Parser a -- Choice between parserstry :: Parser a -> Parser a -- Backtrackingmany :: Parser a -> Parser [a] -- Zero or more occurrencesmany1 :: Parser a -> Parser [a] -- One or more occurrencesoption :: a -> Parser a -> Parser a -- Optional parser with defaultoptional :: Parser a -> Parser () -- Optional parser, discarding resultbetween :: Parser open -> Parser close -> Parser a -> Parser a -- BracketssepBy :: Parser a -> Parser sep -> Parser [a] -- Separated listsepBy1 :: Parser a -> Parser sep -> Parser [a] -- Non-empty separated listendBy :: Parser a -> Parser sep -> Parser [a] -- List ending with separatorchainl :: Parser a -> Parser (a -> a -> a) -> a -> Parser a -- Left-associative chain
Handling Parse Errors
Parsec provides detailed error messages when parsing fails:
parse expr "" "1 + (2 * 3"-- Left (line 1, column 10):-- unexpected end of input-- expecting ")" or digit
You can customize error messages using the <??> operator:
expr' = expr <?> "expression"
Common Patterns
Lexing and Parsing
For complex languages, it’s common to separate lexical analysis (tokenization) from parsing:
-- Lexer (converts string to tokens)lexer :: Parser [Token]lexer = many (space *> token <* spaces)-- Parser (converts tokens to AST)parser :: [Token] -> Either ParseError AST
Recursive Parsers
For recursive structures like expressions, we need to handle precedence and associativity:
When dealing with ambiguous grammars, it’s important to use try to backtrack when a parser fails after consuming input:
-- Without try, this would fail if "let" is found but not followed by a valid definitionletExpr = try (do reserved "let" name <- identifier reservedOp "=" value <- expr reserved "in" body <- expr return (Let name value body)) <|> otherExpr
Error handling in Haskell differs significantly from exception-based approaches in imperative languages. Haskell provides several approaches that align with its functional nature and type system.
Types of Errors in Haskell
Compile-time errors: Type errors, syntax errors, etc.
Runtime errors: Exceptions that occur during program execution
Expected failures: Situations where operations might legitimately fail
This page focuses on handling the last two categories.
Approaches to Error Handling
1. Maybe Type
The Maybe type represents computations that might fail:
data Maybe a = Nothing | Just a
Use Cases
Partial functions (not defined for all inputs)
Functions that could fail without needing detailed error information
Example
safeDivide :: Int -> Int -> Maybe IntsafeDivide _ 0 = NothingsafeDivide x y = Just (x `div` y)-- Usagecase safeDivide 10 0 of Nothing -> putStrLn "Division by zero!" Just result -> putStrLn $ "Result: " ++ show result
Working with Multiple Maybe Values
-- Using do notationcomputeAverage :: [Int] -> Maybe DoublecomputeAverage xs = do if null xs then Nothing else Just (fromIntegral (sum xs) / fromIntegral (length xs))-- Using monadic functionsfindUserAvgScore :: UserId -> [ScoreId] -> Maybe DoublefindUserAvgScore uid scoreIds = do user <- findUser uid scores <- mapM (getScore user) scoreIds return (average scores)
2. Either Type
The Either type provides more detailed error information:
data Either a b = Left a | Right b
By convention, Left contains error information and Right contains successful results.
Use Cases
When you need specific error information
Validation that needs to report why it failed
Functions with multiple failure modes
Example
data DivideError = DivideByZero | OverflowErrorsafeDivide :: Int -> Int -> Either DivideError IntsafeDivide _ 0 = Left DivideByZerosafeDivide x y | x > maxBound `div` y = Left OverflowError | otherwise = Right (x `div` y)-- Usagecase safeDivide 10 0 of Left DivideByZero -> putStrLn "Cannot divide by zero!" Left OverflowError -> putStrLn "Overflow would occur!" Right result -> putStrLn $ "Result: " ++ show result
Working with Multiple Either Values
-- Using do notationvalidatePerson :: String -> Int -> Either String PersonvalidatePerson name age = do validName <- validateName name validAge <- validateAge age Right (Person validName validAge) where validateName "" = Left "Name cannot be empty" validateName n = Right n validateAge a | a < 0 = Left "Age cannot be negative" | a > 120 = Left "Age is unrealistic" | otherwise = Right a
3. ExceptT Monad Transformer
ExceptT combines the Either type with another monad (often IO):
newtype ExceptT e m a = ExceptT { runExceptT :: m (Either e a) }
Use Cases
Handling errors in code that also performs IO or other monadic operations
Creating a clean separation of error handling from other effects
Building complex functions that can fail at multiple points
Example
import Control.Monad.Excepttype AppError = Stringtype App a = ExceptT AppError IO areadConfig :: FilePath -> App ConfigreadConfig path = do exists <- liftIO $ doesFileExist path unless exists $ throwError $ "Config file not found: " ++ path content <- liftIO $ readFile path case parseConfig content of Nothing -> throwError "Invalid config format" Just config -> return configrunApp :: App a -> IO (Either AppError a)runApp = runExceptT
4. Runtime Exceptions
Haskell has a system for runtime exceptions, but it’s generally avoided in favor of explicit error types:
-- Generating exceptionserror :: String -> aundefined :: a-- Catching exceptionscatch :: Exception e => IO a -> (e -> IO a) -> IO atry :: Exception e => IO a -> IO (Either e a)
Sometimes you want to report all errors rather than just the first one:
import Data.Validationdata ValidationError = NameTooShort | AgeTooLow | InvalidEmail deriving (Show)validatePerson :: String -> Int -> String -> Validation [ValidationError] PersonvalidatePerson name age email = Person <$> validateName name <*> validateAge age <*> validateEmail email where validateName n | length n < 2 = Failure [NameTooShort] | otherwise = Success n validateAge a | a < 18 = Failure [AgeTooLow] | otherwise = Success a validateEmail e | not (isValidEmail e) = Failure [InvalidEmail] | otherwise = Success e
Error Handling Patterns
Defensive Programming
processData :: Maybe Data -> Either Error ResultprocessData Nothing = Left (MissingData "No data provided")processData (Just d) | isValid d = Right (computeResult d) | otherwise = Left (InvalidData "Data is invalid")
Railway-Oriented Programming
Thinking of functions as “tracks” where success follows one track and failure another:
validateInput :: Input -> Either Error ValidInputtransformData :: ValidInput -> Either Error TransformedDatacomputeOutput :: TransformedData -> Either Error OutputprocessInput :: Input -> Either Error OutputprocessInput input = do validInput <- validateInput input transformedData <- transformData validInput computeOutput transformedData
Error Handling with Monads
Using monadic functions for cleaner error handling:
-- mapM for processing lists with potential failuresprocessItems :: [Item] -> Either Error [Result]processItems = mapM processItem-- filterM for filtering with side effectsgetValidItems :: [Item] -> IO [Item]getValidItems = filterM isValid-- Using forM_ for actions that might failprocessAllItems :: [Item] -> Either Error ()processAllItems items = forM_ items $ \item -> do result <- processItem item storeResult result
Best Practices
Use the simplest mechanism that meets your needs:
For simple absence/presence of a value, use Maybe
For error details, use Either
For more complex requirements, consider monad transformers
Make error types informative:
data UserError = UserNotFound UserId | InvalidPassword | AccountLocked DateTime
Return errors, don’t throw exceptions in pure code:
-- Goodlookup :: Key -> Map Key Value -> Maybe Value-- Avoidlookup :: Key -> Map Key Value -> Valuelookup k m = case Map.lookup k m of Just v -> v Nothing -> error "Key not found"
Handle all cases explicitly:
processResult :: Either Error Value -> StringprocessResult (Right value) = "Success: " ++ show valueprocessResult (Left (InputError msg)) = "Input error: " ++ msgprocessResult (Left (SystemError code)) = "System error: " ++ show codeprocessResult (Left (OtherError e)) = "Other error: " ++ show e
Compose error-handling functions using monads:
validateAndProcess :: Input -> Either Error OutputvalidateAndProcess = validate >=> process >=> format
Key Points to Remember
Haskell favors explicit error handling using types like Maybe and Either
Exceptions in Haskell are primarily for exceptional conditions, not regular error handling
Monadic composition makes it easy to chain operations that might fail
Custom error types help make error handling more informative and type-safe
For IO operations, consider using ExceptT to combine IO with structured error handling
Monad transformers are a way to combine multiple monads to create a composite monad that has the features of all the component monads.
The Problem: Combining Monads
Different monads provide different computational effects:
Maybe - Computations that might fail
Either e - Computations that might fail with an error of type e
Reader r - Computations with read-only access to an environment of type r
State s - Computations with mutable state of type s
IO - Computations with side effects
But what if we need multiple effects? For example:
A computation that needs access to configuration AND might fail
A computation that maintains state AND performs IO operations
We can’t easily compose monads directly. If we have functions:
f :: a -> Maybe bg :: b -> State s c
There’s no general way to compose them to get a function a -> SomeCombinedMonad c.
Monad Transformers: The Solution
Monad transformers are special types that add capabilities of one monad to another monad.
Key transformer types include:
MaybeT m a - Adds Maybe’s failure handling to monad m
ExceptT e m a - Adds Either e’s error handling to monad m
ReaderT r m a - Adds Reader r’s environment access to monad m
StateT s m a - Adds State s’s state manipulation to monad m
WriterT w m a - Adds Writer w’s logging to monad m
The MonadTrans Typeclass
The MonadTrans typeclass defines how to lift operations from the base monad into the transformer:
class MonadTrans t where lift :: (Monad m) => m a -> t m a
This allows operations from the inner monad to be used in the context of the transformer.
Common Monad Transformers
MaybeT
Adds optional values to any monad:
newtype MaybeT m a = MaybeT { runMaybeT :: m (Maybe a) }instance (Monad m) => Monad (MaybeT m) where return = MaybeT . return . Just x >>= f = MaybeT $ do v <- runMaybeT x case v of Nothing -> return Nothing Just y -> runMaybeT (f y)instance MonadTrans MaybeT where lift = MaybeT . fmap Just
Example
import Control.Monad.Trans.Maybeimport Control.Monad.Trans.ClassfindUser :: UserId -> MaybeT IO UserfindUser uid = do mUser <- lift $ queryDatabase uid case mUser of Nothing -> MaybeT $ return Nothing Just user -> return usergetUserSettings :: User -> MaybeT IO SettingsgetUserSettings user = do mSettings <- lift $ fetchSettings (userId user) MaybeT $ return mSettingsuserProgram :: UserId -> IO (Maybe Settings)userProgram uid = runMaybeT $ do user <- findUser uid getUserSettings user
ExceptT
Adds error handling to any monad:
newtype ExceptT e m a = ExceptT { runExceptT :: m (Either e a) }
newtype ReaderT r m a = ReaderT { runReaderT :: r -> m a }
Example
import Control.Monad.Readerimport Control.Monad.Trans.Classdata Config = Config { apiUrl :: String, timeout :: Int, maxRetries :: Int}type AppM a = ReaderT Config IO afetchData :: Endpoint -> AppM DatafetchData endpoint = do config <- ask let url = apiUrl config ++ endpoint to = timeout config lift $ httpGet url toretryOperation :: AppM a -> AppM aretryOperation operation = do config <- ask lift $ withRetry (maxRetries config) (runReaderT operation config)appMain :: Config -> IO ResultappMain config = runReaderT program config where program = do userData <- fetchData "/users" productData <- fetchData "/products" return (processResults userData productData)
StateT
Adds mutable state to any monad:
newtype StateT s m a = StateT { runStateT :: s -> m (a, s) }
Example
import Control.Monad.Stateimport Control.Monad.Trans.Classtype GameState = (Player, World)type GameM a = StateT GameState IO amovePlayer :: Direction -> GameM ()movePlayer dir = do (player, world) <- get let newPlayer = updatePosition player dir if isValidPosition world newPlayer then put (newPlayer, world) else return ()interactWithObject :: GameM ()interactWithObject = do (player, world) <- get case objectAt world (playerPosition player) of Nothing -> lift $ putStrLn "Nothing here." Just obj -> do lift $ putStrLn $ "Interacting with " ++ show obj let (newWorld, message) = interact world obj put (player, newWorld) lift $ putStrLn messagegameLoop :: GameM ()gameLoop = do (player, _) <- get if playerHealth player <= 0 then lift $ putStrLn "Game over!" else do cmd <- lift getCommand executeCommand cmd gameLooprunGame :: GameState -> IO ()runGame initialState = evalStateT gameLoop initialState
Transformer Stacks
Monad transformers can be stacked to combine multiple effects:
type AppM a = ExceptT AppError (ReaderT Config (StateT AppState IO)) a
This gives us a monad with:
Error handling (ExceptT)
Configuration access (ReaderT)
Mutable application state (StateT)
IO capabilities (IO)
Running a Transformer Stack
To run a monad transformer stack, you apply the “run” functions from outside in:
runApp :: Config -> AppState -> AppM a -> IO (Either AppError a, AppState)runApp config state action = runStateT (runReaderT (runExceptT action) config) state
Example: Complex Stack
{-# LANGUAGE FlexibleContexts #-}import Control.Monad.Readerimport Control.Monad.Stateimport Control.Monad.Exceptimport Control.Monad.Writerdata AppConfig = AppConfig { ... }data AppState = AppState { ... }data AppError = AppError { ... }type Log = [String]type AppM a = ExceptT AppError (ReaderT AppConfig (StateT AppState (WriterT Log IO))) a-- Access configgetConfig :: MonadReader AppConfig m => m AppConfiggetConfig = ask-- Access/modify stategetState :: MonadState AppState m => m AppStategetState = getupdateState :: MonadState AppState m => (AppState -> AppState) -> m ()updateState = modify-- Error handlingthrowAppError :: MonadError AppError m => AppError -> m athrowAppError = throwError-- LogginglogInfo :: MonadWriter Log m => String -> m ()logInfo msg = tell [msg]-- IO operationsliftIOOperation :: MonadIO m => IO a -> m aliftIOOperation = liftIO-- Running the applicationrunApp :: AppConfig -> AppState -> AppM a -> IO (((Either AppError a, AppState), Log))runApp config state app = runWriterT (runStateT (runReaderT (runExceptT app) config) state)
Lifting Through Transformer Stacks
When working with transformer stacks, you often need to lift operations:
-- Lift from innermost monad to transformer stackliftIO :: MonadIO m => IO a -> m a-- Lifting through a stack requires multiple liftsliftMaybeIO :: MaybeT (StateT s IO) a -> ExceptT e (ReaderT r (StateT s IO)) aliftMaybeIO = lift . lift . lift . except . maybe (Left defaultError) Right . runMaybeT
Type Classes for Lifting
The mtl library provides typeclasses for different monad capabilities:
class Monad m => MonadReader r m | m -> r where ask :: m r local :: (r -> r) -> m a -> m aclass Monad m => MonadState s m | m -> s where get :: m s put :: s -> m ()class Monad m => MonadError e m | m -> e where throwError :: e -> m a catchError :: m a -> (e -> m a) -> m aclass Monad m => MonadWriter w m | m -> w where tell :: w -> m () listen :: m a -> m (a, w) pass :: m (a, w -> w) -> m a
These typeclasses make it easier to write code that works with any transformer stack that has the required capabilities.
Common Patterns with Monad Transformers
The ReaderT IO Pattern
The ReaderT env IO monad is a common pattern for applications:
newtype App a = App { unApp :: ReaderT Env IO a } deriving (Functor, Applicative, Monad, MonadReader Env, MonadIO)-- All functions can access the environmentgetConfig :: App ConfiggetConfig = asks envConfig-- Lift IO operationslogMessage :: String -> App ()logMessage msg = do logger <- asks envLogger liftIO $ logger msg-- Run the applicationrunApp :: Env -> App a -> IO arunApp env app = runReaderT (unApp app) env
Error Handling with ExceptT
Adding error handling with ExceptT:
type App a = ExceptT AppError (ReaderT Env IO) a-- Now we can throw and catch errorssaveDocument :: Document -> App ()saveDocument doc = do db <- asks envDatabase result <- liftIO $ tryIOError $ writeToDatabase db doc case result of Left e -> throwError (DatabaseError e) Right _ -> return ()-- Run with error handlingrunApp :: Env -> App a -> IO (Either AppError a)runApp env app = runReaderT (runExceptT app) env
State with StateT
Adding state management:
type App a = StateT AppState (ReaderT Env (ExceptT AppError IO)) a-- Update state based on external operationsprocessTransaction :: Transaction -> App BalanceprocessTransaction tx = do AppState {balance} <- get let newBalance = updateBalance balance tx if newBalance < 0 then throwError InsufficientFunds else do modify $ \s -> s {balance = newBalance} return newBalance-- Run with staterunApp :: Env -> AppState -> App a -> IO (Either AppError (a, AppState))runApp env state app = runExceptT $ runReaderT (runStateT app state) env
Monad Transformer Best Practices
1. Keep the Stack Simple
Don’t add transformers you don’t need. A common stack is:
type App a = ExceptT AppError (ReaderT Env IO) a
This gives you:
Error handling (ExceptT)
Environment access (ReaderT)
IO capabilities (IO)
2. Use Type Classes
Instead of directly referencing your transformer stack, use typeclasses:
-- Instead of this:fetchUser :: UserId -> ExceptT Error (ReaderT Env IO) User-- Prefer this:fetchUser :: (MonadReader Env m, MonadError Error m, MonadIO m) => UserId -> m User
This makes your code more reusable and easier to test.
3. Define Helper Functions
Create helpers for common operations:
throwDbError :: MonadError AppError m => DbError -> m athrowDbError = throwError . DatabaseErrorwithTransaction :: (MonadReader Env m, MonadError AppError m, MonadIO m) => (Connection -> IO a) -> m awithTransaction action = do conn <- asks envDbConnection result <- liftIO $ try $ withDbTransaction conn action either throwDbError return result
This gives you better type safety and lets you define custom instances.
5. Consider Performance
Monad transformers can introduce overhead. For performance-critical applications, consider alternatives like the RIO or ReaderT IO patterns.
Key Points to Remember
Monad transformers let you combine multiple monadic effects
Each transformer adds a specific capability (error handling, state, etc.)
Use lift to promote operations from inner monads to the transformer stack
The mtl library provides typeclasses for common monadic capabilities
The order of transformers matters - effects are applied from inner to outer
Common patterns include ReaderT Env IO and ExceptT Error (ReaderT Env IO)
Use typeclasses and helper functions to make your code more modular
Error handling in Haskell differs significantly from exception-based approaches in imperative languages. Haskell provides several approaches that align with its functional nature and type system. For background on types and monads, see Algebraic Data Types, Common Monads, and Monads Basics.
Types of Errors in Haskell
Compile-time errors: Type errors, syntax errors, etc.
Runtime errors: Exceptions that occur during program execution
Expected failures: Situations where operations might legitimately fail
This page focuses on handling the last two categories.
Approaches to Error Handling
1. Maybe Type
The Maybe type represents computations that might fail. This is a common pattern in Haskell and is discussed in more detail in Common Monads and Algebraic Data Types.
Working with Multiple Maybe Values
Using do notation with Maybe leverages the monadic structure described in Monads Basics.
2. Either Type
The Either type provides more detailed error information. Like Maybe, it is an algebraic data type (see Algebraic Data Types) and a common monad (Common Monads).
Working with Multiple Either Values
Chaining Either computations with do notation is another example of monadic error handling (see Monads Basics).
3. ExceptT Monad Transformer
ExceptT combines the Either type with another monad (often IO). For more on combining effects, see Monad Transformers. For IO, see Introduction to IO.
4. Runtime Exceptions
Haskell has a system for runtime exceptions, but idiomatic Haskell code prefers explicit error types like Maybe and Either (see above and Common Monads).
5. Custom Exceptions
You can define custom exception types using algebraic data types (see Algebraic Data Types).
Functors and Applicative Functors are key typeclasses in Haskell that provide foundational abstractions for working with values in contexts. For related abstractions, see Monads Basics, Common Monads, and Polymorphism.
Functors
A functor represents a type that can be mapped over. Intuitively, it’s a container or context that allows us to apply functions to the values inside without affecting the structure. For examples of functor instances, see Lists and List Comprehensions, Algebraic Data Types, and Introduction to IO.
The Functor Typeclass
class Functor f where fmap :: (a -> b) -> f a -> f b
fmap applies a function to the value(s) inside the functor, preserving the structure.
There’s also an infix operator for fmap:
(<$>) :: Functor f => (a -> b) -> f a -> f b(<$>) = fmap
Functor Laws
For a type to be a valid functor, it must satisfy two laws. These laws are analogous to the monad laws (see Monad Laws) and ensure predictable behavior.
Identity Law: fmap id = id
Mapping the identity function should not change anything
Composition Law: fmap (f . g) = fmap f . fmap g
Mapping a composition of functions should be the same as mapping one function after the other
instance Functor (Either a) where fmap _ (Left e) = Left e fmap f (Right x) = Right (f x)
Example:
fmap (*2) (Right 3) -- Right 6fmap (*2) (Left "error") -- Left "error"
IO Functor
instance Functor IO where fmap f action = do result <- action return (f result)
Example:
fmap length getLine -- IO Int - reads a line and returns its length
Creating Custom Functors
Any type with a single type parameter can potentially be a functor. For more on custom data types, see Algebraic Data Types.
data Tree a = Leaf | Node a (Tree a) (Tree a)instance Functor Tree where fmap _ Leaf = Leaf fmap f (Node x left right) = Node (f x) (fmap f left) (fmap f right)
Practical Uses of Functors
Transforming wrapped values:
fmap show (Just 42) -- Just "42"
Working with computations:
userInput <- fmap read getLine -- Read user input as a number
Applicative functors extend the concept of functors by allowing functions wrapped in a context to be applied to values in the same context. Applicative is a superclass of Monad (see Monads Basics).
The Applicative Typeclass
class Functor f => Applicative f where pure :: a -> f a (<*>) :: f (a -> b) -> f a -> f b
pure wraps a value in the minimal context
<*> (pronounced “apply”) applies a function in a context to a value in a context
Applicative Laws
Valid applicative instances must satisfy several laws, which are similar in spirit to the monad laws (Monad Laws).
Identity: pure id <*> v = v
Homomorphism: pure f <*> pure x = pure (f x)
Interchange: u <*> pure y = pure ($ y) <*> u
Composition: pure (.) <*> u <*> v <*> w = u <*> (v <*> w)
Common Applicatives
Maybe Applicative
instance Applicative Maybe where pure = Just Nothing <*> _ = Nothing (Just f) <*> something = fmap f something
Example:
(+1) <$> Just 3pure +) <*> Just 3 -- Just 4Just (+1) <*> Just 3 -- Just 4Just (+) <*> Just 3 <*> Just 5 -- Just 8Nothing <*> Just 3 -- Nothing
List Applicative
instance Applicative [] where pure x = [x] fs <*> xs = [f x | f <- fs, x <- xs]
Example:
[(*2), (+3)] <*> [1, 2, 3] -- [2, 4, 6, 4, 5, 6]
The list applicative produces all possible combinations of applying each function to each value.
IO Applicative
instance Applicative IO where pure = return aF <*> aX = do f <- aF x <- aX return (f x)
Example:
pure (+) <*> getLine <*> getLine -- Reads two lines and adds them as numbers
Helper Functions for Applicatives
-- Apply a function to two applicative valuesliftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f cliftA2 f a b = f <$> a <*> b-- Sequential application(*>) :: Applicative f => f a -> f b -> f ba *> b = (id <$ a) <*> b-- Sequence actions, discard value from the right(<*) :: Applicative f => f a -> f b -> f aa <* b = liftA2 const a b
data Person = Person { name :: String, age :: Int }validatePerson :: String -> Int -> Maybe PersonvalidatePerson name age = Person <$> validateName name <*> validateAge age
data Validation e a = Failure e | Success ainstance Semigroup e => Applicative (Validation e) where pure = Success Failure e1 <*> Failure e2 = Failure (e1 <> e2) Failure e <*> _ = Failure e _ <*> Failure e = Failure e Success f <*> Success a = Success (f a)
Parsing with Applicatives:
-- Using a parser combinator libraryparseFullName :: Parser FullNameparseFullName = FullName <$> parseFirstName <*> parseLastName
The Relationship Between Functors, Applicatives, and Monads
These typeclasses form a hierarchy with increasing power:
Functor: Ability to map functions over a context
fmap :: (a -> b) -> f a -> f b
Applicative: Ability to apply wrapped functions to wrapped values
(<*>) :: f (a -> b) -> f a -> f b
Monad: Ability to sequence computations that depend on previous results
(>>=) :: f a -> (a -> f b) -> f b
The relationship can be summarized as:
Every Monad is an Applicative
Every Applicative is a Functor
Functors are the most general, Monads are the most specific
The key differences are in how they handle contexts and dependencies:
Functors apply functions to values in a context
Applicatives apply functions in a context to values in a context
Monads use the result of one computation to determine the next computation
When to Use Each
Use Functor when you just need to transform values inside a context
Use Applicative when you need to combine independent values in contexts
Use Monad when each step depends on the result of the previous step
Key Points to Remember
Functors let you apply a function to a value in a context, preserving the structure
The key functor operation is fmap or <$>
Applicative functors let you apply a function in a context to a value in a context
The key applicative operations are pure and <*>
Applicatives are more powerful than functors but less powerful than monads
Applicatives excel at combining independent computations
Haskell:
\x -> \y -> x + y-- Or equivalently\x y -> x + y
Many Haskell concepts can be traced back to lambda calculus:
Lazy evaluation (similar to normal-order evaluation in lambda calculus; see Evaluation Strategies)
Evaluation Strategies
Normal Order Evaluation
In normal order evaluation (which Haskell uses a variant of), function arguments are substituted without being evaluated, and then the resulting expression is evaluated. For more, see Evaluation Strategies.
Church-Rosser Theorem
The Church-Rosser theorem states that if an expression can be reduced in two different ways, then there exists a common form that both reduction paths eventually reach. This property is also discussed in Expressions and Reduction.
Computability and the Lambda Calculus
The lambda calculus is Turing complete, meaning it can express any computable function. This was one of the key insights that led to the development of modern computers.
Lambda calculus is a formal system in mathematical logic and computer science for expressing computation based on function abstraction and application. Developed by Alonzo Church in the 1930s, it forms the theoretical foundation of functional programming languages, including Haskell.
Core Concepts
The lambda calculus consists of three basic components:
Variables: Symbols that represent parameters or values (e.g., x, y, z)
Function abstraction: Creating a function that maps an input to an output
Function application: Applying a function to an argument
Syntax
The syntax of the lambda calculus can be defined as:
<expr> ::= <variable> -- Variable
| λ<variable>.<expr> -- Abstraction (creating a function)
| <expr> <expr> -- Application (applying a function)
Examples
Variable: x
Abstraction: λx.x (the identity function)
Application: (λx.x) y (applying the identity function to y)
Rewrite Rules
α-conversion (Alpha Conversion)
Alpha conversion allows renaming bound variables as long as no free variables are captured:
λx.M ≡ λy.M[y/x]
where M[y/x] means “substitute y for x in M”, and y is not already a free variable in M.
Example:
λx.x ≡ λy.y
β-reduction (Beta Reduction)
Beta reduction represents function application - substituting the argument for the parameter in the function body:
(λx.M) N → M[N/x]
Example:
(λx.x+1) 5 → 5+1 → 6
η-conversion (Eta Conversion)
Eta conversion captures the idea that two functions are equivalent if they give the same result for all inputs:
λx.(f x) ≡ f
where x does not appear free in f.
Church Encodings
Since lambda calculus has only functions, we need to encode data using functions.
Booleans
true = λx.λy.x
false = λx.λy.y
These encode the behavior of true and false in an if-then-else construct:
true returns the first argument
false returns the second argument
Natural Numbers (Church Numerals)
Church numerals represent natural numbers by applying a function n times:
0 = λf.λx.x
1 = λf.λx.f x
2 = λf.λx.f (f x)
3 = λf.λx.f (f (f x))
...
n = λf.λx.f^n x (f applied n times to x)
Arithmetic Operations
succ = λn.λf.λx.f (n f x) -- Successor function
add = λm.λn.λf.λx.m f (n f x) -- Addition
mult = λm.λn.λf.m (n f) -- Multiplication
Fixed Point Combinators
Fixed point combinators allow us to define recursive functions in the lambda calculus, where recursion isn’t directly supported.
The Y combinator is a classic fixed point combinator:
Y = λf.(λx.f (x x)) (λx.f (x x))
Given a function f, Y f is a fixed point of f, meaning that:
Y f = f (Y f)
This allows us to create recursive functions.
Connection to Haskell
Haskell’s syntax is directly inspired by lambda calculus. For example:
Lambda calculus:
λx.λy.x + y
Haskell:
\x -> \y -> x + y-- Or equivalently\x y -> x + y
Many Haskell concepts can be traced back to lambda calculus:
Anonymous functions (lambdas)
Higher-order functions
Currying
Lazy evaluation (similar to normal-order evaluation in lambda calculus)
Evaluation Strategies
Normal Order Evaluation
In normal order evaluation (which Haskell uses a variant of), function arguments are substituted without being evaluated, and then the resulting expression is evaluated.
Example:
(λx.x+x) (1+2)
→ (1+2)+(1+2)
→ 3+3
→ 6
Applicative Order Evaluation
In applicative order evaluation, function arguments are evaluated before being substituted.
Example:
(λx.x+x) (1+2)
→ (λx.x+x) 3
→ 3+3
→ 6
Church-Rosser Theorem
The Church-Rosser theorem states that if an expression can be reduced in two different ways, then there exists a common form that both reduction paths eventually reach.
This ensures that the lambda calculus is confluent, meaning the order of reductions doesn’t affect the final result (if it exists).
Computability and the Lambda Calculus
The lambda calculus is Turing complete, meaning it can express any computable function. This was one of the key insights that led to the development of modern computers.
Examples in Haskell
Encoding Church Numerals in Haskell
-- Church numeral typetype Church a = (a -> a) -> a -> a-- Converting from Integer to ChurchtoChurch :: Integer -> Church atoChurch 0 = \f x -> xtoChurch n = \f x -> f (toChurch (n-1) f x)-- Converting from Church to IntegerfromChurch :: Church Integer -> IntegerfromChurch n = n (+1) 0-- Church arithmeticchurchSucc :: Church a -> Church achurchSucc n = \f x -> f (n f x)churchAdd :: Church a -> Church a -> Church achurchAdd m n = \f x -> m f (n f x)churchMult :: Church a -> Church a -> Church achurchMult m n = \f -> m (n f)
Y Combinator in Haskell
-- Y combinator (with a type annotation to make it work in Haskell)y :: (a -> a) -> ay f = (\x -> f (x x)) (\x -> f (x x))-- Using Y to define factorialfactorial = y $ \f n -> if n <= 0 then 1 else n * f (n-1)
Note: This won’t actually work in Haskell due to the type system, but it illustrates the concept.
Key Points to Remember
Lambda calculus consists of variables, abstractions (functions), and applications
Alpha conversion renames bound variables
Beta reduction substitutes arguments into function bodies
Church encodings represent data as functions
Fixed point combinators enable recursion
The lambda calculus is Turing complete
Haskell’s syntax and semantics are directly inspired by lambda calculus
Equational reasoning is a powerful technique for analyzing and proving properties of functional programs. In pure functional languages like Haskell, it’s particularly effective due to referential transparency. For more on purity and referential transparency, see Expressions and Reduction and Functions and Equations.
The Foundations: Referential Transparency
A program is referentially transparent if any expression can be replaced with its value without changing the program’s behavior. This property is what makes equational reasoning possible and is central to Haskell’s design (Expressions and Reduction).
let x = 5 + 3 in x * 2= let x = 8 in x * 2= 8 * 2= 16
Each step replaces an expression with its equivalent value.
Proof Techniques
Direct Substitution
Directly substitute definitions and simplify:
-- Define a functiondouble x = x + x-- Prove: double (a + b) = double a + double bdouble (a + b)= (a + b) + (a + b) -- By definition of double= a + b + a + b -- Associativity of (+)= a + a + b + b -- Commutativity and associativity of (+)= double a + double b -- By definition of double
data Tree a = Leaf | Node a (Tree a) (Tree a)depth :: Tree a -> Intdepth Leaf = 0depth (Node _ l r) = 1 + max (depth l) (depth r)-- Prove: depth (mirror t) = depth t-- where mirror is:mirror :: Tree a -> Tree amirror Leaf = Leafmirror (Node x l r) = Node x (mirror r) (mirror l)-- Base case: t = Leafdepth (mirror Leaf)= depth Leaf -- By definition of mirror= 0 -- By definition of depth-- Inductive case: t = Node x l r-- Assume: depth (mirror l) = depth l and depth (mirror r) = depth rdepth (mirror (Node x l r))= depth (Node x (mirror r) (mirror l)) -- By definition of mirror= 1 + max (depth (mirror r)) (depth (mirror l)) -- By definition of depth= 1 + max (depth r) (depth l) -- By induction hypothesis= 1 + max (depth l) (depth r) -- By commutativity of max= depth (Node x l r) -- By definition of depth
Working with Higher-Order Functions
Equational reasoning extends to higher-order functions, which is particularly useful for understanding and optimizing functional code (Higher-Order Functions, Functions and Equations).
Example: Map Fusion
-- Prove: map f (map g xs) = map (f . g) xs-- Base case: xs = []map f (map g [])= map f [] -- By definition of map= [] -- By definition of map= map (f . g) [] -- By definition of map-- Inductive case: xs = (x:xs')-- Assume: map f (map g xs') = map (f . g) xs'map f (map g (x:xs'))= map f (g x : map g xs') -- By definition of map= f (g x) : map f (map g xs') -- By definition of map= (f . g) x : map f (map g xs') -- By definition of (.)= (f . g) x : map (f . g) xs' -- By induction hypothesis= map (f . g) (x:xs') -- By definition of map
-- Original functionsum (filter p xs)-- Optimizationsum (filter p (xs ++ ys))= sum (filter p xs ++ filter p ys) -- filter distributes over (++)= sum (filter p xs) + sum (filter p ys) -- sum distributes over (++)
This optimization allows for parallel computation of the sums.
-- Map lawsmap id = idmap f . map g = map (f . g)-- Filter lawsfilter p . filter q = filter (\x -> p x && q x)filter p . map f = map f . filter (p . f)-- Fold lawsfoldr f z [] = zfoldr f z (x:xs) = f x (foldr f z xs)foldr f z (map g xs) = foldr (\x acc -> f (g x) acc) z xs
Functor Laws
-- Identityfmap id = id-- Compositionfmap f . fmap g = fmap (f . g)
Applicative Laws
-- Identitypure id <*> v = v-- Compositionpure (.) <*> u <*> v <*> w = u <*> (v <*> w)-- Homomorphismpure f <*> pure x = pure (f x)-- Interchangeu <*> pure y = pure ($ y) <*> u
Monad Laws
-- Left identityreturn a >>= f = f a-- Right identitym >>= return = m-- Associativity(m >>= f) >>= g = m >>= (\x -> f x >>= g)
Free Theorems
Parametricity in Haskell leads to “free theorems” - properties that must hold based solely on a function’s type signature.
For example, any function of type [a] -> [a] (that doesn’t use undefined or similar) must satisfy:
map f . g = g . map f
for any function f. This is because g can’t inspect, create, or modify the elements - it can only rearrange, drop, or duplicate them.
Tooling Support
Haskell’s ecosystem provides tools to aid equational reasoning:
QuickCheck: Property-based testing to verify equations
LiquidHaskell: Refinement types that can express and verify properties
Coq/Agda integration: For formal verification of Haskell code