Chapter 4. Imperative Programming

Until now, most of the programs we’ve 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. Although pure functions have some interesting features (e.g., 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 covers 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 given to the computer to branch, or write certain values into memory, 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 function style, its output would only depend on the function’s input. Instead, what happens during the function call to Initialize depends on the current state of memory.

Although this chapter won’t teach you how to program planet-conquering robots, it does detail how to write F# programs that can change the environment they run in. You will learn how to declare variables, the values of which 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, whereas 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 storing 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.

Values 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, 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.

Value types versus reference types
Figure 4-1. Value types versus reference types

Default Values

So far, each value you 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, you can 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 you 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 in other .NET languages 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).

Also, that appendix covers the System.Nullable<T> type, which is used in other .NET languages as a primitive form of F#’s option type.

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 (covered shortly), but 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 memory address that x does
let x = [| 0 |]
let y = x;;

val x : int [] = [|0|]
val y : int [] = [|0|]

> // If you modify the value of x...
x.[0] <- 3;;
val it : unit = ()
> // ... x will change...
x;;
val it : int [] = [|3|]
> // ... but so will y...
y;;
val it : int [] = [|3|]

Changing Values

Now that you understand the basics of where and how data is stored in .NET, you can look at how to change that data. Mutable variables are those that you can change, and can be declared using the mutable keyword. To change the contents of a mutable value, use the left arrow operator, <-:

> let mutable message = "World";;

val mutable message : string = "World"

> printfn "Hello, %s" message;;
Hello, World
val it : unit = ()

> message <- "Universe";;
val it : unit = ()
> printfn "Hello, %s" message;;
Hello, Universe
val it : unit = ()

There are several limitations on mutable values, all stemming from security-related CLR restrictions. This prevents you from writing some code using mutable values. Example 4-3 tries to define an inner-function incrementX, which captures a mutable value x in its closure (meaning it can access x, even though it wasn’t passed in as a parameter). This leads to an error from the F# compiler, because mutable values can only be used in the same function they are defined in.

Example 4-3. Errors using mutable values in closures
> // ERROR: Cannot use mutable values except in the function they are defined
let invalidUseOfMutable() =
    let mutable x = 0

    let incrementX() = x <- x + 1
    incrementX()

    x;;

    let incrementX() = x <- x + 1
-----------------------^^^^^^^^^^^

stdin(16,24): error FS0407: The mutable variable 'x' is used in an invalid way.
Mutable variables may not be captured by closures. Consider eliminating this use
 of mutation or using a heap-allocated mutable reference cell via 'ref' and '!'.

The two restrictions related to mutable values are as follows:

  • Mutable values cannot be returned from functions (a copy is made instead)

  • Mutable values cannot be captured in inner-functions (closures)

If you ever run into one of these issues, the simple work around is to store the mutable data on the heap using a ref cell.

Reference Cells

The ref type, sometimes referred to as a ref cell, allows you to store mutable data on the heap, enabling you to bypass limitations with mutable values that are stored on the stack. To retrieve the value of a ref cell, use the ! symbolic operator, and to set the value, use the := operator.

ref is not only the name of a type, but also the name of a function that produces ref values, which has the signature:

val ref: 'a -> 'a ref

The ref function takes a value and returns a copy of it wrapped in a ref cell. Example 4-4 shows passing a list of planets to the ref function and then later altering the contents of the returned ref cell.

Example 4-4. Using ref cells to mutate data
let planets =
    ref [
        "Mercury";  "Venus";     "Earth";
        "Mars";     "Jupiter";   "Saturn";
        "Uranus";   "Neptune";   "Pluto"
    ]

// Oops! Sorry Pluto...

// Filter all planets not equal to "Pluto"
// Get the value of the planets ref cell using (!),
// then assign the new value using (:=)
planets := !planets |> List.filter (fun p -> p <> "Pluto")

Warning

C# programmers should take care when using ref types and Boolean values. !x is simply the value of x, not the Boolean not function applied to x:

> let x = ref true;;

val x : bool ref

> !x;;

val it : bool = true

The F# library has two functions, decr and incr, to simplify incrementing and decrementing int ref types:

> let x = ref 0;;

val x : int ref = {contents = 0;}

> incr x;;
val it : unit = ()
> x;;
val it : int ref = {contents = 1;}
> decr x;;
val it : unit = ()
> x;;
val it : int ref = {contents = 0;}

Mutable Records

Mutability can be applied to more than just single values; record fields can be marked as mutable as well. This allows you to use records with the imperative style. To make a record field mutable, simply prefix the field name with the mutable keyword.

The following example creates a record with a mutable field Miles, which can be modified as if it were a mutable variable. Now you can update record fields without being forced to clone the entire record:

> // Mutable record types
open System

type MutableCar = { Make : string; Model : string; mutable Miles : int }

let driveForASeason car =
    let rng = new Random()
    car.Miles <- car.Miles + rng.Next() % 10000;;

type MutableCar =
  {Make: string;
   Model: string;
   mutable Miles: int;}
val driveForASeason : MutableCar -> unit

> // Mutate record fields
let kitt = { Make = "Pontiac"; Model = "Trans Am"; Miles = 0 }

driveForASeason kitt
driveForASeason kitt
driveForASeason kitt
driveForASeason kitt;;

val kitt : MutableCar = {Make = "Pontiac";
                         Model = "Trans Am";
                         Miles = 4660;}

As Uncle Ben once said, “With great power comes great responsibility.” And the ability to mutate values is no different. Fortunately, in F#, it is difficult to get into too much trouble with incorrect mutations because of the type system’s ability to enforce correctness.

Units of Measure

There are several universal truths in this world: the acceleration of gravity is 9.8 meters per second squared, water will boil at over 100 degrees Celsius (at one atmosphere of pressure), and any programmer, no matter how talented or careful, will have bugs related to units of measure.

