Algebraic Data Types and Pattern Matching
ADTs allow you to define your own data structures. Pattern matching lets you deconstruct them elegantly.
data Maybe a = Nothing | Just a
safeHead :: [a] -> Maybe a
safeHead [] = Nothing
safeHead (x:_) = Just xRecursive ADTs enable trees and more:
data Tree a = Leaf | Node a (Tree a) (Tree a)
treeSum :: Tree Int -> Int
treeSum Leaf = 0
treeSum (Node v l r) = v + treeSum l + treeSum rTypeclasses and Instances
Typeclasses define interfaces; instances implement them for specific types.
class Eq a where
(==) :: a -> a -> Bool
instance Eq Bool where
True == True = True
False == False = True
_ == _ = FalseYou can create your own typeclasses and derive instances:
class Pretty a where
pretty :: a -> String
instance Pretty Int where
pretty = showFunctors, Applicatives, and Monads
Functor
A type that can be mapped over.
instance Functor Maybe where
fmap _ Nothing = Nothing
fmap f (Just x) = Just (f x)Laws:
fmap id = idfmap (f . g) = fmap f . fmap g
Applicative
Allows function application within a context.
instance Applicative Maybe where
pure = Just
Nothing <*> _ = Nothing
(Just f) <*> mx = fmap f mxLaws: Identity, Homomorphism, Interchange, Composition
Monad
Allows sequencing of computations with context.
instance Monad Maybe where
return = Just
Nothing >>= _ = Nothing
(Just x) >>= f = f xLaws: Left identity, Right identity, Associativity
Idioms:
- Use
donotation for readable monadic code. - Use monads for error handling (
Maybe,Either), side effects (IO), state (State), etc.
Monad Transformers
Combine effects from multiple monads.
import Control.Monad.Trans.Maybe
import Control.Monad.Trans.Class (lift)
findUser :: UserId -> MaybeT IO User
findUser uid = do
mUser <- lift $ queryDatabase uid
MaybeT $ return mUserStacking transformers allows you to handle, for example, IO and error handling together.
Property-Based Testing
Test properties for a wide range of inputs using QuickCheck.
import Test.QuickCheck
prop_sortIdempotent :: [Int] -> Bool
prop_sortIdempotent xs = sort (sort xs) == sort xs
-- Custom generator
genEven :: Gen Int
genEven = (*2) <$> arbitrary
prop_evenIsEven :: Property
prop_evenIsEven = forAll genEven evenParser Combinators
Build complex parsers by combining simple ones.
import Text.Parsec
import Text.Parsec.String (Parser)
number :: Parser Int
number = read <$> many1 digit
commaSepNumbers :: Parser [Int]
commaSepNumbers = number `sepBy` char ','Equational Reasoning
Prove properties and optimize code by substituting equals for equals.
Proof Example:
-- Prove: map f (map g xs) = map (f . g) xsOptimization Example:
sum (filter p (xs ++ ys))
= sum (filter p xs ++ filter p ys)
= sum (filter p xs) + sum (filter p ys)Evaluation Strategies
Haskell uses lazy evaluation by default.
naturals = [0..] -- Infinite list, only evaluated as needed
take 5 naturals -- [0,1,2,3,4]Strictness:
sum' :: [Int] -> Int
sum' = go 0
where
go !acc [] = acc
go !acc (x:xs) = go (acc + x) xsUse seq or bang patterns to force evaluation and avoid space leaks.
Referential Transparency
Referential transparency is a property of expressions in programming and mathematics. An expression is referentially transparent if it can be replaced with its value without changing the program’s behavior.
Definition
- An expression is referentially transparent if, whenever it appears in a program, it can be replaced by its result (value) without affecting the program’s meaning or outcome.
Example
-- This function is referentially transparent
add x y = x + y
-- The expression (add 2 3) can always be replaced with 5In the above example, add 2 3 will always yield 5, no matter where or when it is evaluated.
Importance
- Predictability: Code is easier to reason about, test, and debug.
- Optimization: Compilers can safely cache or reorder computations.
- Parallelism: Independent computations can be executed in parallel without side effects.
Contrast: Referential Opacity
If an expression’s value can change depending on context (e.g., due to side effects or mutable state), it is referentially opaque.
# Not referentially transparent
import random
def get_random():
return random.randint(1, 10)
# get_random() may return different values each timeIn Functional Programming
Referential transparency is a core concept in functional programming. Pure functions (without side effects) are referentially transparent, enabling many of the benefits of functional languages.