Intro to Functional Programming With Haskell

Although I’m more familiar with OCaml as my first functional programming language, I wanted to try to communicate some functional programming concepts through Haskell. The two languages share a lot of DNA, but Haskell is in many ways a purer expression of the functional paradigm, which makes it a useful lens.

Every single person who learns how to program is almost always taught in the imperative style first. Meaning that students are meant to visualize a computer program as a set of step-by-step instructions to accomplish a particular task. The example I’ve been given many times is writing a recipe to construct a peanut butter and jelly sandwich. Thinking about how algorithms are executed imperatively is excellent for understanding programming in general, and I would argue that it is the most intuitive way of looking at the world.

However, it is not the only style of programming that exists. There is declarative programming, which specifies what you want and not the how. This is akin to saying “Create a peanut butter sandwich”, instead of fleshing out each detail on how to do so. If you have ever used SQL, you have most certainly written declarative code, e.g. SELECT * FROM employees WHERE last_name='Smith' ORDER BY first_name;. It is almost like magic: you’ve merely told the computer what you want and somehow it has found the answer without you specifying the implementation.

Functional programming is a particular flavor of declarative programming. The core idea is that a program is defined entirely as a composition of mathematical functions - and I mean mathematical in the strict sense. A mathematical function takes some inputs and returns an output. That’s it. It doesn’t print to the screen, it doesn’t modify a global variable, it doesn’t send an HTTP request. Given the same inputs, it always returns the same output. Functions with this property are called pure.

Why Purity Matters

This might sound limiting, but purity buys you a lot. Consider this Python snippet:

count = 0

def increment():
    global count
    count += 1
    return count

What does increment() return? It depends entirely on how many times it’s been called before. That’s state leaking out of the function and into the world. In a large program, tracking down where that state gets modified - and in what order - can be genuinely painful.

In Haskell, functions don’t have this escape hatch. Everything a function needs must be passed in as an argument, and its only effect on the world is its return value. Here’s a trivial example:

add :: Int -> Int -> Int
add x y = x + y

The :: line is the type signature. add takes two Ints and returns an Int. There’s no hidden state, no side effects. You could call add 3 4 a million times and it would return 7 every single time.

Lists and Recursion

In Haskell, you won’t find a for loop. Iteration is expressed through recursion and, more commonly, through a set of standard library functions that operate on lists. Let’s say we want to sum a list of integers:

mySum :: [Int] -> Int
mySum []     = 0
mySum (x:xs) = x + mySum xs

This uses pattern matching to handle two cases: the empty list (base case) and a list with at least one element. The (x:xs) pattern binds x to the head of the list and xs to the rest - a very common Haskell idiom.

Of course, you’d rarely write mySum yourself because sum already exists in the standard library. The point is that the recursion is right there in the open, explicit and traceable.

List Comprehensions

One of the features I really like about Haskell is list comprehensions. They’re borrowed from set-builder notation in math and let you express filtered, transformed lists in a very readable way. Suppose you want the squares of all even numbers from 1 to 20:

evenSquares :: [Int]
evenSquares = [x * x | x <- [1..20], even x]

Read it as: “the list of x * x for each x drawn from 1 to 20, where x is even.” If you’ve spent time in Python, this syntax will feel familiar - Python’s list comprehensions were directly inspired by Haskell’s.

Immutability

In Haskell, variables don’t vary. Once you bind a name to a value, that’s it - the binding is permanent for the duration of its scope. There’s no reassignment. This sounds strange coming from an imperative background, but it makes programs easier to reason about. If you see x = 5 somewhere in the code, you don’t have to worry about whether x got mutated somewhere else down the line. It didn’t!

This is a big shift in mindset. Instead of updating values in place, you produce new values. Instead of mutating a list, you return a new list with the changes applied. It takes some adjustment, but once it clicks, the absence of mysterious mutations is actually a relief.


There’s obviously a lot more to explore - type classes, monads, lazy evaluation. These are some of the things that make Haskell genuinely different from other languages and also what gives it a reputation for having a steep learning curve. But the fundamentals covered here - purity, pattern matching, recursion, immutability - are the foundation that everything else builds on. Next up, we’ll talk about one of the most powerful ideas in functional programming: treating functions as first-class values!

← Back to Writing