If you are ever writing code that deals with real-world units, you will invariably get it wrong. For example, you might pass in seconds when the function takes minutes, or mistake acceleration for velocity. The result of these sorts of bugs in software has ranged from minor annoyances to loss of life.

The problem is that if you represent a value with just a floating-point number, you have no additional information about what that number means. If I give you a float with value 9.8, you have no idea if it is in miles, meters per second, hours, or even megabytes.

A powerful language feature for combating these dimensional analysis issues is units of measure. Units of measure allow you to pass along unit information with a floating-point value—float, float32, decimal—or signed integer types in order to prevent an entire class of software defects. Consider Example 4-5, which describes a temperature. Notice how the parameter temp is encoded to only take fahrenheit values. We will cover exactly what float<_> means later in this section.

Example 4-5. Converting Fahrenheit to Celsius with units of measure
[<Measure>]
type fahrenheit

let printTemperature (temp : float<fahrenheit>) =

    if   temp < 32.0<_>  then
        printfn "Below Freezing!"
    elif temp < 65.0<_>  then
        printfn "Cold"
    elif temp < 75.0<_>  then
        printfn "Just right!"
    elif temp < 100.0<_> then
        printfn "Hot!"
    else
        printfn "Scorching!"

Because the function only accepts fahrenheit values, it will fail to work with any floating-point values encoded with a different unit of measure. Calling the function with an invalid unit of measure will result in a compile-time error (and prevent potentially disastrous results at runtime):

> let seattle = 59.0<fahrenheit>;;

val seattle : float<fahrenheit> = 59.0

> printTemperature seattle;;
Cold
val it : unit = ()
> // ERROR: Different units
[<Measure>]
type celsius

let cambridge = 18.0<celsius>;;

[<Measure>]
type celsius
val cambridge : float<celsius> = 18.0

> printTemperature cambridge;;

  printTemperature cambridge;;
  -----------------^^^^^^^^^

stdin(18,18): error FS0001: Type mismatch. Expecting a
    float<fahrenheit>
but given a
    float<celsius>.
The unit of measure 'fahrenheit' does not match the unit of measure 'celsius'

Units of measure also can be compounded by multiplication or division. So if you divide a meter unit of measure by another such as second, the result will be encoded as float<meter/second>:

> // Define a measure for meters
[<Measure>]
type m;;

[<Measure>]
type m

> // Multiplication, goes to meters squared
1.0<m> * 1.0<m>;;

val it : float<m ^ 2> = 1.0
> // Division, drops unit entirely
1.0<m> / 1.0<m>;;

val it : float = 1.0
> // Repeated division, results in 1 / meters
1.0<m> / 1.0<m> / 1.0<m>;;

val it : float</m> = 1.0

Defining Units of Measure

To define a unit of measure, simply add the [<Measure>] attribute on top of a type declaration. A unit of measure type can only contain static methods and properties, and typically they are defined as opaque types, meaning they have no methods or properties at all. Unit of measure types can also be abbreviated to be relative to other units of measure. In Example 4-6, a new unit of measure, s, for seconds, is defined as well as a relative unit of measure Hz, for hertz, which stands for cycles per second. Because the units are relative to one another, two values with the same semantic meaning are considered equal.

Example 4-6. Defining new units of measure
> // Define seconds and hertz
[<Measure>]
type s

[<Measure>]
type Hz = s ^ −1;;

[<Measure>]
type s
[<Measure>]
type Hz =/s

> // If Hz was not convertible to s, this
// would result in a compile error.
3.0<s ^ −1> = 3.0<Hz>;;
val it : bool = true

Sometimes it can be quite useful to add functions for conversions between units of measures to the measure type itself. The following snippet defines units of measure for Fahrenheit and Celsius like before, except with the addition of static methods to do conversion between the two. Note the use of the and keyword—it is required so that the type far can reference type cel as part of its declaration:

> // Adding methods to units of measure
[<Measure>]
type far =
    static member ConvertToCel(x : float<far>) =
        (5.0<cel> / 9.0<far>) * (x - 32.0<far>)

and [<Measure>] cel =
    static member ConvertToFar(x : float<cel>) =
        (9.0<far> / 5.0<cel> * x) + 32.0<far>;;

[<Measure>]
type far =
  class
    static member ConvertToCel : x:float<far> -> float<cel>
  end
[<Measure>]
and cel =
  class
    static member ConvertToFar : x:float<cel> -> float<far>
  end

> far.ConvertToCel(100.0<far>);;
val it : float<cel> = 37.77777778

Although defining units of measure is an easy task, all of the common elements of the international system of units have been defined in the F# library. They are separated into two namespaces in case you wish to refer to units by their abbreviated form or by name:

// UnitSymbols contains the abbreviated versions of SI units.
open Microsoft.FSharp.Data.UnitSystems.SI.UnitSymbols

// In candela, the SI unit of luminous intensity.
let flaslightIntensity = 80.0<cd>

// The UnitNames contains the full-names of SI units.
open Microsoft.FSharp.Data.UnitSystems.SI.UnitNames

// This might be worth a few dollars, euros, yen, etc.
let world'sLargestGoldNugget = 280.0<kilogram>

Converting Between Units of Measure

Not every function takes a measured value as a parameter. To convert between a measured parameter and the base type, simply pass the value to the appropriate conversion function. Example 4-7 shows how to drop units of measure when calling the sin and cos trigonometric functions, because they do not accept values marked with a unit of measure.

Example 4-7. Converting units of measure
> // Radians
[<Measure>]
type rads;;

[<Measure>]
type rads

> let halfPI = System.Math.PI * 0.5<rads>;;

val halfPI : float<rads>

> // ERROR: Pass a float<_> to a function accepting a float
sin halfPI;;

  sin halfPI;;
  ----^^^^^^^

