Let's implement a counter that returns a bigger number every time we call it. We couldn't do this before, as we could only write deterministic functions, functions that return a value solely based on their input.
Every time we invoke the function next, we'll get the value we got last time, but plus 1.
We'll start with a counter ref, to keep track of the value we're currently at.
let counter = ref 0;;
val counter : int ref = {contents = 0}
Now let's use this ref in our next function.
let next () =
counter := !counter + 1;
!counter
val next : unit -> int = <fun>
Now let's test our function!
next ();;
next ();;
next ();;
- : int = 1
- : int = 2
- : int = 3
Our counter is counting! Now that we've seen that it's working, lets simplify it. The standard library includes two functions, incr and decr that increment and decrement a function respectfully.
let next () =
incr counter;
!counter
val next : unit -> int = <fun>
next ();;
next ();;
- : int = 4
- : int = 5
What if we were to define counter in our function? Let's do it.
let next () =
let counter = ref 0 in
incr counter;
!counter
val next : unit -> int = <fun>
next ();;
next ();;
next ();;
- : int = 1
- : int = 1
- : int = 1
Uh oh... seems like we broke something. That's because when our let expression ends, the counter ref goes out of scope; however, there's one other thing we can try.
let next =
let counter = ref 0 in
fun () ->
incr counter;
!counter
val next : unit -> int = <fun>
To understand this, let's think back to our evaluation rules for let-expressions.
First, we evaluate the value of the let expression, then we replace that value with the evaluated value. This works, because we're replacing counter with the value that ref 0 evaluates to, the location in memory, so when we pass counter to incr or ( ! ), we're actually passing the value that ref 0 evaluates to, the location in memory.
next ();;
next ();;
next ();;
- : int = 1
- : int = 2
- : int = 3
The call to ref 0 only gets evaluated once, never again.