You are previewing Programming F#.

Programming F#

Cover of Programming F# by Chris Smith Published by O'Reilly Media, Inc.
O'Reilly logo

Chapter 4. Imperative Programming

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:

  • Improved performance

  • 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 Initialize 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 memory.

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 executes.

Understanding Memory in .NET

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 Versus Reference Types

Values stored on the stack are known as value types, and values stored on the heap are known as reference types.

Value types have a fixed size of bytes on the stack. int and 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. list and string are both examples of reference types.

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.

Stack types versus reference types

Figure 4-1. Stack types versus reference types

Default Values

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.

To get the default value of a type, use the type function Unchecked.defaultof<'a>. This will return the default value for the type specified.

Note

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:

Unchecked.defaultof<'a>

Gets the default value for 'a

typeof<'a>

Returns the System.Type object describing 'a

sizeof<'a>

Returns the underlying stack size of 'a

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 0b00000000.

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 null 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 value:

> 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 null:

> 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 for excessive null checking. (If you call a method on an uninitialized reference type, your program will throw a NullReferenceException, 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 value null, where the value None represents an uninitialized state and Some('a) represents an initialized state.

Note

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.

Reference Type Aliasing

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 of an array (which we’ll cover shortly), but it has two values that point to the same instance. Modifying value 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.[0] <- 3;;
val it : unit = ()
> // ... x will change...
x;;
val it : int array = [|3|]
> // ... but so will y...
y;;
val it : int array = [|3|]

The best content for your career. Discover unlimited learning on demand for around $1/day.