United Kingdom: +44 (0)208 088 8978

Immutability and expressions

In the third post in our functional programming fundamentals series, Matt discusses immutability and expressions.

What it means for a function to be pure was explored in the first post in this series. The benefits of pure functions were discussed in the second post. This post—the third in the series—explores the concept of immutability, its relation to pure functions and why expressions are necessary when working with immutable data.

Immutability

Data is mutable if its value can be changed (or "mutated") over time. Data is immutable if its value cannot be changed once it's been created. The following example demonstrates the difference.

let mutable variable = 1
let addVariable x = x + variable

addVariable 3 // 4
variable <- 2 // Change the value of variable to 2.
addVariable 3 // 5

let constVal = 3
let addConst x = x + constVal // This definition is the same as x + 3.

addConst 3 // 6
let constVal = 4 // Bind the value 4 to the symbol constVal.
addConst 3 // 6

variable is mutable (as the mutable keyword signifies). As a result, the addVariable function which relies on it changes its behaviour when it is assigned a new value using the <- operator.

In contrast, constVal is immutable. When used in the definition of addConst, it's just the same as using its value directly. Because of this, binding the symbol constVal to a new value doesn't change the behaviour of addConst. (For curious readers: trying to use the <- operator with constVal gives a compiler error).

Mutable variables are more complex than immutable values: the value that a mutable variable holds (its "state") can change over time, whereas an immutable value is just a value.

Immutability and pure functions

Mutating a non-local variable or an argument within a function is a side effect of that function. This means that a function that mutates variables that exist outside its scope is impure. Similarly, relying on a non-local mutable variable or a mutable parameter makes a function non-deterministic, hence impure.

As discussed in the last post in this series, pure functions have plenty of benefits. As a result, functional programmers tend to avoid mutation, hence immutability is a sensible default for many F# data types. Even better, if a function only relies on immutable data, the compiler can guarantee that the data isn't mutated, preventing a few ways for a function to be (accidentally or deliberately) impure. In practice, this means that in many F# programs the only source of impurity is I/O.

The need for expressions

When working with mutable data, it's quite common to set an initial value in multiple steps. For example:

let setUpAccount newCustomer =
    let mutable freeCredit = 0
    if isSpecial newCustomer then freeCredit <- 100
    ...

As already discussed, working with immutable data is beneficial because it lowers the number of potential sources of impurity, so a natural question arises: how can a programmer do the equivalent of the above code snippet when working with immutable data?

Achieving this relies on expressions.

Expressions

An expression is some code that can be evaluated to determine its value. This is in contrast to a statement, which is an instruction for something to be done.

In F#, common language constructs are expressions. The if...then...else construct is an expression in F#: when used in code, the whole construct can be evaluated and its value determined. This allows the value bound to a symbol to depend on a condition:

let setUpAccount newCustomer =
    let freeCredit = if isSpecial newCustomer then 100 else 0
    ...

In this example, if isSpecial newCustomer then 100 else 0 is an expression; its value is 100 if the sub-expression isSpecial newCustomer evauluates to true; otherwise, its value is 0.

Aside from things like namespaces/modules/import declarations, type definitions and the let keyword, all F# code is made up of expressions. This makes it easier to get by without using mutable data.

Summary

Mutability is a potential source of impurity, so functional programmers prefer to work with immutable data. To make that possible, expressions capturing branching logic are needed. F# provides these, like conditional expressions or match expressions. As a result, F# can make most of its commonly-used data types, such as records and lists, immutable.