Chapter 7: Recursion = GOTO Recursion

  1. Introduction
  2. How Recursion Works
  3. What is Looping?
  4. Why Recursion is Better
  5. How to Write Recursive Functions in Haskell
  6. Thinking Recursively Instead of Imperatively
  7. Recap & Exercises

1. Introduction

Recursion is a fundamental concept in functional programming, and it plays a critical role in Haskell. At its core, recursion allows a function to call itself, enabling us to solve complex problems by breaking them down into smaller, simpler subproblems. This idea isn’t just limited to programming—it’s a concept rooted in mathematics and nature.

2. How Recursion Works

The Structure of a Recursive Function

Every recursive function consists of two essential parts:

  1. Base Case: The condition where the function stops calling itself. This prevents infinite recursion. Without a base case, recursion would continue infinitely, eventually causing a stack overflow.
  2. Recursive Case: The part of the function where it calls itself with a smaller or simpler input. This defines how the problem is broken down into smaller instances that the function can solve by itself.

Example 1: Summing a List

Let's look at a simple recursive function to calculate the sum of a list:

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

Breaking It Down:

  1. Base Case:
    • When the list is empty ([]), the sum is 0. This is the simplest possible input for this function.
  2. Recursive Case:
    • The list is split into its head (x) and tail (xs). The function computes the sum of the tail (sumList xs) and adds it to the head.

Execution Trace

Let’s see how this function works step-by-step for the input [1, 2, 3]:

  1. sumList [1, 2, 3] = 1 + sumList [2, 3]
  2. sumList [2, 3] = 2 + sumList [3]
  3. sumList [3] = 3 + sumList []
  4. sumList [] = 0

Combining the results:

The result is 6.

Example 2: Factorial of a Number

The factorial of a number n is the product of all integers from 1 to n. For example, factorial(4) equals 4 * 3 * 2 * 1.

Here’s the recursive implementation in Haskell:

factorial :: Int -> Int
factorial 0 = 1               -- Base case: factorial of 0 is 1
factorial n = n * factorial (n - 1) -- Recursive case: n * factorial of (n-1)

Execution Trace

Let’s compute factorial 4:

  1. factorial 4 = 4 * factorial 3
  2. factorial 3 = 3 * factorial 2
  3. factorial 2 = 2 * factorial 1
  4. factorial 1 = 1 * factorial 0
  5. factorial 0 = 1 (base case)

Combining the results:

The result is 24.

Why Recursion is So Powerful

1. Purity and Safety

Recursive functions in Haskell are pure expressions:

2. Mathematical Precision

Recursion mirrors the way many problems are defined in mathematics:

This alignment with mathematical principles ensures:

3. Expressiveness

Recursive functions often lead to concise and elegant solutions. The entire problem-solving process is embedded directly in the function definition, making the code easier to read and maintain.

Recursive Thinking in Everyday Life

To understand recursion intuitively, consider this example:

This process of narrowing down the problem until you reach the simplest case mirrors how recursive functions work.

Recursive Problems in Nature

Recursion isn’t just a programming construct—it appears in nature, mathematics, and even art:

These examples highlight how recursion is a universal concept that helps us break down and understand complex systems.

3. What is Looping?

In programming, looping is a technique for repeatedly executing a block of code. This is a core concept in imperative programming, where loops like for, while, and do-while are commonly used to handle repetitive tasks such as traversing a list, iterating over numbers, or processing files.

While looping works, it comes with significant challenges, particularly in how it handles state, mutability, and side effects. Functional programming avoids traditional looping constructs entirely, opting instead for recursion, which aligns better with functional principles.

How Looping Works in Imperative Programming

In an imperative language like Python, Java, or C#, looping relies on mutable variables that change their state during each iteration of the loop.

Example: Summing a List (Imperative Style)

Let’s sum a list of integers in Python:

def sum_list(lst):
    total = 0              # Initialize mutable state
    for x in lst:          # Loop through the list
        total += x         # Mutate the total in each iteration
    return total           # Return the result

