Jump to content

F Sharp Programming/Tuples and Records

From Wikibooks, open books for an open world
Previous: Option Types Index Next: Lists
F# : Tuples and Records

Defining Tuples

[edit | edit source]

A tuple is defined as a comma separated collection of values. For example, (10, "hello") is a 2-tuple with the type (int * string). Tuples are extremely useful for creating ad hoc data structures which group together related values. Note that the parentheses are not part of the tuple but it is often necessary to add them to ensure that the tuple only includes what you think it includes.

let average (a, b) =
    (a + b) / 2.0

This function has the type float * float -> float, it takes a float * float tuple and returns another float.

> let average (a, b) =
    let sum = a + b
    sum / 2.0;;

val average : float * float -> float

> average (10.0, 20.0);;
val it : float = 15.0

Notice that a tuple is considered a single argument. As a result, tuples can be used to return multiple values:

Example 1 - a function which multiplies a 3-tuple by a scalar value to return another 3-tuple.

> let scalarMultiply (s : float) (a, b, c) = (a * s, b * s, c * s);;

val scalarMultiply : float -> float * float * float -> float * float * float

> scalarMultiply 5.0 (6.0, 10.0, 20.0);;
val it : float * float * float = (30.0, 50.0, 100.0)

Example 2 - a function which reverses the input of whatever is passed into the function.

> let swap (a, b) = (b, a);;
val swap : 'a * 'b -> 'b * 'a

> swap ("Web", 2.0);;
val it : float * string = (2.0, "Web")

> swap (20, 30);;
val it : int * int = (30, 20)

Example 3 - a function which divides two numbers and returns the remainder simultaneously.

> let divrem x y =
    match y with
    | 0 -> None
    | _ -> Some(x / y, x % y);;

val divrem : int -> int -> (int * int) option

> divrem 100 20;; (* 100 / 20 = 5 remainder 0 *)
val it : (int * int) option = Some (5, 0)

> divrem 6 4;; (* 6 / 4 = 1 remainder 2 *)
val it : (int * int) option = Some (1, 2)

> divrem 7 0;; (* 7 / 0 throws a DivisionByZero exception *)
val it : (int * int) option = None

Every tuple has a property called arity, which is the number of arguments used to define a tuple. For example, an int * string tuple is made up of two parts, so it has an arity of 2, a string * string * float has an arity of 3, and so on.

Pattern Matching Tuples

[edit | edit source]

Pattern matching on tuples is easy, because the same syntax used to declare tuple types is also used to match tuples.

Example 1

Let's say that we have a function greeting that prints out a custom greeting based on the specified name and/or language.

let greeting (name, language) =
    match (name, language) with
    | ("Steve", _) -> "Howdy, Steve"
    | (name, "English") -> "Hello, " + name
    | (name, _) when language.StartsWith("Span") -> "Hola, " + name
    | (_, "French") -> "Bonjour!"
    | _ -> "DOES NOT COMPUTE"

This function has type string * string -> string, meaning that it takes a 2-tuple and returns a string. We can test this function in fsi:

> greeting ("Steve", "English");;
val it : string = "Howdy, Steve"
> greeting ("Pierre", "French");;
val it : string = "Bonjour!"
> greeting ("Maria", "Spanish");;
val it : string = "Hola, Maria"
> greeting ("Rocko", "Esperanto");;
val it : string = "DOES NOT COMPUTE"

Example 2

We can conveniently match against the shape of a tuple using the alternative pattern matching syntax:

> let getLocation = function
    | (0, 0) -> "origin"
    | (0, y) -> "on the y-axis at y=" + y.ToString()
    | (x, 0) -> "on the x-axis at x=" + x.ToString()
    | (x, y) -> "at x=" + x.ToString() + ", y=" + y.ToString() ;;

val getLocation : int * int -> string

> getLocation (0, 0);;
val it : string = "origin"
> getLocation (0, -1);;
val it : string = "on the y-axis at y=-1"
> getLocation (5, -10);;
val it : string = "at x=5, y=-10"
> getLocation (7, 0);;
val it : string = "on the x-axis at x=7"

fst and snd

[edit | edit source]

F# has two built-in functions, fst and snd, which return the first and second items in a 2-tuple. These functions are defined as follows:

let fst (a, b) = a
let snd (a, b) = b

They have the following types:

val fst : 'a * 'b -> 'a
val snd : 'a * 'b -> 'b

Here are a few examples in FSI:

> fst (1, 10);;
val it : int = 1
> snd (1, 10);;
val it : int = 10
> fst ("hello", "world");;
val it : string = "hello"
> snd ("hello", "world");;
val it : string = "world"
> fst ("Web", 2.0);;
val it : string = "Web"
> snd (50, 100);;
val it : int = 100

Assigning Multiple Variables Simultaneously

[edit | edit source]

Tuples can be used to assign multiple values simultaneously. This is the same as tuple unpacking in Python. The syntax for doing so is:

let val1, val2, ... valN = (expr1, expr2, ... exprN)

In other words, you assign a comma-separated list of N values to an N-tuple. Here's an example in FSI:

> let x, y = (1, 2);;

val y : int
val x : int

> x;;
val it : int = 1

> y;;
val it : int = 2

The number of values being assigned must match the arity of tuple returned from the function, otherwise F# will raise an exception:

> let x, y = (1, 2, 3);;

  let x, y = (1, 2, 3);;
  ------------^^^^^^^^

stdin(18,13): error FS0001: Type mismatch. Expecting a
	'a * 'b
but given a
	'a * 'b * 'c.
