Immutability is at the core of functional programming, and it fundamentally changes the way we think about writing code and how programs execute. In Haskell, this isn’t just a recommendation—it’s enforced. All data in Haskell is immutable by default, meaning once a value is assigned, it cannot be changed.
In contrast, object-oriented programming (OOP) tends to favor mutable state — where data can be modified directly. This seems convenient at first but introduces many hidden dangers. Mutable state can cause unpredictable behavior and bugs because data can change in ways you don’t expect.
One of the cornerstones of immutability in Haskell is the concept of currying. Every function in Haskell is curried, meaning it can be partially applied. You give it some arguments, and it returns a new function that takes the remaining arguments. This naturally leads to pure evaluation, where each step of a function is evaluated in isolation, without changing any state. This makes it easy to reason about your code since you know exactly what each part of the program does.
Example:
add :: Int -> Int -> Int
add x y = x + y
-- Partial application of the curried function
increment = add 1
result = increment 5 -- Result is 6
Currying promotes immutability because the partially applied function is itself an immutable value. Once a function is curried, you can reuse it with different inputs without changing its behavior.
Think of a bucket that represents the state of your program. In a world of mutable state, you can throw things into the bucket, mix them up, and hand the bucket to someone else. But the problem is, the person you hand the bucket to doesn’t know what rules you’ve been following for mixing things. They have no idea what the original contents were or how you manipulated them.
On the other hand, in a world of immutable state, the bucket is passed around for people to observe, but no one is allowed to change its contents. Every time you need to modify the bucket’s contents, you create a new bucket that reflects the new state, while the original bucket stays the same. This way, everyone knows what’s in the bucket at every point in time, and there’s no ambiguity.
This metaphor captures the essence of immutability: you don’t change the state, you create a new state. This makes the flow of information in your program predictable, safer, and easier to reason about.
Immutability might seem like a restriction at first, but ironically, it frees us from many of the problems that plague mutable systems. Here’s why immutability is such a game-changer:
In object-oriented languages, mutability is often the default behavior. Consider this Python example:
x = 5
x += 5 # Mutates the variable 'x', changing its value to 10
Here, the variable x is being mutated, which is a simple example, but when used in larger systems, this leads to a tangled web of state changes. In complex applications, you need to introduce mechanisms like:
In contrast, functional programming avoids these issues altogether. Since data is immutable and functions are pure, you don’t need mocks or dependency injection to isolate behavior. You can test each function in isolation without worrying about hidden state or side effects.
Let’s look at how Haskell handles immutability and avoids the problems of mutable state.
In Haskell, once you assign a value to a variable, it can never change. This ensures that your data is always consistent, and you don’t have to worry about it being changed unexpectedly.
-- Example of immutable variables
x :: Int
x = 5
-- Attempting to change 'x' will result in a compile-time error
-- x = x + 5 -- This is illegal in Haskell
In this code, x is assigned the value 5, and you cannot change x later. If you need a new value, you would create a new variable:
y :: Int
y = x + 5
Now, y contains the new value, but x remains untouched.
In Haskell, state changes are handled using pure functions, which means that a function’s output depends only on its input, and it doesn’t affect any external state. If you want to represent a change in state, you return a new version of the state, leaving the old state intact.
-- Pure function example
addFive :: Int -> Int
addFive x = x + 5
-- The input 'x' is unchanged; a new value is returned
result = addFive 10 -- result is 15
This pure function guarantees that the input x remains unchanged and a new value is returned.
Haskell uses monads (such as the State monad) to manage state in a controlled and predictable way. Monads allow you to work with state changes while preserving immutability, ensuring that every state transition is explicit.
import Control.Monad.State
-- Define a stateful computation
incrementState :: State Int Int
incrementState = do
n <- get
put (n + 1)
return n
-- Running the stateful computation
main = print $ runState incrementState 10
In this example:
Note: We will dive into Monads in chapter 6. For now, just understand that they allow us to model some context around the value we care about.
Note: We will dive into State management in chapter 8. For now, just understand that state management is done in a pure and deterministic manner.
A pure function is a function that:
In a system that embraces immutability, pure functions are everywhere. They take inputs and produce outputs without altering anything in between. This creates code that is easy to reason about, predictable, and free from the hidden side effects that plague mutable systems.
Example:
-- Pure function: No side effects, output depends only on the input
multiplyByTwo :: Int -> Int
multiplyByTwo x = x * 2
result = multiplyByTwo 10 -- result is 20
Here, multiplyByTwo is a pure function. It doesn’t rely on or modify any external state. It simply takes an integer, multiplies it by two, and returns the result. Every time you call multiplyByTwo 10, the result will always be 20. There’s no hidden state that could change the outcome.
Contrast this with an impure function from an object-oriented perspective:
int counter = 0;
public int incrementCounter() {
return ++counter;
}
This incrementCounter function in Java is impure because it relies on the external mutable state (counter). Each time you call it, the result will change based on the current value of counter. This makes it much harder to predict how the program behaves at any given time, especially as the system grows in complexity.
One of the properties that follows directly from pure functions and immutability is referential transparency. A function or expression is referentially transparent if it can be replaced by its value without changing the behavior of the program.
Example of Referential Transparency:
-- Pure function
square :: Int -> Int
square x = x * x
-- The following expressions are referentially transparent
result1 = square 4 -- This expression can be replaced with its result: 16
result2 = 16 -- Both are equivalent
In a language that guarantees immutability like Haskell, referential transparency is always maintained. This is not true in mutable languages, where state can change unexpectedly, and replacing an expression with its value could yield different results based on the current state.
In Haskell, all data structures are immutable by default. This means that once you create a data structure, it cannot be changed. If you want to “modify” it, you actually create a new data structure based on the old one.
One of the most fundamental data structures in Haskell is the list, which is immutable. If you want to add or remove elements from a list, you don’t modify the list in place. Instead, you create a new list.
Example:
-- Define a list
myList :: [Int]
myList = [1, 2, 3]
-- Add an element to the front of the list (creates a new list)
newList = 0 : myList -- newList is [0, 1, 2, 3]
In this example, the : operator creates a new list by prepending the element 0 to the front of the existing list myList. The original list [1, 2, 3]
remains unchanged.
Haskell also supports more complex data structures like records, which allow you to define immutable types with named fields.
Example:
-- Define a record for a User
data User = User { userId :: Int, userName :: String }
-- Create a User
user :: User
user = User { userId = 1, userName = "Alice" }
-- Create a new User with a different name
updatedUser = user { userName = "Bob" }
In this example:
At first glance, immutability might seem inefficient—if you’re creating new data structures all the time, won’t that slow things down? In reality, Haskell uses techniques like persistent data structures and lazy evaluation to optimize performance.
Immutability has a profound effect on concurrency. In a mutable system, concurrency can lead to serious problems like race conditions and deadlocks when multiple threads try to access and modify the same state at the same time. This makes concurrent programming difficult and error-prone.
With immutability, these problems simply disappear. Since state cannot change, you don’t have to worry about threads interfering with each other. You can safely share data between threads without needing locks or other synchronization mechanisms.
In Haskell, you can safely share immutable data across multiple threads without worrying about one thread changing the state and causing unexpected behavior in another thread.
Example using multiple threads:
import Control.Concurrent
-- Define an immutable value
sharedValue :: Int
sharedValue = 100
-- Define a function to simulate work on a thread
printSharedValue :: IO ()
printSharedValue = do
putStrLn $ "Shared value is: " ++ show sharedValue
main = do
-- Create two threads that both access the immutable shared value
_ <- forkIO printSharedValue
_ <- forkIO printSharedValue
-- Wait for threads to finish
threadDelay 1000000
In this example, two threads both access the immutable value sharedValue. Since the value is immutable, we can be sure that no matter how many threads access it, the value will remain unchanged and there will be no race conditions or synchronization issues.
Immutability fundamentally changes the way we write programs by removing the complexities and risks associated with mutable state. By enforcing immutability, Haskell guarantees that:
Immutability fundamentally changes the way we write programs by removing the complexities and risks associated with mutable state. By enforcing immutability, Haskell guarantees that:
In this chapter, we’ve explored the concept of immutability in functional programming and its significant impact on how we write and reason about code. Lets recap the main points:
x += 5
don’t make sense when modeling real-world behavior, as things change by creating new states, not modifying old ones.
By embracing immutability, Haskell makes it easier to write code that is predictable, scalable, and free from many of the pitfalls associated with mutable state. This drastically reduces bugs and complexity in concurrent and parallel systems.
The following exercises will help you practice using immutability in Haskell and reinforce the concepts discussed in this chapter.
Objective: Write a few pure functions that take inputs and return outputs without modifying any external state.
Write a pure function square
that takes an integer and returns its square.
square :: Int -> Int
-- Your implementation here
Write a pure function calculateDiscount
that takes a price and a discount percentage and returns the price after applying the discount. The discount should be a Float
.
calculateDiscount :: Float -> Float -> Float
-- Your implementation here
Write a pure function doubleList
that takes a list of integers and returns a new list with all integers doubled.
doubleList :: [Int] -> [Int]
-- Your implementation here
Hint: Remember, these functions should not modify the input data. They should return new values based on the inputs.
Objective: Practice working with immutable data structures in Haskell by creating lists and modifying them without mutating the original data.
Define a list numbers :: [Int]
that contains the integers 1 through 5.
numbers :: [Int]
-- Your implementation here
Write a function addElement
that takes an integer and a list of integers and returns a new list with the integer added to the front of the list.
addElement :: Int -> [Int] -> [Int]
-- Your implementation here
Use the addElement
function to add the number 6 to the numbers
list, and then print both the original and the new list to verify that numbers
has not been modified.
main :: IO ()
main = do
let newNumbers = addElement 6 numbers
print numbers -- Should still be [1, 2, 3, 4, 5]
print newNumbers -- Should be [6, 1, 2, 3, 4, 5]
Hint: Use the :
operator to add an element to the front of the list.
Objective: Practice managing state using immutable structures by implementing a simple counter.
Write a function incrementCounter
that takes an integer (the current counter value) and returns the new counter value after incrementing it by 1.
incrementCounter :: Int -> Int
-- Your implementation here
Write a function updateCounter
that simulates a sequence of counter updates. It should take an initial counter value and a list of updates (e.g., [+1, +1, -1]
), and return the final counter value.
updateCounter :: Int -> [Int] -> Int
-- Your implementation here
Simulate counter updates and print the results. For example, starting with a counter of 5 and applying updates [1, 1, -1]
should return 6.
main :: IO ()
main = do
let result = updateCounter 5 [1, 1, -1]
print result -- Should be 6
Objective: Write a pure function that simulates state changes over time without using any mutable state.
Imagine a simple game where a character moves along a 2D grid. The state of the game is represented by the character’s coordinates ((x, y)
).
Write a function moveCharacter
that takes the current position (x, y)
and a direction (e.g., Left
, Right
, Up
, Down
), and returns the new position.
data Direction = Left | Right | Up | Down
moveCharacter :: (Int, Int) -> Direction -> (Int, Int)
-- Your implementation here
Simulate a sequence of moves using a list of directions and print the final position of the character. For example, starting at (0, 0)
and moving [Right, Up, Up, Left]
should return (-1, 2)
.
main :: IO ()
main = do
let finalPosition = foldl moveCharacter (0, 0) [Right, Up, Up, Left]
print finalPosition -- Should print (-1, 2)
Hint: The foldl
function can be used to apply moveCharacter
to each direction in the list, updating the position step by step.