Reading — step 1 of 7
Learn
F# is functional first but has full support for imperative code. Mutable bindings, refs, arrays, and loops are all available — use sparingly, where they fit. Microsoft's F# docs explicitly call F# 'functional first, but multi-paradigm.'
Mutable bindings
let mutable counter = 0
counter <- counter + 1 // assignment with <-
printfn "%d" counter // 1
let mutabledeclares a mutable binding<-assigns a new value (NOT=)
Without mutable, let counter = 0 is immutable; later assignment is a type error.
ref cells
The older imperative style:
let counter = ref 0
incr counter // counter := !counter + 1
printfn "%d" !counter // 1 — dereference with !
ref initialcreates a mutable cell!rdereferencesr := valueassignsincr r/decr rfor int refs
In modern F#, let mutable is usually preferred over ref for simple mutation. ref cells are useful when you need to pass a mutable cell around as a value.
Arrays — fixed-size mutable
let arr = [| 1; 2; 3; 4; 5 |] // | brackets, semicolons inside
arr.[0] // 1 — read with .[i]
arr.[2] <- 99 // assign
arr.Length // 5
Arrays are MUTABLE and FIXED-SIZE — different from lists (immutable, linked). The Array module mirrors List:
Array.map (fun x -> x * 2) [|1; 2; 3|]
Array.filter (fun x -> x > 2) [|1; 2; 3; 4|]
Array.sum [|1; 2; 3|]
Array.length arr
When to use arrays vs lists
- List — immutable, easy to prepend/recurse
- Array — fixed size, fast random access (O(1) by index), in-place updates
- ResizeArray — mutable growable list (.NET List<T>)
- Seq — lazy sequence (similar to IEnumerable)
Most F# code uses lists. Arrays for performance-critical numeric work.
for and while loops
for i in 1..5 do
printfn "%d" i
for i = 1 to 5 do // alternative syntax
printfn "%d" i
for i = 5 downto 1 do // descending
printfn "%d" i
let mutable n = 0
while n < 5 do
printfn "%d" n
n <- n + 1
Loops execute their bodies for side effects. The body must have type unit.
for-in over collections
for x in [1..5] do
printfn "%d" x
for x in [|1; 2; 3|] do
printfn "%d" x
for (k, v) in Map.ofList ["a", 1; "b", 2] do
printfn "%s=%d" k v
Works on any IEnumerable<T>. Pattern matching in the loop binding (like the (k, v) destructuring) works too.
Mutable record fields
type Counter = { mutable Count: int }
let c = { Count = 0 }
c.Count <- c.Count + 1 // mutate the field
The mutable keyword allows in-place modification with <-. Other fields stay immutable.
Dictionary and ResizeArray
For mutable .NET-style collections:
open System.Collections.Generic
let dict = Dictionary<string, int>()
dict.["alice"] <- 30
dict.["bob"] <- 25
dict.ContainsKey("alice") // true
let list = ResizeArray<int>()
list.Add(1)
list.Add(2)
list.[0] // 1
list.RemoveAt(0)
Dictionary<TKey, TValue> and ResizeArray<T> are .NET BCL types. F# bindings use <- for assignment.
When to mix functional and imperative
Yes:
- Performance-critical inner loops
- Hash tables for fast lookup
- .NET interop (the BCL is mostly imperative)
- Implementing stateful APIs
No:
- Default coding style — stay functional where you can
- Public API surface
- Where pattern matching + recursion is clearer
Common mistakes
=vs<-—=is comparison;<-is assignment. Mixing them is a common newcomer error.- Forgetting
mutable— without it,let x = 0; x <- 1fails to compile. !vs ref —!dereferences ref cells.let mutabledoesn't need!.- Array
[| |]vs list[ ]— different brackets. Mix the syntax and the compiler complains. arr.[i]vsarr[i]— F# uses dotted indexingarr.[i]. Bracket-less is being added butarr.[i]is the safe form.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…