stdin(7,5): error FS0001: The type 'float<rads>' does not match type 'float'.
> // Drop the units from value halfPi, to convert float<_> to float
sin (float halfPI);;
val it : float = 1.0

Generic Units of Measure

Relying on custom conversion can be a pain, especially if you want to create a generic function. Fortunately, you can allow the F# type system to infer a unit of measure type. If you leave the unit of measure type off and use float<_> instead, the F# type inference system will define the value as having a generic unit of measure:

> let squareMeter (x : float<m>) = x * x;;

val squareMeter : float<m> -> float<m ^ 2>

> let genericSquare (x : float<_>) = x * x;;

val genericSquare : float<'u> -> float<'u ^ 2>

> genericSquare 1.0<m/s>;;
val it : float<m ^ 2/s ^ 2> = 1.0
> genericSquare 9.0;;
val it : float = 81.0

If you want to create a type that is generic with regard to a unit of measure, add the [<Measure>] attribute to a generic type parameter. That generic type parameter will allow you to refer to the unit of measure, but it cannot be anything else. In fact, the compiled form of the type will not expose the generic type parameter at all.

Example 4-8 shows defining a point type that preserves a unit of measure.

Example 4-8. Creating a type that is generic with respect to a unit of measure
// Represents a point respecting the unit of measure
type Point< [<Measure>] 'u >(x : float<'u>, y : float<'u>) =

    member this.X = x
    member this.Y = y

    member this.UnitlessX = float x
    member this.UnitlessY = float y

    member this.Length =
        let sqr x = x * x
        sqrt <| sqr this.X + sqr this.Y

    override this.ToString() =
        sprintf
            "{%f, %f}"
            this.UnitlessX
            this.UnitlessY

When executed in an FSI session, Example 4-8 looks like the following. Notice how the unit of measure is persisted through the Length property—taking the square root of the sum of two squares:

> let p = new Point<m>(10.0<m>, 10.0<m>);;

val p : Point<m>

> p.Length;;
val it : float<m> = 14.14213562

Warning

Units of measure are a feature specific to the F# language and not to the Common Language Runtime. As a result, custom types you create in F# will not have their units of measure types exposed across assembly boundaries. So types that are float<'a> will only be exported as float when referenced from C#.

Units of measure are not only suited for real-world values. They can also be helpful when dealing with abstract units as well, such as clicks, pixels, game tiles, and so on.

Arrays

Until now, when you’ve needed to join multiple pieces of data together, you’ve used lists. Lists are extremely efficient at adding and removing elements from the beginning of a list, but they aren’t ideal for every situation. For example, random access of list elements is very slow. In addition, if you needed to change the last element of a list, you would need to clone the entire list. (The performance characteristics of lists are covered more in-depth in Chapter 7.)

When you know ahead of time how many items you will need in your collection and would like to be able to update any given item, arrays are the ideal type to use.

Arrays in .NET are a contiguous block of memory containing zero or more elements, each of which can be modified individually. (This is unlike lists, which are immutable.)

Arrays can be constructed using array comprehensions, which are identical to list comprehensions (discussed in Chapter 2), or manually via a list of values separated by semicolons and enclosed between [| |]:

> // Using the array comprehension syntax
let perfectSquares = [| for i in 1 .. 7 -> i * i |];;

val perfectSquares : int [] = [|1; 4; 9; 16; 25; 36; 49|]

> // Manually declared
let perfectSquares2 = [| 1; 4; 9; 16; 25; 36; 49; 64; 81 |];;

val perfectSquares2 : int []

Indexing an Array

To retrieve an element from the array, you can use an indexer, .[], which is a zero-based index into the array:

> // Indexing an array
printfn
    "The first three perfect squares are %d, %d, and %d"
    perfectSquares.[0]
    perfectSquares.[1]
    perfectSquares.[2];;
The first three perfect squares are 1, 4, and 9
val it : unit = ()

Example 4-9 uses array indexers to change individual characters of a character array to implement a primitive form of encryption known as ROT13, which works by simply taking each letter and rotating it 13 places forward in the alphabet. The example achieves this by converting each letter to an integer, adding 13, and then converting it back to a character.

Example 4-9. ROT13 encryption in F#
open System

// Encrypt a letter using ROT13
let rot13Encrypt (letter : char) =

    // Move the letter forward 13 places in the alphabet (looping around)
    // Otherwise ignore.
    if Char.IsLetter(letter) then
        let newLetter =
            (int letter)
            |> (fun letterIdx -> letterIdx - (int 'A'))
            |> (fun letterIdx -> (letterIdx + 13) % 26)
            |> (fun letterIdx -> letterIdx + (int 'A'))
            |> char
        newLetter
    else
        letter

// Loop through each array element, encrypting each letter
let encryptText (text  : char[]) =

    for idx = 0 to text.Length - 1 do
        let letter = text.[idx]
        text.[idx] <- rot13Encrypt letter

let text =
    Array.ofSeq "THE QUICK BROWN FOX JUMPED OVER THE LAZY DOG"

printfn "Original = %s" <| new String(text)
encryptText(text)
printfn "Encrypted = %s" <| new String(text)

// A unique trait of ROT13 is that to decrypt, simply encrypt again
encryptText(text)
printfn "Decrypted = %s" <| new String(text)

The output of our simple program is:

Original = THE QUICK FOX JUMPED OVER THE LAZY DOG
Encrypted = GUR DHVPX SBK WHZCRQ BIRE GUR YNML QBT
Decrypted = THE QUICK FOX JUMPED OVER THE LAZY DOG

Warning

Unlike C# and VB.NET, indexers in F# require using the dot notation. You can think of an indexer then as just another method or property:

// Incorrect
x[0]
// Correct
x.[0]

Attempting to access an element in the array with an index either less than zero or greater than or equal to the number of elements in the array will raise an IndexOutOfRangeException exception (exceptions are covered later in this chapter). Fortunately, arrays have a Length property, which will return the number of items in the array. Because array indexes are zero-based, you need to subtract one from Length to get the index of the last element in the array:

