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.
The next post in this series will show a way to change partial functions into total ones, continuing on from a previous post by Prash.