Until now most programs we have written have been pure, meaning that they never changed state. Whenever a function does something other than just return a value, it is known as a side effect. While pure functions have some interesting features like composability, the fact of the matter is that programs aren’t interesting unless they do something: save data to disk, print values to the screen, issue network traffic, and so on. These side effects are where things actually get done.
This chapter will cover how to change program state and alter control flow, which is known as imperative programming. This style of programming is considered to be more error-prone than functional programming because it opens up the opportunity for getting things wrong. The more detailed the instructions you give the computer to branch, or write certain values into certain memory locations, the more likely the programmer will make a mistake. When you programmed in the functional style, all of your data was immutable, so you couldn’t assign a wrong value by accident. However, if used judiciously, imperative programming can be a great boon for F# development.
Some potential benefits for imperative programming are:
Ease of maintenance through code clarity
Interoperability with existing code
Imperative programming is a style in which the program performs tasks by altering data in memory. This typically leads to patterns where programs are written as a series of statements or commands. Example 4-1 shows a hypothetical program for using a killer robot to take over the earth. The functions don’t return values, but do impact some part of the system, such as updating an internal data structure.
Example 4-1. Taking over the earth with imperative programming
let robot = new GiantKillerRobot() robot.Initialize() robot.EyeLaserIntensity <- Intensity.Kill robot.Target <- [| Animals; Humans; Superheroes |] // Sequence for taking over the Earth let earth = Planets.Earth while robot.Active && earth.ContainsLife do if robot.CurrentTarget.IsAlive then robot.FireEyeLaserAt(robot.CurrentTarget) else robot.AquireNextTarget()
Although the code snippet makes taking over the earth look fairly
easy, you don’t see all the hard work going on behind the scenes. The
Initialize function may require powering
up a nuclear reactor; and if
is called twice in a row, the reactor might explode. If
Initialize were written in a purely functional
style, its output would depend only on the function’s input. Instead, what
happens during the function call to
Initialize depends on the current state of
While this chapter won’t teach you how to program planet-conquering
robots, it will detail how to write F# programs that can change the
environment they run in. You will learn how to declare
variables, whose values you can change during the
course of your program. You’ll learn how to use mutable collection types,
which offer an easier-to-use alternative to F#’s
list type. Finally, you will learn about control
flow and exceptions, allowing you to alter the order in which code
Before you can start making changes to memory, you first need to understand how memory works in .NET. Values in .NET applications are stored in one of two locations: on the stack or in the heap. (Experienced programmers may already be familiar with these concepts.) The stack is a fixed amount of memory for each process where local variables are stored. Local variables are temporary values used only for the duration of the function, like a loop counter. The stack is relatively limited in space, while the heap (also called RAM) may contain several gigabytes of data. .NET uses both the stack and the heap to take advantage of the cheap memory allocations on the stack when possible, and store data on the heap when more memory is required.
The area in memory where a value is stored affects how you can use it.
Value types have a fixed size of bytes on the stack.
float are both examples of value types,
because their size is constant. Reference types, on the other hand, only
store a pointer on the stack, which is the
address of some blob of memory on the heap. So while the pointer has a
fixed size—typically four or eight bytes—the blob of memory it points to
can be much larger.
string are both examples of reference
This is visualized in Figure 4-1. The integer
5 exists on the stack, and has no counterpart
on the heap. A string, however, exists on the stack as a memory address,
pointing to some sequence of characters on the heap.
So far, each value we have declared in F# has been initialized as soon as it has been created, because in the functional style of programming values cannot be changed once declared. In imperative programming, however, there is no need to fully initialize values because you can update them later. This means there is a notion of a default value for both value and reference types, that is, the value something has before it has been initialized.
A type function is a special type of function that takes no arguments other than a generic type parameter. There are several helpful type functions that we will explore in forthcoming chapters:
Gets the default value for
Returns the underlying stack size of
For value types, their default value is simply a zero-bit pattern.
Because the size of a value type is known once it is created, its size
in bytes is allocated on the stack, with each byte being given the value
The default value for reference types is a little more
complicated. Before reference types are initialized they first point to
a special address called
null. This is used to
indicate an uninitialized reference type.
In F#, you can use the
keyword to check if a reference type is equal to
null. The following code defines a function to
check if its input is
null or not,
and then calls it with an initialized and an uninitialized string
> let isNull = function
null-> true | _ -> false;; val isNull : obj -> bool > isNull "a string";; val it : bool = false > isNull (
null: string);; val it : bool = true
However, reference types defined in F# do not have
null as a proper value, meaning that they
cannot be assigned to be
> type Thing = Plant | Animal | Mineral;; type Thing = | Plant | Animal | Mineral > // ERROR: Thing cannot be null let testThing thing = match thing with | Plant -> "Plant" | Animal -> "Animal" | Mineral -> "Mineral" |
null-> "(null)";; | null -> "(null)";; ------^^^^^
stdin(9,7): error FS0043: The type 'Thing' does not have 'null' as a proper value.
This seems like a strange restriction, but it eliminates the need
null checking. (If you
call a method on an uninitialized reference type, your program will
so defensively checking all function parameters for
null is typical.) If you do need to represent
an uninitialized state in F#, consider using the
Option type instead of a reference type with
null, where the value
None represents an uninitialized state and
Some('a) represents an initialized
You can attribute some F# types to accept
null as a proper value to ease
interoperation with other .NET languages; see Appendix B for more information.
It is possible that two reference types point to the same memory address on the heap. This is known as aliasing. When this happens, modifying one value will silently modify the other because they both point to the same memory address. This situation can lead to bugs if you aren’t careful.
Example 4-2 creates one instance
array (which we’ll cover
shortly), but it has two values that point to the same instance.
x also modifies
y and vice versa.
Example 4-2. Aliasing reference types
> // Value x points to an array, while y points // to the same value that x does let x = [| 0 |] let y = x;; val x : int array = [|0|] val y : int array = [|0|] >
// If you modify the value of x...x. <- 3;; val it : unit = () >
// ... x will change...x;; val it : int array = [|3|] >
// ... but so will y...y;; val it : int array = [|3|]