The tuples have differing lengths of 2 and 3.

Tuples and the .NET Framework

[edit | edit source]

From a point of view F#, all methods in the .NET Base Class Library take a single argument, which is a tuple of varying types and arity. For example:

Class C# Function Signature F# Function Signature
System.String String Join(String separator, String[] value) val Join : (string * string array) -> string
System.Net.WebClient void DownloadFile(String uri, String fileName) val DownloadFile : (string * string) -> unit
System.Convert String ToString(int value, int toBase) val ToString : (int * int) -> string
System.Math int DivRem(int a, int b, out int remainder) val DivRem : (int * int) -> (int * int)
System.Int32 bool TryParse(String value, out int result) val TryParse : string -> (bool * int)

Some methods, such as the System.Math.DivRem shown above, and others such as System.Int32.TryParse return multiple through output variables. F# allows programmers to omit an output variable; using this calling convention, F# will return results of a function as a tuple, for example:

> System.Int32.TryParse("3");;
val it : bool * int = (true, 3)

> System.Math.DivRem(10, 7);;
val it : int * int = (1, 3)

Defining Records

[edit | edit source]

A record is similar to a tuple, except it contains named fields. A record is defined using the syntax:

type recordName =
    { [ fieldName : dataType ] + }
+ means the element must occur one or more times.

Here's a simple record:

type website =
    { Title : string;
        Url : string }

Unlike a tuple, a record is explicitly defined as its own type using the type keyword, and record fields are defined as a semicolon-separated list. (In many ways, a record can be thought of as a simple class.)

A website record is created by specifying the record's fields as follows:

> let homepage = { Title = "Google"; Url = "http://www.google.com" };;
val homepage : website

Note that F# determines a records type by the name and type of its fields, not the order that fields are used. For example, while the record above is defined with Title first and Url second, it's perfectly legitimate to write:

> { Url = "http://www.microsoft.com/"; Title = "Microsoft Corporation" };;
val it : website = {Title = "Microsoft Corporation";
                    Url = "http://www.microsoft.com/";}

It's easy to access a record's properties using dot notation:

> let homepage = { Title = "Wikibooks"; Url = "http://www.wikibooks.org/" };;

val homepage : website

> homepage.Title;;
val it : string = "Wikibooks"

> homepage.Url;;
val it : string = "http://www.wikibooks.org/"

Cloning Records

[edit | edit source]

Records are immutable types, which means that instances of records cannot be modified. However, records can be cloned conveniently using the clone syntax:

type coords = { X : float; Y : float }

let setX item newX =
    { item with X = newX }

The method setX has the type coords -> float -> coords. The with keyword creates a clone of item and set its X property to newX.

> let start = { X = 1.0; Y = 2.0 };;
val start : coords

> let finish = setX start 15.5;;
val finish : coords

> start;;
val it : coords = {X = 1.0;
                   Y = 2.0;}
> finish;;
val it : coords = {X = 15.5;
                   Y = 2.0;}

Notice that the setX creates a copy of the record, it doesn't actually mutate the original record instance.

Here's a more complete program:

type TransactionItem =
    { Name : string;
        ID : int;
        ProcessedText : string;
        IsProcessed : bool }

let getItem name id =
    { Name = name; ID = id; ProcessedText = null; IsProcessed = false }

let processItem item =
    { item with
        ProcessedText = "Done";
        IsProcessed = true }
    
let printItem msg item =
    printfn "%s: %A" msg item

let main() =
    let preProcessedItem = getItem "Steve" 5
    let postProcessedItem = processItem preProcessedItem

    printItem "preProcessed" preProcessedItem
    printItem "postProcessed" postProcessedItem
    
main()

This program processes an instance of the TransactionItem class and prints the results. This program outputs the following:

preProcessed: {Name = "Steve";
 ID = 5;
 ProcessedText = null;
 IsProcessed = false;}
postProcessed: {Name = "Steve";
 ID = 5;
 ProcessedText = "Done";
 IsProcessed = true;}

Pattern Matching Records

[edit | edit source]

We can pattern match on records just as easily as tuples:

open System

type coords = { X : float; Y : float }
 
let getQuadrant = function
    | { X = 0.0; Y = 0.0 } -> "Origin"
    | item when item.X >= 0.0 && item.Y >= 0.0 -> "I"
    | item when item.X <= 0.0 && item.Y >= 0.0 -> "II"
    | item when item.X <= 0.0 && item.Y <= 0.0 -> "III"
    | item when item.X >= 0.0 && item.Y <= 0.0 -> "IV"
 
let testCoords (x, y) =
    let item = { X = x; Y = y }
    printfn "(%f, %f) is in quadrant %s" x y (getQuadrant item)
 
let main() =
    testCoords(0.0, 0.0)
    testCoords(1.0, 1.0)
    testCoords(-1.0, 1.0)
    testCoords(-1.0, -1.0)
    testCoords(1.0, -1.0)
    Console.ReadKey(true) |> ignore
 
main()

Note that pattern cases are defined with the same syntax used to create a record (as shown in the first case), or using guards (as shown in the remaining cases). Unfortunately, programmers cannot use the clone syntax in pattern cases, so a case such as | { item with X = 0 } -> "y-axis" will not compile.

The program above outputs:

(0.000000, 0.000000) is in quadrant Origin
(1.000000, 1.000000) is in quadrant I
(-1.000000, 1.000000) is in quadrant II
(-1.000000, -1.000000) is in quadrant III
(1.000000, -1.000000) is in quadrant IV
Previous: Option Types Index Next: Lists