> let alphabet = [| 'a' .. 'z' |];;

val alphabet : char []

> // First letter
alphabet.[0];;

val it : char = 'a'
> // Last leter
alphabet.[alphabet.Length - 1];;

val it : char = 'z'
> // Some nonexistent letter
alphabet.[10000];;

System.IndexOutOfRangeException: Index was outside the bounds of the array.
   at <StartupCode$FSI_0012>.$FSI_0012._main()
stopped due to error

Array Slices

When you’re analyzing data stored in arrays, it is sometimes convenient to just work with a subset of the data. In F#, there is a special syntax for taking a slice of an array, where you specify optional lower and upper bounds for the subset of data. The syntax for taking a slice is:

array.[lowerBound..upperBound]

If no lower bound is specified, 0 is used. If no upper bound is specified, then the length of the array — 1 is used. If neither a lower nor upper bound is specified, by using *, then the entire array is copied.

Example 4-10 shows the various ways for taking a slice of an array, but we will break it down line by line shortly.

Example 4-10. Using array slices
open System
let daysOfWeek = Enum.GetNames( typeof<DayOfWeek> )

// Standard array slice, elements 2 through 4
daysOfWeek.[2..4]

// Just specify lower bound, elements 4 to the end
daysOfWeek.[4..]

// Just specify an upper bound, elements 0 to 2
daysOfWeek.[..2]

// Specify no bounds, get all elements (copies the array)
daysOfWeek.[*]

The first way we sliced an array was specifying both an upper and lower bound; this returned all array elements within that range:

> // Standard array slice, elements 2 through 4
daysOfWeek.[2..4];;
val it : string [] = [|"Tuesday"; "Wednesday"; "Thursday"|]

Next we specified just a lower or just an upper bound. This returns each element from the lower bound to the end of the array, or from the beginning of the array to the upper bound:

> // Just specify lower bound, elements 4 to the end
daysOfWeek.[4..];;
val it : string [] = [|"Thursday"; "Friday"; "Saturday"|]

> // Just specify an upper bound, elements 0 to 2
daysOfWeek.[..2];;
val it : string [] = [|"Sunday"; "Monday"; "Tuesday"|]

And finally we just copied the entire array using an *. Note that for every slice operation, a new array is returned, so there will never be problems with aliasing:

> // Specify no bounds, get all elements (copies the array)
daysOfWeek.[*];;

Creating Arrays

Array comprehensions and manually specifying each element aren’t the only ways to construct arrays. You can also use the Array.init function, which takes a function used to generate each array element based on its index. To create an uninitialized array, use the Array.zeroCreate function. With that function, each element is initialized to its default value—zero or null.

Example 4-11 shows how to use Array.init and Array.zeroCreate to construct arrays.

Example 4-11. Initializing arrays using Array.init
> // Initialize an array of sin-wave elements
let divisions = 4.0
let twoPi = 2.0 * Math.PI;;

val divisions : float = 4.0
val twoPi : float = 6.283185307

> Array.init (int divisions) (fun i -> float i * twoPi / divisions);;
val it : float [] = [|0.0; 1.570796327; 3.141592654; 4.71238898|]
> // Construct empty arrays
let emptyIntArray    : int [] = Array.zeroCreate 3
let emptyStringArray : string [] = Array.zeroCreate 3;;

val emptyIntArray : int [] = [|0; 0; 0|]
val emptyStringArray : string [] = [|null; null; null|]

Note

The CLR limits arrays to take up no more than 2GB of memory, even on 64-bit machines. If you need to allocate an array to store a massive amount of data, then use a custom data structure instead.

Pattern Matching

Pattern matching against arrays is just as easy as using lists. And just like pattern matching against lists, when matching against arrays, you can capture element values as well as match against the structure of the array. Example 4-12 shows matching an array value against null or an array with 0, 1, or 2 elements.

Example 4-12. Pattern matching against arrays
> // Describe an array
let describeArray arr =
    match arr with
    | null       -> "The array is null"
    | [| |]      -> "The array is empty"
    | [| x |]    -> sprintf "The array has one element, %A" x
    | [| x; y |] -> sprintf "The array has two elements, %A and %A" x y
    | a -> sprintf "The array had %d elements, %A" a.Length a;;

val describeArray : 'a [] -> string

> describeArray [| 1 .. 4 |];;
val it : string = "The array had 4 elements, [|1; 2; 3; 4|]"
> describeArray [| ("tuple", 1, 2, 3) |];;
val it : string = "The array has one element, ("tuple", 1, 2, 3)"

Array Equality

Arrays in F# are compared using structural equality. Two arrays are considered equal if they have the same rank, length, and elements (rank is the dimensionality of the array, something we cover in the next section):

> [| 1 .. 5 |] = [| 1; 2; 3; 4; 5 |];;
val it : bool = true
> [| 1 .. 3 |] = [| |];;
val it : bool = false

Note

This is different from the behavior of equality on arrays in C#. In F#, the = operator contains special logic for comparing arrays so the default referential equality is not used. For more information on object equality in .NET, refer to Chapter 5.

Array Module Functions

Just like the List and Seq modules, there is an Array module containing methods like Array.iter, Array.map, and Array.fold. Among these methods in the Array module are a pair used for creating new arrays, detailed in Table 4-1.

Table 4-1. Array construction methods

Function

Description

Array.init