How This Works:

  1. State Initialization: The total variable is initialized to 0.
  2. Iteration: For each element in the list, the loop mutates total by adding the element.
  3. Final Result: Once the loop ends, the accumulated value of total is returned.

Problems with Looping

While loops are effective, they introduce several issues that can complicate programming and lead to bugs:

1. Mutable State

Example Issue: Shared Mutable State in Loops

In multithreaded programs, shared variables that are mutated inside a loop can lead to race conditions — unpredictable behavior caused by concurrent threads modifying the same variable. This problem becomes worse when references to shared objects are used, as changes made by one thread affect all threads accessing the same reference.

Imperative Example: Python

Let’s simulate a program where two threads increment a shared counter in a loop:

import threading

# Shared mutable state
counter = 0

# Function to increment the counter in a loop
def increment_counter():
    global counter
    for _ in range(100000):
        counter += 1  # This is not atomic!

# Create two threads
thread1 = threading.Thread(target=increment_counter)
thread2 = threading.Thread(target=increment_counter)

# Start the threads
thread1.start()
thread2.start()

# Wait for both threads to finish
thread1.join()
thread2.join()

# Print the counter
print(f"Final Counter: {counter}")

What Happens:

  1. Both threads are modifying the shared variable counter simultaneously.
  2. Race Condition: The counter += 1 operation involves three steps:
    • Read the current value of counter.
    • Increment it.
    • Write the new value back.
  3. If two threads perform these steps at the same time, they may overwrite each other’s updates, leading to an incorrect final value.

Expected Output:

Final Counter: 200000

Actual Output (Varies):

Final Counter: 165743

Why References Make This Worse

In imperative languages, shared references (e.g., objects or arrays) allow multiple threads to manipulate the same data. This is inherently unsafe without explicit synchronization.

Example: Java (Shared Array)

import java.util.concurrent.atomic.AtomicInteger;

public class SharedReferenceExample {
    static int[] counter = {0};

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> incrementCounter());
        Thread thread2 = new Thread(() -> incrementCounter());

        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();

        System.out.println("Final Counter: " + counter[0]);
    }

    static void incrementCounter() {
        for (int i = 0; i < 100000; i++) {
            counter[0]++; // Not thread-safe
        }
    }
}

Here:

Why Functional Programming Avoids This Problem

Functional programming avoids shared mutable state entirely:

Recursive Example: Haskell

In Haskell, the equivalent logic would be implemented using pure functions and immutable data:

incrementCounter :: Int -> Int -> Int
incrementCounter 0 counter = counter                     -- Base case
incrementCounter n counter = incrementCounter (n - 1) (counter + 1) -- Recursive case

main :: IO ()
main = do
    let result = incrementCounter 100000 0  -- Thread 1
    let result2 = incrementCounter 100000 0 -- Thread 2
    print (result + result2)                -- Combine results

Key Points:

  1. Each thread operates on its own independent copy of the state (counter).
  2. State is passed explicitly, ensuring immutability and thread safety.
  3. There’s no chance of race conditions because functions in Haskell are pure and cannot modify shared state.

Why References and Mutability Are Dangerous

  1. Unpredictability: Shared references lead to hard-to-debug race conditions.
  2. Non-Deterministic Behavior: The program’s output depends on the order of thread execution, which is inherently unpredictable.
  3. Scalability Issues: As the number of threads increases, synchronization mechanisms (e.g., locks) become necessary, adding complexity and reducing performance.

2. Side Effects

Example Issue:

def increment_list(lst):
    for i in range(len(lst)):
        lst[i] += 1    # Mutating the original list (side effect)

This function directly modifies the input list, which might not be the intended behavior. Functional programming avoids this by working with immutable data.

3. Loss of Declarativeness

Imperative vs Declarative Imperative:

result = []
for x in range(10):
    if x % 2 == 0:
        result.append(x)

Declarative (Haskell):

result = [x | x <- [0..9], x `mod` 2 == 0]

