Mutability and Imperative Control Flow
Imperative and functional programming both have unique merits, and OCaml allows combining them efficiently. In the first part of this tutorial, we introduce mutable state and imperative control flow. See the second part for examples of recommended or discouraged use of these features.
Immutable vs Mutable Data
When you use let … = …
to bind a value to a name, this name-value binding is immutable, so it is impossible to mutate (which is a fancy term for “change,” “update,” or “modify”) the value assigned to the name.
In the following sections, we introduce OCaml's language features for dealing with mutable states.
References
There is a special kind of value, called reference, whose contents can be updated:
# let d = ref 0;;
val d : int ref = {contents = 0}
# d := 1;;
- : unit = ()
# !d;;
- : int = 1
Here is what happens in this example:
- The value
{ contents = 0 }
is bound to the named
. This is a normal definition. Like any other definition, it is immutable. However, the value0
in thecontents
field ofd
is mutable, so it can be updated. - The assignment operator
:=
is used to update the mutable value insided
from0
to1
. - The dereference operator
!
reads the contents of the mutable value insided
.
The ref
identifier above refers to two different things:
- The function
ref : 'a -> 'a ref
that creates a reference - The type of mutable references:
'a ref
Assignment Operator
# ( := );;
- : 'a ref -> 'a -> unit = <fun>
The assignment operator :=
is just a function. It takes
- the reference to be updated, and
- the value that replaces the previous contents.
The update takes place as a side effect, and the value ()
is returned.
Dereference Operator
# ( ! );;
- : 'a ref -> 'a = <fun>
The dereference operator is a function that takes a reference and returns its contents.
Refer to the Operators documentation for more information on how unary and binary operators work in OCaml.
When working with mutable data in OCaml,
- it is impossible to create uninitialised references, and
- the mutable content and the reference have different syntax and type: no confusion between them is possible.
Mutable Record Fields
Any field in a record can be tagged using the mutable
keyword. Such a field can be updated.
# type book = {
series : string;
volume : int;
title : string;
author : string;
mutable stock : int;
};;
type book = {
series : string;
volume : int;
title : string;
author : string;
mutable stock : int;
}
For instance, here is how a bookshop could track its inventory:
- Fields
title
,author
,volume
,series
are constants. - Field
stock
is mutable because this value changes with each sale or restocking.
Such a database should have an entry like this:
# let vol_7 = {
series = "Murderbot Diaries";
volume = 7;
title = "System Collapse";
author = "Martha Wells";
stock = 0
};;
val vol_7 : book =
{series = "Murderbot Diaries"; volume = 7; title = "System Collapse";
author = "Martha Wells"; stock = 0}
When the bookshop receives a delivery of 10 of these books, we update the mutable stock
field:
# vol_7.stock <- vol_7.stock + 10;;
- : unit = ()
# vol_7;;
- : book =
{series = "Murderbot Diaries"; volume = 7; title = "System Collapse";
author = "Martha Wells"; stock = 10 }
Mutable record fields are updated using the left arrow symbol <-
. In the expression vol_7.stock <- vol_7.stock + 10
, the meaning of vol_7.stock
depends on its context:
- In the left-hand side of
<-
, it refers to the mutable field to be updated. - In the right-hand side of
<-
, it denotes the contents of the mutable field.
In contrast to references, there is no special syntax to dereference a mutable record field.
Remark: References Are Single Field Records
In OCaml, references are records with a single mutable field:
# #show_type ref;;
type 'a ref = { mutable contents : 'a; }
The type 'a ref
is a record with a single field contents
, which is marked with the mutable
keyword.
Since references are single field records, we can define functions create
, assign
, and deref
using the mutable record field update syntax:
# let create v = { contents = v };;
val create : 'a -> 'a ref = <fun>
# let assign f v = f.contents <- v;;
val assign : 'a ref -> 'a -> unit = <fun>
# let deref f = f.contents;;
val deref : 'a ref -> 'a = <fun>
# let f = create 0;;
val f : int ref = {contents = 0}
# deref f;;
- : int = 0
# assign f 2;;
- : unit = ()
# deref f;;
- : int = 2
The functions:
create
does the same as theref
function provided by the standard library.assign
does the same as the( := )
operator.deref
does the same as the( ! )
operator.
Arrays
In OCaml, an array is a mutable, fixed-size data structure that can store a sequence of elements of the same type. Arrays are indexed by integers, provide constant-time access, and allow the update of elements.
# let g = [| 2; 3; 4; 5; 6; 7; 8 |];;
val g : int array = [|2; 3; 4; 5; 6; 7; 8|]
# g.(0);;
- : int = 2
# g.(0) <- 9;;
- : unit = ()
# g.(0);;
- : int = 9
The left arrow symbol <-
is used to update an array element at a given index. The array index access syntax g.(i)
, where g
is a value of type array
and i
is an integer, stands for either
- the array location to update (when on the left-hand side of
<-
), or - the cell's content (when on the right-hand side of
<-
).
For a more detailed discussion on arrays, see the Arrays tutorial.
Byte Sequences
The bytes
type in OCaml represents a fixed-length, mutable byte sequence. In a value of type bytes
, each element has 8 bits. Since characters in OCaml are represented using 8 bits, bytes
values are mutable char
sequences. Like arrays, byte sequences support indexed access.
# let h = Bytes.of_string "abcdefghijklmnopqrstuvwxyz";;
val h : bytes = Bytes.of_string "abcdefghijklmnopqrstuvwxyz"
# Bytes.get h 10;;
- : char = 'k'
# Bytes.set h 10 '_';;
- : unit = ()
# h;;
- : bytes = Bytes.of_string "abcdefghij_lmnopqrstuvwxyz"
Byte sequences can be created from string
values using the function Bytes.of_string
. Individual elements in the sequence can be updated or read by their index using Bytes.set
and Bytes.get
.
You can think of byte sequences as either:
- updatable strings that can't be printed, or
char
arrays without syntactic sugar for indexed read and update.
Note: the bytes
type uses a much more compact memory representation than char array
. As of writing this tutorial, there is an 8-factor between bytes`` and
char array. The former should always be preferred, except when
array` is required by polymorphic functions handling arrays.
get_char
Function
Example: In this section, we compare two ways to implement a get_char
function. The function waits until a key is pressed and returns the corresponding character without echoing it. This function will also be used later on in this tutorial.
We use two functions from the Unix
module to read and update attributes of the terminal associated with standard input:
tcgetattr stdin TCSAFLUSH
returns the terminal attributes as a record (similar toderef
)tcsetattr stdin TCSAFLUSH
updates the terminal attributes (similar toassign
)
These attributes need to be set correctly (i.e., turn off echoing and disable canonical mode) in order to read it the way we want. The logic is the same in both implementations:
- Read and record the terminal attributes
- Set the terminal attributes
- Wait until a key is pressed, read it as a character
- Restore the initial terminal attributes
- Return the read character
We read characters from standard input using the input_char
function from the OCaml standard library.
Below is the first implementation. If you're working in macOS, run #require "unix";;
first to avoid an Unbound module error
.
# let get_char () =
let open Unix in
let termio = tcgetattr stdin in
let c_icanon, c_echo = termio.c_icanon, termio.c_echo in
termio.c_icanon <- false;
termio.c_echo <- false;
tcsetattr stdin TCSAFLUSH termio;
let c = input_char (in_channel_of_descr stdin) in
termio.c_icanon <- c_icanon;
termio.c_echo <- c_echo;
tcsetattr stdin TCSAFLUSH termio;
c;;
val get_char : unit -> char = <fun>
In this implementation, we update the fields of termio
- before
input_char
, setting bothc_icanon
andc_echo
tofalse
, and - after
input_char
, restoring the initial values.
Here is the second implementation:
# let get_char () =
let open Unix in
let termio = tcgetattr stdin in
tcsetattr stdin TCSAFLUSH
{ termio with c_icanon = false; c_echo = false };
let c = input_char (in_channel_of_descr stdin) in
tcsetattr stdin TCSAFLUSH termio;
c;;
val get_char : unit -> char = <fun>
In this implementation, the record returned by the call to tcgetattr
is not modified. A copy is made using { termio with c_icanon = false; c_echo = false }
. This copy only differs from the termio
value on fields c_icanon
and c_echo
.
In the second call to tcsetattr
, we restore the terminal attributes to their initial state.
Imperative Control Flow
OCaml allows you to evaluate expressions in sequence and provides for
and while
loops to execute a block of code repeatedly.
Evaluating Expressions in Sequence
let … in
# let () = print_string "This is" in print_endline " really Disco!";;
This is really Disco!
- : unit = ()
Using the let … in
construct means two things:
- Names may be bound. In the example, no name is bound since
()
is used. - Side effects take place in sequence. The bound expression (
print_string "This is"
) is evaluated first, and the referring expression (print_endline " really Disco!"
) is evaluated second.
Semicolon
The single semicolon ;
operator is known as the sequence operator. It allows you to evaluate multiple expressions in order, with the last expression's value as the entire sequence's value.
The values of any previous expressions are discarded. Thus, it makes sense to use expressions with side effects, except for the last expression of the sequence, which could be free of side effects.
# let _ =
print_endline "Hello,";
print_endline "world!";
42;;
Hello,
world!
- : int = 42
In this example, the first two expressions are print_endline
function calls, which produce side effects (printing to the console), and the last expression is simply the integer 42
, which becomes the value of the entire sequence. The ;
operator is used to separate these expressions.
Remark Even though it's called the sequence operator, the semicolon is not truly an operator because it is not a function of type unit -> 'a -> 'a
. It is rather a construct of the language. It allows adding a semicolon at the end of a sequence expression.
# (); 42; ;;
- : int = 42
Here, the semicolon after 42 is ignored.
begin … end
expressions
In OCaml, begin … end
and parentheses are the same.
Imagine we want to write a function that:
- Has an
int
reference parameter containing value n - Updates the reference's contents to 2 × (n + 1)
This is arguably convoluted and does not work:
# let f r = r := incr r; 2 * !r;;
Error: This expression has type unit but an expression was expected of type int
But here is how it can be made to work:
# let f r = r := begin incr r; 2 * !r end;;
val f : int ref -> unit = <fun>
The error came from assign :=
, which associates stronger than a semicolon ;
. Here is what we want to do, in order:
- Increment
r
- Compute
2 * !r
- Assign into
r
Remember the value of a semicolon-separated sequence is the value of its last expression. Grouping the first two steps with begin … end
fixes the error.
Fun fact: begin … end
and parentheses are literally the same:
# begin end;;
- : unit = ()
if … then … else …
and Side Effects
In OCaml, if … then … else …
is an expression.
# 6 * if "foo" = "bar" then 5 else 5 + 2;;
- : int = 42
A conditional expression return type can be unit
if both branches are too.
# if 0 = 1 then print_endline "foo" else print_endline "bar";;
bar
- : unit = ()
The above can also be expressed this way:
# print_endline (if 0 = 1 then "foo" else "bar");;
bar
- : unit = ()
The unit
value ()
can serve as a no-op when only one branch has something to execute.
# if 0 = 1 then print_endline "foo" else ();;
- : unit = ()
But OCaml also allows writing if … then …
expressions without an else
branch, which is the same as the above.
# if 0 = 1 then print_endline "foo";;
- : unit = ()
In parsing, conditional expressions groups more than sequencing:
# if true then print_endline "A" else print_endline "B"; print_endline "C";;
A
C
- : unit = ()
Here ; print_endline "C"
is executed after the whole conditional expression, not after print_endline "B"
.
If you want to have two prints in a conditional expression branch, use begin … end
.
# if true then
print_endline "A"
else begin
print_endline "B";
print_endline "C"
end;;
A
- : unit = ()
Here is an error you might encounter:
# if true then
print_endline "A";
print_endline "C"
else
print_endline "B";;
Error: Syntax error
Failing to group in the first branch results in a syntax error. What's before the semicolon is parsed as an if … then …
without an else
expression. What's after the semicolon appears as a dangling else
.
For Loop
A for
loop is an expression of type unit
. Here, for
, to
, do
, and done
are keywords.
# for i = 0 to 5 do Printf.printf "%i\n" i done;;
0
1
2
3
4
5
- : unit = ()
Here:
i
is the loop counter; it is incremented after every iteration.0
is the first value ofi
.5
is the last value ofi
.- The expression
Printf.printf "%i\n" i
is the body of the loop.
The iteration evaluates the body expression (which may contain i
) until i
reaches 5
.
The body of a for
loop must be an expression of type unit
:
# let j = [| 2; 3; 4; 5; 6; 7; 8 |];;
val j : int array = [|2; 3; 4; 5; 6; 7; 8|]
# for i = Array.length j - 1 downto 0 do 0 done;;
Line 1, characters 39-40:
Warning 10 [non-unit-statement]: this expression should have type unit.
- : unit = ()
When you use the downto
keyword (instead of the to
keyword), the counter decreases on every iteration of the loop.
for
loops are convenient to iterate over and modify arrays:
# let sum = ref 0 in
for i = 0 to Array.length j - 1 do sum := !sum + j.(i) done;
!sum;;
- : int = 35
Note: Here is how to do the same thing using an iterator function:
# let sum = ref 0 in Array.iter (fun i -> sum := !sum + i) j; !sum;;
- : int = 35
While Loop
A while
loop is an expression of type unit
. Here, while
, do
, and done
are keywords.
# let i = ref 0 in
while !i <= 5 do
Printf.printf "%i\n" !i;
i := !i + 1;
done;;
0
1
2
3
4
5
- : unit = ()
Here:
!i <= 5
is the condition.- The expression
Printf.printf "%i\n" !i; i := !i + 1;
is the body of the loop.
The iteration executes the body expression as long as the condition remains true.
In this example, the while
loop continues to execute as long as the value held by the reference i
is less than 5
.
Breaking Loops Using Exceptions
Throwing the Exit
exception is a recommended way to immediately exit from a loop.
The following example uses the get_char
function we defined earlier (in the section Example: get_char
Function).
# try
print_endline "Press Escape to exit";
while true do
let c = get_char () in
if c = '\027' then raise Exit;
print_char c;
flush stdout
done
with Exit -> ();;
This while
loop echoes characters typed on the keyboard. When the ASCII Escape
character is read, the Exit
exception is thrown, which terminates the iteration and displays the REPL reply: - : unit = ()
.
References Inside Closures
In the following example, the function create_counter
returns a closure that hides a mutable reference n
. This closure captures the environment where n
is defined and can modify n
each time it's invoked. The n
reference is "hidden" within the closure, encapsulating its state.
# let create_counter () =
let n = ref 0 in
fun () -> incr n; !n;;
val create_counter : unit -> unit -> int = <fun>
First, we define a function named create_counter
that takes no arguments. Inside create_counter
, a reference n
is initialised with the value 0. This reference will hold the state of the counter. Next, we define a closure that takes no arguments (fun () ->). The closure increments the value of n
(the counter) using incr n
, then returns the current value of n
using !n
.
# let c1 = create_counter ();;
val c1 : unit -> int = <fun>
# let c2 = create_counter ();;
val c2 : unit -> int = <fun>
Now, we shall create a closure c1
that encapsulates a counter. Calling c1 ()
will increment the counter associated with c1
and return its current value. Similarly, we create another closure c2
with its own independent counter.
# c1 ();;
- : int = 1
# c1 ();;
- : int = 2
# c2 ();;
- : int = 1
# c1 ();;
- : int = 3
Calling c1 ()
increments the counter associated with c1
and returns its current value. Since this is the first call, the counter starts at 1. Another call to c1 ()
increments the counter again, so it returns 2.
Calling c2 ()
increments the counter associated with c2
. Since c2
has its own independent counter, it starts at 1. Another call to c1 ()
increments its counter, resulting in 3.
Recommendations for Mutable State and Side Effects
Functional and imperative programming styles are often used together. However, not all ways of combining them give good results. We show some patterns and anti-patterns relating to mutable states and side effects in this section.
Good: Function-Encapsulated Mutability
Here is a function that computes the sum of an array of integers.
# let sum m =
let result = ref 0 in
for i = 0 to Array.length m - 1 do
result := !result + m.(i)
done;
!result;;
val sum : int array -> int = <fun>
The function sum
is written in an imperative style, using mutable data structures and a for
loop. However, no mutability is exposed. It is a fully encapsulated implementation choice. This function is safe to use; no problems are to be expected.
Good: Application-Wide State
Some applications maintain some state while they are running. Here are a couple of examples:
- A Read-Eval-Print-Loop (REPL). The state is the environment where values are bound to names. In OCaml, the environment is append-only, but some other languages allow replacing or removing name-value bindings.
- A server for a stateful protocol. Each session has a state. The global state consists of all the session states.
- A text editor. The state includes the most recent commands (to allow undo), the state of any open files, the settings, and the state of the UI.
- A cache.
The following is a toy line editor, using the get_char
function defined earlier. It waits for characters on standard input and exits on end-of-file, carriage return, or newline. Otherwise, if the character is printable, it prints it and records it in a mutable list used as a stack. If the character is the delete code, the stack is popped and the last printed character is erased.
# let record_char state c =
(String.make 1 c, c :: state);;
val record_char : char list -> char -> string * char list = <fun>
# let remove_char state =
("\b \b", if state = [] then [] else List.tl state);;
val remove_char : 'a list -> string * 'a list = <fun>
# let state_to_string state =
List.(state |> rev |> to_seq |> String.of_seq);;
val state_to_string : char list -> string = <fun>
# let rec loop state =
let c = get_char () in
if c = '\004' || c = '\n' || c = '\r' then raise Exit;
let s, new_state = match c with
| '\127' -> remove_char !state
| c when c >= ' ' -> record_char !state c
| _ -> ("", !state) in
print_string s;
state := new_state;
flush stdout;
loop state;;
val loop : char list ref -> 'a = <fun>
# let state = ref [] in try loop state with Exit -> state_to_string !state;;
After this last command, you can type and edit any single line of text. Then, press return to get back to the REPL.
This example illustrates the following:
- The functions
record_char
andremove_char
neither update the state nor produce side effects. Instead, they each return a pair of values consisting of a string to print and the next state,new_state
. - I/O and state-update side effects happen inside the
loop
function. - The state is passed as argument to the
loop
function.
This is a possible way to handle an application-wide state. As in the Function-Encapsulated Mutability example, state-aware code is contained in a narrow scope; the rest of the code is purely functional.
Note: Here, the state is copied, which is not memory efficient. In a memory-aware implementation, state-update functions would produce a “diff” (data describing the difference between the state's old and updated version).
Good: Precomputing Values
Let's imagine you store angles as fractions of the circle in 8-bit unsigned integers, storing them as char
values. In this system, 64 is 90 degrees, 128 is 180 degrees, 192 is 270 degrees, 256 is full circle, and so on. If you need to compute cosine on those values, an implementation might look like this:
# let char_cos c =
c |> int_of_char |> float_of_int |> ( *. ) (Float.pi /. 128.0) |> cos;;
val char_cos : char -> float = <fun>
However, it is possible to make a faster implementation by precomputing all the possible values in advance. There are only 256 of them, which you'll see listed after the first result below:
# let char_cos_tab = Array.init 256 (fun i -> i |> char_of_int |> char_cos);;
val char_cos_tab : float array =
# let char_cos c = char_cos_tab.(int_of_char c);;
val char_cos : char -> float = <fun>
Good: Memoization
The memoization technique relies on the same idea as the previous section's example: lookup results from a table of previously computed values.
However, instead of precomputing everything, memoization uses a cache that is populated when calling the function. Either, the provided arguments
- are found in the cache (it is a hit) and the stored result is returned, or they
- are not found in the cache (it's a miss), and the result is computed, stored in the cache, and returned.
You can find a concrete example of memoization and a more in-depth explanation in the chapter on Memoization of "OCaml Programming: Correct + Efficient + Beautiful."
Good: Functional by Default
By default, OCaml programs should be written in a mostly functional style. This constitutes trying to avoid side effects where possible and relying on immutable data instead of mutable state.
It is possible to use an imperative programming style without losing the benefits of type and memory safety. However, it doesn't usually make sense to only program in an imperative style. Not using functional programming idioms at all would result in non-idiomatic OCaml code.
Most existing modules provide an interface meant to be used in a functional way. Some would require the development and maintenance of wrapper libraries to be used in an imperative setting and such use would in many cases be inefficient.
It Depends: Module State
A module may expose or encapsulate a state in several different ways:
- Good: expose a type representing a state, with state creation or reset functions
- It depends: only expose state initialisation, which implies there only is a single state
- Bad: mutable state with no explicit initialisation function or no name referring to the mutable state
For example, the Hashtbl
module provides an interface of the first kind. It has the type Hashtbl.t
representing mutable data. It also exposes create
, clear
, and reset
functions. The clear
and reset
functions return unit
. This strongly signals the reader that they perform the side-effect of updating the mutable data.
# #show Hashtbl.t;;
type ('a, 'b) t = ('a, 'b) Hashtbl.t
# Hashtbl.create;;
- : ?random:bool -> int -> ('a, 'b) Hashtbl.t = <fun>
# Hashtbl.reset;;
- : ('a, 'b) Hashtbl.t -> unit = <fun>
# Hashtbl.clear;;
- : ('a, 'b) Hashtbl.t -> unit = <fun>
On the other hand, a module may define mutable data internally impacting its behaviour without exposing it in its interface. This is inadvisable.
Bad: Undocumented Mutation
Here's an example of bad code:
# let partition p k =
let m = Array.copy k in
let k_len = ref 0 in
let m_len = ref 0 in
for i = 0 to Array.length k - 1 do
if p k.(i) then begin
k.(!k_len) <- k.(i);
incr k_len
end else begin
m.(!m_len) <- k.(i);
incr m_len
end
done;
(Array.truncate k_len k, Array.truncate m_len m);;
Error: Unbound value Array.truncate
Note: This example will not run in the REPL, since the function Array.truncate
is not defined.
To understand why this is bad code, assume that the function Array.truncate
has type int -> 'a array -> 'a array
. It behaves such that Array.truncate 3 [5; 6; 7; 8; 9]
returns [5; 6; 7]
, and the returned array physically corresponds to the 3 first cells of the input array.
The type of partition
would be ('a -> bool) -> 'a array -> 'a array * 'a array
, and it could be documented as:
partition p k
returns a pair of arrays(m, n)
wherem
is an array containing all the elements ofk
that satisfy the predicatep
, andn
is an array containing the elements ofk
that do not satisfyp
. The order of the elements from the input array is preserved.
On first glance, this looks like an application of Function-Encapsulated Mutability. However, it is not. The input array is modified. This function has a side effect that is either
- not intended, or
- not documented.
In the latter case, the function should be named differently (e.g., partition_in_place
or partition_mut
), and the effect on the input array should be documented.
Bad: Undocumented Side Effects
Consider this code:
# module Array = struct
include Stdlib.Array
let copy a =
if Array.length a > 1000000 then Analytics.collect "Array.copy" a;
copy a
end;;
Error: Unbound module Analytics
Note: This code will not run because there is no module called Analytics
. Analytics are remote monitoring libraries.
A module called Array
is defined; it shadows and includes the Stdlib.Array
module. See the Module Inclusion part of the Modules tutorial for details about this pattern.
To understand why this code is bad, figure out that Analytics.collect
is a function that makes a network connection to transmit data to a remote server.
Now, the newly defined Array
module contains a copy
function that has a potentially unexpected side effect, but only if the array to copy has a million cells or above.
If you're writing functions with non-obvious side effects, don't shadow existing definitions. Instead, give the function a descriptive name (for instance, Array.copy_with_analytics
) and document the fact that there's a side-effect that the caller may not be aware of.
Bad: Side Effects Depending on Order of Evaluation
Consider the following code:
# let id_print s = print_string (s ^ " "); s;;
val id_print : string -> string = <fun>
# let s =
Printf.sprintf "%s %s %s"
(id_print "Monday")
(id_print "Tuesday")
(id_print "Wednesday");;
Wednesday Tuesday Monday val s : string = "Monday Tuesday Wednesday "
The function id_print
returns its input unchanged. However, it has a side effect: it first prints the string it receives as an argument.
In the second line, we apply id_print
to the arguments "Monday"
, "Tuesday"
, and "Wednesday"
. Then Printf.sprintf "%s %s %s "
is applied to the results.
Since the evaluation order for function arguments in OCaml is not explicitly defined, the order in which the id_print
side effects take place is unreliable. In this example, the arguments are evaluated from right to left, but this could change in future compiler releases.
This issue also arises when applying arguments to variant constructors, building tuple values, or initialising record fields. Here, it is illustrated on a tuple value:
# let r = ref 0 in ((incr r; !r), (decr r; !r));;
- : int * int = (0, -1)
The value of this expression depends on the order of subexpression evaluation. Since this order is not specified, there is no reliable way to know what this value is. At the time of writing this tutorial, the evaluation produced (0, -1)
, but if you see something else, it is not a bug. Such an unreliable value must a avoided.
To ensure that evaluation takes place in a specific order, use the means to put expressions in sequences. Check the Evaluating Expressions in Sequence section.
Conclusion
A mutable state is neither good nor bad. For the cases where a mutable state enables a significantly simpler implementation, OCaml provides fine tools to deal with it. We looked at references, mutable record fields, arrays, byte sequences, and imperative control flow expressions like for
and while
loops. Finally, we discussed several examples of recommended and discouraged use of side effects and mutable states.
Help Improve Our Documentation
All OCaml docs are open source. See something that's wrong or unclear? Submit a pull request.