int -> (int -> 'a) -> 'a[]

Creates a new array with the given number of elements; each element is initialized by the result of the provided function.

Array.zeroCreate

int -> 'a[]

Creates an array with the given length where each entry is the type’s default value.

partition

Array.partition divides the given array into two new arrays. The first array contains only elements where the provided function returns true, the second array contains elements where the provided function returns false:

> // Simple Boolean function
let isGreaterThanTen x = (x > 10);;

val isGreaterThanTen : int -> bool

> // Partitioning arrays
[| 5; 5; 6; 20; 1; 3; 7; 11 |]
|> Array.partition isGreaterThanTen;;
val it : int [] * int [] = ([|20; 11|], [|5; 5; 6; 1; 3; 7|])

tryFind and tryFindIndex

Array.tryFind returns Some of the first element for which the given function returns true. Otherwise, it returns None. Array.tryFindIndex works just like Array.tryFind, except rather than returning the element, it returns its index in the array:

> // Simple Boolean function
let rec isPowerOfTwo x =
    if x = 2 then
        true
    elif x % 2 = 1  (* is odd *) then
        false
    else isPowerOfTwo (x / 2);;

val isPowerOfTwo : int -> bool

> [| 1; 7; 13; 64; 32 |]
|> Array.tryFind isPowerOfTwo;;
val it : int option = Some 64
> [| 1; 7; 13; 64; 32 |]
|> Array.tryFindIndex isPowerOfTwo;;
val it : int option = Some 3

Note

Array.tryFind and Array.tryFindIndex illustrate why the Option type is so powerful. In C#, functions similar to tryFindIndex will return −1 to indicate failure (as opposed to None). However, if trying to implement tryFind, −1 could both indicate a failure to find an array element or finding an element with value −1.

Aggregate operators

The Array module also contains the aggregate operators of the List module. Namely, fold, foldBack, map, and iter. In addition, there are also index-aware versions of these methods. Example 4-13 demonstrates the iteri function, which behaves just like iter except that in addition to the array element, the element’s index is provided as well.

Example 4-13. Using the iteri array aggregate function
> let vowels = [| 'a'; 'e'; 'i'; 'o'; 'u' |];;

val vowels : char [] = [|'a'; 'e'; 'i'; 'o'; 'u'|]

> Array.iteri (fun idx chr -> printfn "vowel.[%d] = %c" idx chr) vowels
vowel.[0] = a
vowel.[1] = e
vowel.[2] = i
vowel.[3] = o
vowel.[4] = u
val it : unit = ()

Multidimensional Arrays

Arrays are helpful for storing data in a linear fashion, but what if you need to represent data as a two-, three-, or higher-dimensional grid? You can create multidimensional arrays, which enable you to treat data as a block indexed by several values.

Multidimensional arrays come in two flavors: rectangular and jagged. The difference is illustrated in Figure 4-2. Rectangular arrays are in a solid block whereas jagged arrays are essentially arrays of arrays.

Jagged and rectangular arrays
Figure 4-2. Jagged and rectangular arrays

Which type of multidimensional array to use depends on the situation. Using jagged arrays allows you to save memory if each row doesn’t need to be the same length; however, rectangular arrays are more efficient for random access because the elements are stored in a contiguous block of memory (and therefore can benefit from your processor’s cache).

Rectangular arrays

Rectangular arrays are rectangular blocks of n by m elements in memory. Rectangular arrays are indicated by rectangular brackets with a comma separating them for each new dimension. Also, just like single-dimensional arrays, there are the Array2D and Array3D modules for creating and initializing rectangular arrays:

> // Creating a 3x3 array
let identityMatrix : float[,] = Array2D.zeroCreate 3 3
identityMatrix.[0,0] <- 1.0
identityMatrix.[1,1] <- 1.0
identityMatrix.[2,2] <- 1.0;;


val identityMatrix : float [,] = [[1.0; 0.0; 0.0]
                                  [0.0; 1.0; 0.0]
                                  [0.0; 0.0; 1.0]]

Two-dimensional rectangular arrays can also take slices, using the same syntax but providing a slice for each dimension:

> // All rows for columns with index 1 through 2
identityMatrix.[*, 1..2];;
val it : float [,] = [[0.0; 0.0]
                      [1.0; 0.0]
                      [0.0; 1.0]]

Jagged arrays

Jagged arrays are simply single-dimensional arrays of single-dimensional arrays. Each row in the main array is a different array, each needing to be initialized separately:

> // Create a jagged array
let jaggedArray : int[][]  = Array.zeroCreate 3
jaggedArray.[0] <- Array.init 1 (fun x -> x)
jaggedArray.[1] <- Array.init 2 (fun x -> x)
jaggedArray.[2] <- Array.init 3 (fun x -> x);;


val jaggedArray : int [] [] = [|[|0|]; [|0; 1|]; [|0; 1; 2|]|]

Mutable Collection Types

Often you will need to store data, but you won’t know how many items you will have ahead of time. For example, you might be loading records from a database. If you used an array, you run the risk of allocating too many elements and wasting memory, or even worse, not enough and crashing instead.

Mutable collection types allow you to dynamically add and remove elements over time. Although mutable collection types can be problematic when doing parallel and asynchronous programming, they are very simple to work with.

List<'T>

The List type in the System.Collections.Generic namespace—not to be confused with the F# list type—is a wrapper on top of an array that allows you to dynamically adjust the size of the array when adding and removing elements. Because List is built on top of standard arrays, retrieving and adding values is typically very fast. But once the internal array is filled up, a new, larger one will need to be created and the List ’s contents copied over.

Example 4-14 shows basic usage of a List. Again, we’re denying poor Pluto the title of planet.

Example 4-14. Using the List type
> // Create a List<_> of planets
open System.Collections.Generic
let planets = new List<string>();;

val planets : Collections.Generic.List<string>

> // Add individual planets
planets.Add("Mercury")
planets.Add("Venus")
planets.Add("Earth")
planets.Add("Mars");;
> planets.Count;;
val it : int = 4
> // Add a collection of values at once
planets.AddRange( [| "Jupiter"; "Saturn"; "Uranus"; "Neptune"; "Pluto" |] );;
val it : unit = ()
> planets.Count;;
val it : int = 9
> // Sorry bro
planets.Remove("Pluto");;
val it : bool = true
> planets.Count;;
val it : int = 8

Table 4-2 shows common methods on the List type.

Table 4-2. Methods and properties on the List<'T> type

Function and type

Description

Add:

'a -> unit

Adds an element to the end of the list.

Clear:

unit -> unit

Removes all elements from the list.

Contains:

'a -> bool

Returns whether or not the item can be found in the list.

Count:

int

Property for the number of items in the list.

IndexOf:

'a -> int

Returns a zero-based index of the given item in the list. If it is not present, returns −1.

Insert:

int * 'a -> unit

Inserts the given item at the specified index into the list.

Remove:

'a -> bool

Removes the given item if present from the list.

RemoveAt:

int -> unit

Removes the item at the specified index.

Dictionary<'K,'V>

The Dictionary type in the System.Collections.Generic namespace contains key-value pairs. Typically you would use a dictionary when you want to store data and require a friendly way to look it up, rather than an index, such as using a name to look up someone’s phone number.

Example 4-15 shows using a dictionary to map element symbols to a custom Atom type. Note the use of F#’s units of measure feature to encode an atom’s weight in atomic mass units.

Example 4-15. Using a dictionary
// Atomic Mass Units
[<Measure>]
type amu

type Atom = { Name : string; Weight : float<amu> }

open System.Collections.Generic
let periodicTable = new Dictionary<string, Atom>()

periodicTable.Add( "H", { Name = "Hydrogen";  Weight = 1.0079<amu> })
periodicTable.Add("He", { Name = "Helium";    Weight = 4.0026<amu> })
periodicTable.Add("Li", { Name = "Lithium";   Weight = 6.9410<amu> })
periodicTable.Add("Be", { Name = "Beryllium"; Weight = 9.0122<amu> })
periodicTable.Add( "B", { Name = "Boron ";    Weight = 10.811<amu> })
// ...

// Lookup an element
let printElement name =

    if periodicTable.ContainsKey(name) then
        let atom = periodicTable.[name]
        printfn
            "Atom with symbol with '%s' has weight %A."
            atom.Name atom.Weight
    else
        printfn "Error. No atom with name '%s' found." name

// Alternate syntax to get a value. Return a tuple of 'success * result'
let printElement2 name =

    let (found, atom) = periodicTable.TryGetValue(name)
    if found then
        printfn
            "Atom with symbol with '%s' has weight %A."
            atom.Name atom.Weight
    else
        printfn "Error. No atom with name '%s' found." Name

Table 4-3 shows common methods on the Dictionary<'k,'v> type.

Table 4-3. Methods and properties on the Dictionary<'k, ‘v> type

Function and Type

Description

Add:

'k * 'v -> unit

Adds a new key-value pair to the dictionary.

Clear:

unit -> unit

Removes all items from the dictionary.

ContainsKey:

'k -> bool

Checks if the given key is present in the dictionary.

ContainsValue:

'v -> bool

Checks if the given value is present in the dictionary.

Count:

int

Returns the number of items in the dictionary.

Remove:

'k -> unit

Removes a key-value pair from the dictionary with then given key.

HashSet<'T>

A HashSet, also defined in the System.Collections.Generic namespace, is an efficient collection for storing an unordered set of items. Let’s say you were writing an application to crawl web pages. You would need to keep track of which pages you have visited before so you didn’t get stuck in an infinite loop; however, if you stored the page URLs you have already visited in a List, it would be very slow to loop through each element to check if a page had already been visited. A HashSet stores a collection of unique values based on their hash code, so checking if an element exists in the set can be done rather quickly.

Hash codes are better explained in Chapter 5, but for now you can just think of them as a way to uniquely identify an object. They are the reason why looking up elements in a HashSet or Dictionary is fast.

Example 4-16 shows using a HashSet to check if a film has won the Oscar for Best Picture.

Example 4-16. Using the HashSet type
open System.Collections.Generic

let bestPicture = new HashSet<string>()
bestPicture.Add("The Artist")
bestPicture.Add("The King's Speech")
bestPicture.Add("The Hurt Locker")
bestPicture.Add("Slumdog Milionaire")
bestPicture.Add("No Country for Old Men")
bestPicture.Add("The Departed")
// ...

// Check if it was a best picture
if bestPicture.Contains("Manos: The Hands of Fate") then
    printfn "Sweet..."
    // ...

Table 4-4 shows common methods on the HashSet<'T> type.

Table 4-4. Methods and properties on the HashSet<'T> type

Function and Type

Description

Add:

'a -> unit

Adds a new item to the HashSet.

Clear:

unit -> unit

Removes all items from the HashSet.

Count:

int

Returns the number of items in the HashSet.

IntersectWith:

seq<'a> -> unit

Modifies the HashSet to contain only elements that are also contained in the given sequence.

IsSubsetOf:

seq<'a > -> bool

Returns whether the HashSet is a subset of the sequence; that is, every element in the HashSet is found in the sequence.

IsSupersetOf:

seq<'a > -> bool

Returns whether the HashSet is a superset of the sequence; that is, every element in the sequence is contained in the HashSet.

Remove:

'a -> unit

Removes the given item from the HashSet.

UnionWith:

seq<'a> -> unit

Modifies the HashSet to contain at least every element in the given sequence.

Looping Constructs

F# has the traditional sorts of looping constructs seen in imperative languages. These allow you to repeat the same function or piece of code a set number of times or until a condition is met.

While Loops

while expressions loop until a Boolean predicate evaluates to false. (For this reason, while loops cannot be used in a purely functional style; otherwise the loop would never terminate because you could never update the predicate.)

Note that the while loop predicate must evaluate to bool and the body of the loop must evaluate to unit. The following code shows how to use a while loop to count up from zero:

> // While loop
let mutable i = 0
while i < 5 do
    i <- i + 1
    printfn "i = %d" i;;
i = 1
i = 2
i = 3
i = 4
i = 5

val mutable i : int = 5

The predicate for the while loop is checked before the loop starts, so if the initial value of the predicate is false, the loop will never execute.

For Loops

When you need to iterate a fixed number of times, you can use a for expression, of which there are two flavors in F#: simple and enumerable.

Simple for loops

Simple for loops introduce a new integer value and count up to a fixed value. The loop won’t iterate if the counter is greater than the maximum value:

> // For loop
for i = 1 to 5 do
    printfn "%d" i;;
1
2
3
4
5
val it : unit = ()

To count down, use the downto keyword. The loop won’t iterate if the counter is less than the minimum value:

> // Counting down
for i = 5 downto 1 do
    printfn "%d" i;;
5
4
3
2
1
val it : unit = ()

Numerical for loops are only supported with integers as the counter, so if you need to loop more than System.Int32.MaxValue times, you will need to use enumerable for loops.

Enumerable for loop

The more common type of for loop is one that iterates through a sequence of values. Enumerable for loops work with any seq type, such as array or list. The following example loops over a list literal:

> // Enumerable loop
for i in [1 .. 5] do
    printfn "%d" i;;
1
2
3
4
5
val it : unit = ()

What makes enumerable for loops more powerful than a foreach loop in C# is that enumerable for loops can take advantage of pattern matching. The element that follows the for keyword is actually a pattern-match rule, so if it is a new identifier like i, it simply captures the pattern-match value. But more complex pattern-match rules can be used instead.

In Example 4-17, a discriminated union is introduced with two union cases, Cat and Dog. The for loop iterates through a list but only executes when the element in the list is an instance of the Dog union case.

Example 4-17. For loops with pattern matching
> // Pet type
type Pet =
    | Cat of string * int // Name, Lives
    | Dog of string       // Name
;;

type Pet =
  | Cat of string * int
  | Dog of string

> let famousPets = [ Dog("Lassie"); Cat("Felix", 9); Dog("Rin Tin Tin") ];;

val famousPets : Pet list = [Dog "Lassie"; Cat ("Felix",9); Dog "Rin Tin Tin"]

> // Print famous dogs. (Prints warning due to incomplete match.)
for Dog(name) in famousPets do
    printfn "%s was a famous dog." name;;
Lassie was a famous dog.
Rin Tin Tin was a famous dog.
val it : unit = ()

Note

There are no break or continue keywords in F#. If you want to exit prematurely in the middle of a for loop, you will need to create a mutable value and convert the for loop to a while loop.

Exceptions

Every programmer knows things don’t always happen as planned. I’ve been careful in examples so far to avoid showing you code where things fail. But dealing with the unexpected is a crucial aspect of any program and, fortunately, the .NET platform supports a powerful mechanism for handling the unexpected.

An exception is a failure in a .NET program that causes an abnormal branch in control flow. If an exception occurs, the function immediately exits, as well as its calling function, and its calling function, and so on until the exception is caught by an exception handler. If the exception is never caught, then the program terminates.

The simplest way to report an error in an F# program is to use the failwith function. This function takes a string as a parameter and, when called, throws an instance of System.Exception. An alternate version exists called failwithf that takes a format string similar to printf and sprintf:

> // Using failwithf
let divide x y =
    if y = 0 then failwithf "Cannot divide %d by zero!" x
    x / y;;

val divide : int -> int -> int

> divide 10 0;;
System.Exception: Cannot divide 10 by zero!
   at FSI_0003.divide(Int32 x, Int32 y)
   at <StartupCode$FSI_0004>.$FSI_0004._main()
stopped due to error

In the previous example, FSI indicated an exception was thrown, displaying the two most important properties on an exception type: the exception message and stack trace. Each exception has a Message property, which is a programmer-friendly description of the problem. The Stacktrace property is a string printout of all the functions waiting on a return value before the exception occurred, and is invaluable for tracking down the origin of an exception. Because the stack unwinds immediately after an exception is thrown, the exception could be caught far away from the where the exception originated.

Although a descriptive message helps programmers debug the exception, it is a best practice in .NET to use a specific exception type, because using typed exceptions makes it far easier to catch and deal with any problems appropriately. To throw a more specific type of exception, you use the raise function. This takes a custom exception type (any type derived from System.Exception) and throws the exception just like failwith:

> // Raising a DivideByZeroException
let divide2 x y =
    if y = 0 then raise <| new System.DivideByZeroException()
    x / y;;

val divide2 : int -> int -> int

> divide2 10 0;;
System.DivideByZeroException: Attempted to divide by zero.
   at FSI_0005.divide2(Int32 x, Int32 y)
   at <StartupCode$FSI_0007>.$FSI_0007._main()
stopped due to error

Warning

It is tempting to throw exceptions liberally when your program reaches an unexpected situation; however, throwing exceptions incurs a significant performance hit. Whenever possible, situations that would throw an exception should be obviated.

Handling Exceptions

To handle an exception, you catch it using a try-catch expression. Any exceptions raised while executing code within a try-catch expression will be handled by a with block, which is a pattern match against the exception type.

Because the exception handler to execute is determined by pattern matching, you can combine exception handlers for multiple types using Or. Similarly, you can use a wildcard to catch any exception. If an exception is thrown within a try-catch expression and an appropriate exception handler cannot be found, the exception will continue bubbling up until caught or the program terminates.

try-catch expressions return a value, just like a pattern match or if expression. So naturally the last expression in the try block must have the same type as each rule in the with pattern match.

Example 4-18 shows some code that runs through a minefield of potential problems, with each possible exception handled by an appropriate exception handler. In the example, the :? dynamic type test operator is used to match against the exception type; this operator is covered in more detail in Chapter 5.

Example 4-18. Try–catch expressions
open System.IO

[<EntryPoint>]
let main (args : string[]) =

    let exitCode =
        try
            let filePath = args.[0]

            printfn "Trying to gather information about file:"
            printfn "%s" filePath

            // Does the drive exist?
            let matchingDrive =
                Directory.GetLogicalDrives()
                |> Array.tryFind (fun drivePath -> drivePath.[0] = filePath.[0])

            if matchingDrive = None then
                raise <| new DriveNotFoundException(filePath)

            // Does the folder exist?
            let directory = Path.GetPathRoot(filePath)
            if not <| Directory.Exists(directory) then
                raise <| new DirectoryNotFoundException(filePath)

            // Does the file exist?
            if not <| File.Exists(filePath) then
                raise <| new FileNotFoundException(filePath)

            let fileInfo = new FileInfo(filePath)
            printfn "Created  = %s" <| fileInfo.CreationTime.ToString()
            printfn "Access   = %s" <| fileInfo.LastAccessTime.ToString()
            printfn "Size     = %d" fileInfo.Length

            0

        with
        // Combine patterns using Or
        | :? DriveNotFoundException
        | :? DirectoryNotFoundException
            ->  printfn "Unhandled Drive or Directory not found exception"
                1
        // Bind the exception value to value ex
        | :? FileNotFoundException as ex
            ->  printfn "Unhandled FileNotFoundException: %s" ex.Message
                3
        | :? IOException as ex
            ->  printfn "Unhandled IOException: %s" ex.Message
                4
        // Use a wildcard match (ex will be of type System.Exception)
        | _ as ex
            ->  printfn "Unhandled Exception: %s" ex.Message
                5

    // Return the exit code
    printfn "Exiting with code %d" exitCode
    exitCode

Because not catching an exception might prevent unmanaged resources from being freed, such as closing file handles or flushing buffers, there is a second way to catch process exceptions: try-finally expressions. In a try-finally expression, the code in the finally block is executed whether or not an exception is thrown, giving you an opportunity to do required cleanup work.

Example 4-19 demonstrates a try-finally expression in action.

Example 4-19. Try–finally expressions
> // Try-finally expressions
let tryFinallyTest() =
    try
        printfn "Before exception..."
        failwith "ERROR!"
        printfn "After exception raised..."
    finally
        printfn "Finally block executing..."

let test() =
    try
        tryFinallyTest()
    with
    | ex -> printfn "Exception caught with message: %s" ex.Message;;

val tryFinallyTest : unit -> unit
val test : unit -> unit

> test();;
Before exception...
Finally block executing...
Exception caught with message: ERROR!
val it : unit = ()

Note

Unlike C#, Java, and other languages, there is no try-catch-finally expression in F#. If you need to clean up any resources within an exception handler, you must do it for each exception handler or simply after the try-catch block.

Reraising Exceptions

Sometimes, despite your best efforts to take corrective action for raised exceptions, you just can’t fix the problem. In those situations, you can reraise the exception, which will allow the original exception to continue bubbling up from within an exception handler.

Example 4-20 demonstrates reraising an exception by using the reraise function.

Example 4-20. Reraise exceptions
// Retry a function throwing an exception N times before failing.
let tryWithBackoff f times =
    let mutable attempt = 1
    let mutable success = false

    while not success do
        try
            f()
            success <- true
        with ex ->
            attempt <- attempt + 1
            if attempt >= times then
                reraise()

Defining Exceptions

The reason for throwing specialized exceptions is for consumers of your code to only catch the exceptions they know how to handle. Other exceptions will then continue to bubble up until an appropriate exception handler is found.

You can define your own custom exceptions by creating types that inherit from System.Exception, which you will see in Chapter 5. However, in F#, there is an easier way to define exceptions using a lightweight exception syntax. Declaring exceptions in this way allows you to define them with the same syntax as discriminated unions.

Example 4-21 shows creating several new exception types, some of which are associated with data. The advantage of these lightweight exceptions is that when they are caught, they are easier to extract the relevant data from because you can use the same syntax for pattern matching against discriminated unions. Also, there is no need for a dynamic type test operator, :?, which we saw in previous examples.

Example 4-21. Lightweight F# exception syntax
open System
open System.Collections.Generic

exception NoMagicWand
exception NoFullMoon of int * int
exception BadMojo of string

let castHex (ingredients : HashSet<string>) =
    try

        let currentWand = Environment.MagicWand

        if currentWand = null then
            raise NoMagicWand

        if not <| ingredients.Contains("Toad Wart") then
            raise <| BadMojo("Need Toad Wart to cast the hex!")

        if not <| isFullMoon(DateTime.Today) then
            raise <| NoFullMoon(DateTime.Today.Month, DateTime.Today.Day)

        // Begin the incantation...
        let mana =
            ingredients
            |> Seq.map (fun i -> i.GetHashCode())
            |> Seq.fold (+) 0

        sprintf "%x" mana

    with
    | NoMagicWand
        -> "Error: A magic wand is required to hex!"
    | NoFullMoon(month, day)
        -> "Error: Hexes can only be cast during a full moon."
    | BadMojo(msg)
        -> sprintf "Error: Hex failed due to bad mojo [%s]" msg 

In Chapter 3, you looked at the functional style of programming, which provided some interesting ways to write code, but doesn’t quite stand on its own. The purely functional style doesn’t integrate well with the existing .NET framework class libraries and sometimes requires complicated solutions for simple problems.

In this chapter, you learned how to update values, which enables you to write new types of programs. Now you can use efficient collections to store program results, loop as necessary, and should any problems occur, throw exceptions.

Now you can make a choice for how to approach problems, and you can begin to see the value of multiparadigm computing. Some problems can be solved by simply building up a mutable data structure, whereas others can be built up through combining simple functions to transform immutable data. In F#, you have options.

In the next chapter, we look at object-oriented programming. This third paradigm of F# doesn’t necessarily add much more computational power, but does provide a way for programmers to organize and abstract code.

Get Programming F# 3.0, 2nd Edition now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.