The declarative Haskell code directly expresses the result rather than the steps to compute it.

Why Functional Programming Avoids Loops

Functional programming avoids traditional loops because they:

  1. Encourage Mutability: Loops inherently rely on changing variables, which is at odds with functional programming’s principle of immutability.
  2. Introduce Complexity: Tracking mutable state and ensuring correctness in loops can be error-prone.
  3. Conflict with Purity: Loops often involve side effects, breaking Haskell’s core principle of pure functions.
  4. Make Reasoning Harder: Loops require understanding both the current state and how it changes with each iteration, which can be difficult to follow in complex programs.

Instead, functional programming uses recursion, which eliminates these pitfalls by transforming iteration into pure expressions.

Comparing Looping to Recursion

1. State Management

Example: Counting Down from 10 Imperative (Python):

def count_down(n):
    while n > 0:
        print(n)
        n -= 1  # Mutable state

Recursive (Haskell):

countDown :: Int -> IO ()
countDown 0 = return ()         -- Base case
countDown n = do
    print n
    countDown (n - 1)           -- Recursive case with explicit state

2. Control Flow

Example: Finding the First Even Number Imperative (Python):

Copy code
def find_first_even(lst):
    for x in lst:
        if x % 2 == 0:
            return x
    return None  # No even number found

Recursive (Haskell):

findFirstEven :: [Int] -> Maybe Int
findFirstEven [] = Nothing                         -- Base case
findFirstEven (x:xs)
    | x `mod` 2 == 0 = Just x                      -- Found even number
    | otherwise      = findFirstEven xs           -- Recursive case

3. Debugging and Reasoning

Why Recursion is Safer

Recursion in Haskell provides:

  1. Immutability: State is passed explicitly, ensuring it cannot change unexpectedly.
  2. Purity: Each recursive step is a pure function, guaranteeing no side effects.
  3. Composability: Recursive functions are modular and reusable, fitting seamlessly into larger programs.

By avoiding traditional looping constructs, Haskell eliminates entire categories of bugs related to mutable state and side effects, allowing developers to focus on solving problems rather than debugging loops.

Summary: Looping vs Recursion

FeatureLoopingRecursion
State ManagementMutable variablesImmutable function arguments
Side EffectsOften introduces side effectsPure and side-effect free
Control Flowbreak, continueBase and recursive cases
DeclarativenessFocuses on "how"Focuses on "what"
Error-ProneSusceptible to bugs in stateGuaranteed correctness via purity

Functional programming's avoidance of loops leads to safer, more declarative, and easier-to-maintain code.

4. Why Recursion is Better

Recursion doesn’t just replace loops—it introduces an entirely different mindset for solving problems. By leveraging recursion, functional programming eliminates common pitfalls of looping, such as mutable state and side effects, while aligning computations with mathematical precision.

In this section, we focus on why recursion is better than loops, emphasizing how it resolves the shortcomings of imperative constructs and why it’s fundamental to functional programming.

1. Recursion is Immutable by Design

Traditional loops rely on mutable variables to track state, like counters or accumulators, which can lead to subtle bugs and unintended behavior. In contrast, recursion passes state explicitly as arguments, ensuring immutability and predictability.

Example: Counting Down

Imperative (Python):

def count_down(n):
    while n > 0:
        print(n)
        n -= 1  # Mutable state

Recursive (Haskell):

countDown :: Int -> IO ()
countDown 0 = return ()          -- Base case
countDown n = do
    print n
    countDown (n - 1)            -- Recursive case

Key Differences:

2. Recursion Eliminates Side Effects

Loops often interact with external variables or systems during iterations, creating side effects that are difficult to control. Recursion, being pure, guarantees no side effects, making programs easier to reason about and debug.

Problem: Mutable Global State in Loops

Imperative (Python):

counter = 0
def increment():
    counter += 1

Recursive (Haskell):

incrementCounter :: Int -> Int
incrementCounter n = n + 1  -- Pure function, no external dependencies

