Haskell for the Mathematical Programmer
Study Guide
📖 Core Concepts
Haskell Fundamentals Haskell is a pure functional language where functions are mathematical mappings without side effects, and code execution is deferred through lazy evaluation until a result is needed.
Type System Haskell’s strong, static type system uses algebraic data types (Sums and Products) to model data with mathematical precision, ensuring correctness at compile-time.
Type Classes Type classes define sets of operations for types, analogous to algebraic structures like groups or rings, enabling ad-hoc polymorphism that is verified by the compiler.
Monads & Applicatives Monads provide a powerful abstraction for sequencing computations within a context, such as state or error handling, turning complex operations into manageable, composable pipelines.
Side Effects & State The IO and State monads isolate real-world interactions and mutable state from pure logic, describing actions to be performed rather than executing them directly.
Advanced Haskell Advanced features like GADTs and Type Families allow for type-level programming, enabling the creation of highly expressive, type-safe Domain Specific Languages (DSLs) for complex modeling.
📌 Must Remember
Haskell Fundamentals
- Purity: Functions in Haskell have no side effects. Given the same input, a function will always return the same output, a property known as referential transparency.
- Everything is an Expression: Haskell has no statements.
if-then-else,case, andletconstructs all evaluate to a value. - Lazy Evaluation: Expressions are not evaluated when they are bound to a variable, but only when their result is demanded. Unevaluated expressions are stored as “thunks.”
- Immutability: Once a value is defined, it cannot be changed. New values are created instead of modifying existing ones.
- Currying is Default: Every function in Haskell officially takes only one argument. Functions of multiple arguments are implemented as functions that return functions.
- Function Application: Function application is done by simple juxtaposition (e.g.,
f x), not with parentheses likef(x).
Type System
- Strong & Static: Types are checked at compile time, not runtime. If a program compiles, it is guaranteed to be free of a large class of runtime errors.
- Type Inference: You rarely need to write explicit type signatures for top-level functions, as the compiler can infer them, though it is good practice to do so.
- Algebraic Data Types (ADTs): Data is modeled using Sum Types (logical OR, like
data Bool = True | False) and Product Types (logical AND, likedata Point = Point Float Float). - Pattern Matching: This is the primary way to deconstruct data. It is a powerful form of control flow that checks for structural equality and binds variables.
- Parametric Polymorphism: Functions can be written to operate on any type by using type variables (e.g.,
id :: a -> a). This is similar to generics in other languages. - Maybe and Either: These are standard Sum Types used for safe error handling.
Maybe arepresents an optional value, andEither a brepresents a value that can be one of two types, often used for success/failure cases.
Type Classes
- Not Object-Oriented Classes: Type classes are more like interfaces or mathematical structures (e.g., a Group). They define a set of functions that a type must implement to be an “instance” of that class.
- Ad-Hoc Polymorphism: A function can operate on any type that is an instance of a particular type class. For example,
(+)works on any type in theNumclass. - Key Standard Classes:
Eq(equality),Ord(ordering),Show(string representation), andRead(parsing from a string) are fundamental. - Functor: A type class for types that can be mapped over (e.g., lists,
Maybe). It abstracts the action of applying a function to every element inside a structure. - Derived Instances: The compiler can automatically generate instances for common type classes like
Eq,Ord, andShowfor many data types.
Monads & Applicatives
- Contextual Computation: Monads are a design pattern for building pipelines of computations that are wrapped in a context (e.g., potential absence of a value with
Maybe, I/O actions withIO). - Applicative is a Superset of Functor: All Applicatives are Functors. Applicatives allow you to apply a function wrapped in a context to a value wrapped in a context.
- Monad is a Superset of Applicative: All Monads are Applicatives. Monads add the ability to sequence computations where the next computation depends on the result of the previous one.
do-notation is Syntactic Sugar: It is a cleaner syntax for chaining monadic operations. Everydoblock can be rewritten using the bind operator(>>=).- The Monad Laws: All valid Monad instances must obey three laws: Left Identity, Right Identity, and Associativity. These ensure predictable, chainable behavior.
Side Effects & State
IO ais a Description: AnIO avalue does not contain a value of typea. It is a description of a computation that, when executed, will produce a value of typeaand may have side effects.- Purity is Maintained: Side effects are isolated within the
IOmonad. Code outside ofIOremains pure and referentially transparent. mainis the Entry Point: The entire Haskell program is the evaluation ofmain :: IO (). The runtime system executes the I/O actions described by this value.- The State Monad:
State s aallows for the simulation of mutable state purely. It represents a computation that takes an initial statesand produces a resultaalong with a new states. - **No
escaping” a Monad in Pure Code:** You cannot extract a value from an IO or State context and use it in pure code. You must use monadic functions to work within the context.
Advanced Haskell
- Monad Transformers: Libraries like
mtlprovide transformers (e.g.,StateT,ReaderT) to combine the effects of multiple monads into a single monad stack. - GADTs (Generalized Algebraic Data Types): GADTs allow the return type of a data constructor to be more specific than the data type itself, enabling you to encode more complex invariants at the type level.
- Type Families: These are functions that operate on types, allowing for type-level programming. They are a way to compute a type from another type.
- DSLs (Domain Specific Languages): Haskell's strong type system and flexible syntax make it ideal for creating embedded DSLs, which allow you to write programs for a specific domain (like quantum circuits or linear algebra) that are type-checked by the Haskell compiler.
- Kinds: Kinds are the “types of types.” For example,
Inthas kind*, whileMaybehas kind* -> *because it takes a type to produce a type.
📚 Key Terms
Purity: A property of a function that guarantees it has no observable side effects (like I/O or modifying global state) and always returns the same output for the same input.
- Used in context: The
(+)function is pure; it only calculates a sum and doesn't change anything in the outside world. - Topic: Haskell Fundamentals
Lazy Evaluation: An evaluation strategy where expressions are not computed until their results are actually needed, allowing for the definition of infinite data structures.
- Used in context:
take 5 [1..]works because lazy evaluation only computes the first five elements of the infinite list[1..]. - Topic: Haskell Fundamentals
Currying: The technique of translating a function that takes multiple arguments into a sequence of functions, each with a single argument.
- Used in context:
add x yis syntactic sugar for(add x) y;add xis a curried function that returns a new function waiting fory. - Don't confuse with: Tupled arguments, where a function takes a single tuple like
add (x, y). - Topic: Haskell Fundamentals
Sum Type: An algebraic data type that can have a value from a set of possible options (a logical OR), where each option is a distinct data constructor.
- Used in context:
data Shape = Circle Float | Rectangle Float Floatis a Sum Type; aShapecan be either aCircleor aRectangle. - Topic: Type System
Product Type: An algebraic data type that combines several values into one (a logical AND), typically using a single data constructor.
- Used in context:
data Person = Person String Intis a Product Type; aPersonvalue contains both aStringand anInt. - Topic: Type System
Type Class: A construct that defines a set of functions and properties that a type must support to be a member of that class, enabling ad-hoc polymorphism.
- Used in context: To use the
==operator on ourPersontype, we must make it an instance of theEqtype class. - Don't confuse with: Classes in object-oriented programming.
- Topic: Type Classes
Functor: A type class representing data structures that can be mapped over, defined by the function fmap :: (a -> b) -> f a -> f b.
- Used in context:
List,Maybe, andIOare all Functors because you can usefmapto apply a function to the value(s) inside them. - Topic: Type Classes
Monad: A design pattern and type class that provides a structure for sequencing computations within a context, defined by the return function and the bind operator (>>=).
- Used in context: The
Maybemonad sequences computations that might fail, automatically propagatingNothingvalues. - Topic: Monads & Applicatives
Thunk: An unevaluated expression stored in memory, representing a computation that will be forced (evaluated) when its result is needed.
- Used in context: In
let x = 1 + 2,xis initially bound to a thunk representing1 + 2, not the value3. - Topic: Haskell Fundamentals
IO Monad: A specific monad that encapsulates actions with side effects (I/O), isolating them from the pure parts of the program.
- Used in context:
readFile "file.txt"doesn't return aString, but anIO String—a description of an action that will produce a string. - Topic: Side Effects & State
GADT (Generalized Algebraic Data Type): An extension to Haskell's data types that allows data constructors to explicitly specify their return type, enabling more expressive and safe type-level constraints.
- Used in context: Using a GADT, we can define a type for expressions that guarantees type safety, e.g., an
Expr awhereais the type of the evaluated expression. - Topic: Advanced Haskell
🔍 Key Comparisons
| Feature | Imperative (Python/Node.js) | Functional (Haskell) |
|---|---|---|
| Execution | Eager evaluation of statements in a sequence. | Lazy evaluation of expressions based on demand. |
| State | Relies on mutable state and variables. | Avoids mutable state; data is immutable. |
| Functions | Procedures that can have side effects. | Mathematical mappings from inputs to outputs. |
| Control Flow | Loops (for, while), if statements. | Recursion, pattern matching, function composition. |
| Error Handling | Exceptions, try-catch blocks. | Type-based (Maybe, Either), monadic control flow. |
Memory trick: Imperative programming is a list of instructions for the computer to follow. Functional programming is a description of what the result is.
Topic: Haskell Fundamentals
| Feature | Functor | Applicative | Monad |
|---|---|---|---|
| Purpose | Applying a pure function to a value in a context. | Applying a function in a context to a value in a context. | Sequencing computations where one depends on the last. |
| Key Function | fmap :: (a -> b) -> f a -> f b | <*> :: f (a -> b) -> f a -> f b | >>= :: m a -> (a -> m b) -> m b |
| Analogy | A conveyor belt applying one tool to each item. | Two conveyor belts merging items with tools. | A conveyor belt where the item's properties decide the next tool. |
| Use Case | fmap (+1) (Just 2) | (+) <$> Just 2 <*> Just 3 | Just 3 >>= \x -> Just (x + 1) |
Memory trick: Functor maps, Applicative applies, Monad binds (chains). Each is a superset of the one before it: Monad > Applicative > Functor.
Topic: Monads & Applicatives
| Feature | Maybe a | Either a b |
|---|---|---|
| Purpose | Represents a value that may or may not exist. | Represents a value that can be one of two distinct types. |
| Constructors | Just a (value exists) or Nothing (value is absent). | Left a (often an error) or Right b (often a success). |
| Error Info | Nothing carries no information about why it failed. | Left a can hold detailed information about the failure. |
| Common Use | Optional values, safe dictionary lookups. | Functions that can fail with a specific error type. |
| Example | lookup :: Eq a => a -> [(a, b)] -> Maybe b | parseNumber :: String -> Either String Int |
Memory trick: Maybe it's there, Maybe it's not. With Either, you get either this or that.
Topic: Type System
⚠️ Common Mistakes
❌ MISTAKE: Thinking IO String is a String.
- Why it happens: In imperative languages, a function like
readFile()returns a string directly. In Haskell, it returns a description of an action. - ✅ Instead: Treat
IO Stringas a program that, when run, will produce aString. Usedo-notation or(>>=)to work with the value inside the IO context. - Topic: Side Effects & State
❌ MISTAKE: Confusing Type Classes with object-oriented classes.
- Why it happens: The name “class” is overloaded. OO classes bundle data and methods together into objects.
- ✅ Instead: Think of Type Classes as contracts or mathematical structures. They define a set of functions that a type must support, but they don't hold data or state.
- Topic: Type Classes
❌ MISTAKE: Trying to “get a value out” of a Monad in pure code.
- Why it happens: The goal is often to use a value from a
MaybeorIOin a pure function, leading to attempts to “unwrap” it unsafely. - ✅ Instead: Bring the pure function into the Monad's context. Use
fmapfor simple function application, or bind(>>=)to chain operations that stay within the context. - Topic: Monads & Applicatives
❌ MISTAKE: Using parentheses for function application, like f(x, y).
- Why it happens: This syntax is standard in nearly all C-style languages (Python, Java, JavaScript, etc.).
- ✅ Instead: Remember that function application in Haskell is just juxtaposition:
f x y. Parentheses are only used for grouping expressions, likef (x + y). - Topic: Haskell Fundamentals
❌ MISTAKE: Writing complex chains of (>>=) instead of using do-notation.
- Why it happens: Beginners often learn
(>>=)first and try to write everything with it, leading to deeply nested and hard-to-read lambda expressions (the “pyramid of doom”). - ✅ Instead: Use
do-notation for any monadic sequence with more than two steps. It is functionally equivalent but vastly more readable. - Topic: Monads & Applicatives
🔄 Key Processes
Lazy Evaluation with Thunks
Step 1: Binding
- What happens: An expression is bound to a variable. The expression is NOT evaluated.
- Key indicator: A
letorwhereclause is used, e.g.,let x = someComplexCalculation 10. - In memory: A “thunk” is created. This is a data structure containing the function (
someComplexCalculation) and its arguments (10).
Step 2: Demanding the Value
- What happens: Another part of the code requires the value of the variable (e.g., trying to print it).
- Key indicator: The variable is used in a context that needs a concrete value, like an argument to
printor anifcondition.
Step 3: Forcing the Thunk
- What happens: The runtime system executes the code stored in the thunk.
- Key indicator: The CPU performs the actual computation.
Step 4: Updating the Thunk
- What happens: The thunk is overwritten in memory with the result of the computation (its Normal Form).
- Key indicator: The thunk is replaced by the value (e.g.,
12345). - Why it matters: The next time the variable
xis accessed, the value is returned immediately without re-computation. This is call-by-need.
Topic: Haskell Fundamentals
Pattern Matching Execution Flow
Step 1: Function Call
- What happens: A function with multiple clauses (patterns) is called with arguments.
- Example:
safeHead [1,2,3]wheresafeHeadis defined as:safeHead :: [a] -> Maybe a safeHead [] = Nothing -- Clause 1 safeHead (x:xs) = Just x -- Clause 2
Step 2: Top-to-Bottom Pattern Check
- What happens: The runtime checks the input
[1,2,3]against the patterns of each clause in order. - Clause 1: Does
[]match[1,2,3]? No. - Clause 2: Does
(x:xs)match[1,2,3]? Yes.
Step 3: Variable Binding
- What happens: Upon a successful match, variables in the pattern are bound to the corresponding parts of the input data.
- Key indicator: In our example,
xis bound to1andxsis bound to[2,3].
Step 4: Right-Hand Side Evaluation
- What happens: The expression on the right-hand side of the matching clause is evaluated using the newly bound variables.
- In our example: The expression
Just xis evaluated. Sincexis1, the result isJust 1. - Common error: Forgetting a catch-all pattern (
_), which can lead to a runtime exception if no patterns match.
Topic: Type System
📐 Key Formulas
Functor Law: Identity
- Variables:
fmap: The mapping function for any Functor.id: The identity function (id x = x).
- When to use: This is a property that every valid
Functorinstance must satisfy. It's used to reason about code and ensure predictable behavior. - Common error: Creating a Functor instance that modifies the structure of the container, thus violating this law.
Topic: Type Classes
Monad Law: Left Identity
- Variables:
return: Lifts a pure value into a monad.a: A pure value.>>=: The bind operator.k: A function from a pure value to a monadic value (a -> m b).
- When to use: This law must hold for any valid
Monadinstance. It ensures thatreturndoesn't add any extra effects. - Common error: Defining
returnin a way that introduces unexpected side effects or contextual changes.
Topic: Monads & Applicatives