F Sharp Programming/Mutable Data
From Wikibooks, the open-content textbooks collection
| F# : Mutable Data |
All of the data types and values in F# seen so far have been immutable, meaning the values cannot be reassigned another value after they've been declared. However, F# allows programmers to create variables in the true sense of the word: variables whose values can change throughout the lifetime of the application.
Contents |
[edit] mutable Keyword
The simplest mutable variables in F# are declared using the mutable keyword. Here is a sample using fsi:
> let mutable x = 5;; val mutable x : int > x;; val it : int = 5 > x <- 10;; val it : unit = () > x;; val it : int = 10
As shown above, the <- operator is used to assign a mutable to a new value. Notice that variable assignment actually returns unit as a value.
The mutable keyword is frequently used with record types to create mutable records:
open System type transactionItem = { ID : int; mutable IsProcessed : bool; mutable ProcessedText : string; } let getItem id = { ID = id; IsProcessed = false; ProcessedText = null; } let processItems (items : transactionItem list) = items |> List.iter(fun item -> item.IsProcessed <- true item.ProcessedText <- sprintf "Processed %s" (DateTime.Now.ToString("hh:mm:ss")) Threading.Thread.Sleep(1000) (* Putting thread to sleep for 1 second to similate processing overhead. *) ) let printItems (items : transactionItem list) = items |> List.iter (fun x -> printfn "%A" x) let main() = let items = let items = List.init 5 getItem printfn "Before Process:" printItems items printfn "After process:" processItems items printItems items Console.ReadKey(true) |> ignore main()
Before Process:
{ID = 0;
IsProcessed = false;
ProcessedText = null;}
{ID = 1;
IsProcessed = false;
ProcessedText = null;}
{ID = 2;
IsProcessed = false;
ProcessedText = null;}
{ID = 3;
IsProcessed = false;
ProcessedText = null;}
{ID = 4;
IsProcessed = false;
ProcessedText = null;}
After process:
{ID = 0;
IsProcessed = true;
ProcessedText = "Processed 10:00:31";}
{ID = 1;
IsProcessed = true;
ProcessedText = "Processed 10:00:32";}
{ID = 2;
IsProcessed = true;
ProcessedText = "Processed 10:00:33";}
{ID = 3;
IsProcessed = true;
ProcessedText = "Processed 10:00:34";}
{ID = 4;
IsProcessed = true;
ProcessedText = "Processed 10:00:35";}
[edit] Limitations of Mutable Variables
Mutable variables are somewhat limited: mutables are inaccessible outside of the scope of the function where they are defined. Specifically, this means its not possible to reference a mutable in a subfunction of another function. Here's a demonstration in fsi:
> let testMutable() = let mutable msg = "hello" printfn "%s" msg let setMsg() = msg <- "world" setMsg() printfn "%s" msg;; msg <- "world" --------^^^^^^^^^^^^^^^ stdin(18,9): error FS0191: The mutable variable 'msg' 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 '!'.
[edit] Ref cells
Ref cells get around some of the limitations of mutables. In fact, ref cells are very simple data type which wrap up a mutable field in a record type. Ref cells are defined by F# as follows:
type 'a ref = { mutable contents : 'a }
The F# library contains several built-in functions and operators for working with ref cells:
let ref v = { contents = v } (* val ref : 'a -> 'a ref *) let (!) r = r.contents (* val (!) : 'a ref -> 'a *) let (:=) r v = r.contents <- v (* val (:=) : 'a ref -> 'a -> unit *)
The ref function is used to create a ref cell, the ! operator is used to read the contents of a ref cell, and the := operator is used to assign a ref cell a new value. Here is a sample in fsi:
> let x = ref "hello";; val x : string ref > x;; (* returns ref instance *) val it : string ref = {contents = "hello";} > !x;; (* returns x.contents *) val it : string = "hello" > x := "world";; (* updates x.contents with a new value *) val it : unit = () > !x;; (* returns x.contents *) val it : string = "world"
Since ref cells are allocated on the heap, they can be shared across multiple functions:
open System let withSideEffects x = x := "assigned from withSideEffects function" let refTest() = let msg = ref "hello" printfn "%s" !msg let setMsg() = msg := "world" setMsg() printfn "%s" !msg withSideEffects msg printfn "%s" !msg let main() = refTest() Console.ReadKey(true) |> ignore main()
The withSideEffects function has the type val withSideEffects : string ref -> unit.
This program outputs the following:
hello world assigned from withSideEffects function
The withSideEffects function is named as such because it has a side-effect, meaning it can change the state of a variable in other functions.
[edit] Aliasing Ref Cells
- Note: While imperative programming uses aliasing extensively, this practice has a number of problems. In particular it makes programs hard to follow since the state of any variable can be modified at any point elsewhere in an application. Additionally, multithreaded applications sharing mutable state are difficult to reason about since one thread can potentially change the state of a variable in another thread, which can result in a number of subtle errors related to race conditions and dead locks.
A ref cell is very similar to a C or C++ pointer. Its possible to point to two or more ref cells to same memory address; changes at that memory address change the state of all ref cells pointing to it. Conceptually, this process looks like this:
Let's say we have 3 ref cells looking at the same address in memory:
Memory ____ |____| cell1 |____| / |__7_| - cell2 |____| \ |____| cell3
cell1, cell2, and cell3 are all pointing to the same address in memory. The .contents property of each cell is 7. Let's say, at some point in our program, we execute the code cell1 := 10, this changes the value in memory to the following:
Memory ____ |____| cell1 |____| / |_10_| - cell2 |____| \ |____| cell3
By assigning cell1.contents a new value, the variables cell2 and cell3 were changed as well. This can be demonstrated using fsi as follows:
> let cell1 = ref 7;; val cell1 : int ref > let cell2 = cell1;; val cell2 : int ref > let cell3 = cell2;; val cell3 : int ref > !cell1;; val it : int = 7 > !cell2;; val it : int = 7 > !cell3;; val it : int = 7 > cell1 := 10;; val it : unit = () > !cell1;; val it : int = 10 > !cell2;; val it : int = 10 > !cell3;; val it : int = 10
[edit] Encapsulating Mutable State
F# discourages the practice of passing mutable data between functions. Functions that rely on mutation should generally hide its implementation details behind a private function, such as the following example in FSI:
> let incr = let counter = ref 0 fun () -> counter := !counter + 1 !counter;; val incr : (unit -> int) > incr();; val it : int = 1 > incr();; val it : int = 2 > incr();; val it : int = 3