Key Insight:

3. Recursion Aligns with Mathematical Definitions

Many problems are inherently recursive in nature, such as traversing a tree or calculating factorials. Recursive functions in Haskell mirror these mathematical definitions, making them intuitive and reliable.

Example: Factorial

Mathematical Definition:

Recursive Implementation:

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

This direct alignment with mathematics ensures correctness and makes the code easier to understand. There’s no need to manage complex loop logic or mutable counters.

4. Recursion Simplifies Parallelism and Concurrency

By avoiding mutable state, recursion is inherently safe for concurrent and parallel programming. Recursive functions can be divided into independent subproblems that run in parallel without fear of race conditions.

Example: Parallel Sum of a List

Recursive (Haskell):

parallelSum :: [Int] -> Int
parallelSum [] = 0
parallelSum [x] = x
parallelSum xs = let (left, right) = splitAt (length xs `div` 2) xs
                 in parallelSum left + parallelSum right

Here:

5. Recursion Encourages Declarative Thinking

Unlike loops, which focus on how to iterate, recursion describes what the solution looks like. This shift in perspective leads to cleaner, more readable code.

Example: Maximum Element in a List

Imperative (Python):

def find_max(lst):
    max_value = lst[0]
    for x in lst[1:]:
        if x > max_value:
            max_value = x
    return max_value

Declarative (Haskell):

findMax :: [Int] -> Int
findMax [x] = x
findMax (x:xs) = max x (findMax xs)

Haskell expresses the solution directly:

6. Recursion Eliminates Entire Classes of Bugs

By avoiding mutable state and side effects, recursion prevents many common bugs found in imperative programming:

  1. Race Conditions: Recursion avoids shared state, eliminating concurrency issues.
  2. Off-by-One Errors: Recursive definitions naturally handle boundaries without needing to track counters or indices.
  3. Invalid States: Recursive functions can enforce valid states through their structure and base cases.

Example: Fibonacci Sequence

Imperative (Python):

def fibonacci(n):
    a, b = 0, 1
    for _ in range(n):
        a, b = b, a + b
    return a

Recursive (Haskell):

fibonacci :: Int -> Int
fibonacci 0 = 0
fibonacci 1 = 1
fibonacci n = fibonacci (n - 1) + fibonacci (n - 2)

Key Benefits:

5. How to Write Recursive Functions in Haskell

Recursion with Pattern Matching

Haskell’s pattern matching makes recursion intuitive. Here’s an example to find the factorial of a number:

factorial :: Int -> Int
factorial 0 = 1                  -- Base case: factorial of 0 is 1
factorial n = n * factorial (n-1) -- Recursive case: n * factorial of (n-1)

Recursion with Guards

Guards allow you to express conditions in a clear and readable way. Here’s the same factorial example using guards:

factorial :: Int -> Int
factorial n
    | n == 0    = 1
    | n > 0     = n * factorial (n - 1)
    | otherwise = error "Negative input not allowed"

Recursion with Higher-Kinded Types

Recursive functions can also work on recursive data structures, like trees:

data Tree a = Empty | Node a (Tree a) (Tree a)
    deriving (Show)

treeSum :: Tree Int -> Int
treeSum Empty = 0
treeSum (Node val left right) = val + treeSum left + treeSum right

6. Thinking Recursively Instead of Imperatively

When writing recursive functions, the key is to break the problem into smaller subproblems and focus on the following:

  1. What is the simplest possible input (the base case)?
  2. How can you reduce the problem into a smaller version of itself (the recursive case)?

Example: Reversing a List

Imperative (Python-style):

def reverse_list(lst):
    result = []
    for x in lst:
        result.insert(0, x)
    return result

Recursive (Haskell-style):

reverseList :: [a] -> [a]
reverseList [] = []                     -- Base case
reverseList (x:xs) = reverseList xs ++ [x] -- Recursive case

Real-world example of recursion (Directory Traversal)

import System.Directory
import System.FilePath

listFiles :: FilePath -> IO [FilePath]
listFiles dir = do
    contents <- listDirectory dir
    paths <- mapM (name -> do
                     let path = dir </> name
                     isDir <- doesDirectoryExist path
                     if isDir
                        then listFiles path
                        else return [path]) contents
    return (concat paths)

7. Recap & Exercises

Recap

Recursion is a core concept in functional programming, replacing traditional looping constructs with pure, declarative solutions. In this chapter, we explored:

  1. What is Recursion: Recursive functions solve problems by breaking them down into smaller subproblems, with a base case to ensure termination and a recursive case to reduce the problem.
  2. Why Functional Programming Avoids Loops: Loops rely on mutable state and side effects, which lead to bugs and make reasoning about code harder. Recursion eliminates these issues by ensuring immutability and purity.
  3. Why Recursion is Better: Recursive functions mirror mathematical definitions, eliminate shared state, and align with functional programming principles like composability and declarative thinking.
  4. Examples of Recursion in Haskell: We saw practical examples using pattern matching, guards, and recursive data structures, and explored how recursion simplifies parallelism and concurrency.

Exercises

Exercise 1: Flatten a Nested List

Write a recursive function to flatten a nested list into a single list. For example:

data NestedList a = Elem a | List [NestedList a]
    deriving (Show)

flatten :: NestedList a -> [a]
-- Your implementation here

Example:

flatten (List [Elem 1, List [Elem 2, Elem 3], Elem 4])
-- Output: [1, 2, 3, 4]

Exercise 2: Find the Depth of a Tree

Write a recursive function to calculate the depth of a binary tree. The depth of a tree is the number of edges on the longest path from the root to a leaf.

data Tree a = Empty | Node a (Tree a) (Tree a)
    deriving (Show)

treeDepth :: Tree a -> Int
-- Your implementation here

Example:

let tree = Node 1 (Node 2 Empty Empty) (Node 3 (Node 4 Empty Empty) Empty)
treeDepth tree
-- Output: 3

Exercise 3: Merge Two Sorted Lists

Write a recursive function to merge two sorted lists into a single sorted list.

mergeSorted :: Ord a => [a] -> [a] -> [a]
-- Your implementation here

Example:

mergeSorted [1, 3, 5] [2, 4, 6]
-- Output: [1, 2, 3, 4, 5, 6]

Exercise 4: Count Occurrences of an Element

Write a recursive function to count how many times a specific element appears in a list.

countOccurrences :: Eq a => a -> [a] -> Int
-- Your implementation here

Example:

countOccurrences 3 [1, 3, 5, 3, 3, 6]
-- Output: 3

Exercise 5: Generate Pascal’s Triangle

Write a recursive function to generate the nth row of Pascal’s Triangle. In Pascal’s Triangle, each number is the sum of the two numbers directly above it.

pascalsTriangle :: Int -> [Int]
-- Your implementation here

Example:

pascalsTriangle 5
-- Output: [1, 5, 10, 10, 5, 1]

Exercise 6: Implement Quicksort

Using recursion, implement the quicksort algorithm to sort a list of elements.

quicksort :: Ord a => [a] -> [a]
-- Your implementation here

Example:

quicksort [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]
-- Output: [1, 1, 2, 3, 3, 4, 5, 5, 5, 6, 9]

Exercise 7: Detect Palindromes

Write a recursive function to check if a list is a palindrome. A list is a palindrome if it reads the same backward as forward.

isPalindrome :: Eq a => [a] -> Bool
-- Your implementation here

Example:

isPalindrome [1, 2, 3, 2, 1]
-- Output: True

isPalindrome [1, 2, 3]
-- Output: False

Exercise 8: Generate All Subsets of a List

Write a recursive function to generate all subsets (the power set) of a list.

subsets :: [a] -> [[a]]
-- Your implementation here

Example:

subsets [1, 2]
-- Output: [[], [1], [2], [1,2]]

Previous Chapter