Skip to content
AgileLogics
7 min read

Working with Collections

Prerequisites: Arrays and sequences

Instead of writing loops, F# code transforms collections with functions from the List (or Array, or Seq) module.

map, filter, and friends

let numbers = [5; 3; 8; 1; 9; 2]

printfn "%A" (numbers |> List.map (fun n -> n * 2))
printfn "%A" (numbers |> List.filter (fun n -> n > 4))
printfn "%A" (numbers |> List.sort)
printfn "%A" (numbers |> List.sortDescending)
printfn "%d %d %d" (List.sum numbers) (List.max numbers) (List.min numbers)
printfn "%.2f" (numbers |> List.map float |> List.average)
printfn "%b" (numbers |> List.exists (fun n -> n > 8))
printfn "%b" (numbers |> List.forall (fun n -> n > 0))
printfn "%A" (numbers |> List.truncate 3)
printfn "%A" (List.rev numbers)
[10; 6; 16; 2; 18; 4]
[5; 8; 9]
[1; 2; 3; 5; 8; 9]
[9; 8; 5; 3; 2; 1]
28 9 1
4.67
true
true
[5; 3; 8]
[2; 9; 1; 8; 3; 5]

Working with records of data

Real data is usually a list of tuples or records. The By functions take a function that picks what to compare, sum, or group on:

let sales =
    [ ("Ada", "North", 120)
      ("Grace", "South", 340)
      ("Linus", "North", 90)
      ("Margaret", "South", 210) ]

let amount (_, _, a) = a
let region (_, r, _) = r

printfn "Total: %d" (sales |> List.sumBy amount)
printfn "Top: %A" (sales |> List.maxBy amount)
printfn "%A" (sales |> List.sortByDescending amount |> List.map (fun (n, _, _) -> n))

sales
|> List.groupBy region
|> List.iter (fun (r, rows) -> printfn "%s: %d" r (List.sumBy amount rows))

printfn "%A" (sales |> List.countBy region)
Total: 760
Top: ("Grace", "South", 340)
["Grace"; "Margaret"; "Ada"; "Linus"]
North: 210
South: 550
[("North", 2); ("South", 2)]

find and tryFind

let words = ["apple"; "banana"; "cherry"]

printfn "%s" (words |> List.find (fun w -> w.StartsWith "b"))
printfn "%A" (words |> List.tryFind (fun w -> w.StartsWith "z"))
printfn "%A" (words |> List.tryFindIndex (fun w -> w = "cherry"))
banana
None
Some 2

find throws an exception when nothing matches; tryFind returns an option — Some value or None — which you'll learn about shortly.

fold and reduce

fold walks a list carrying an accumulator — the general tool for "combine everything into one result":

let numbers = [1; 2; 3; 4]

let total = numbers |> List.fold (fun acc n -> acc + n) 0
let product = numbers |> List.reduce (*)
let sentence = ["F#"; "is"; "fun"] |> List.fold (fun acc w -> acc + " " + w) ">"

printfn "%d %d %s" total product sentence
printfn "%A" (numbers |> List.scan (+) 0)
10 24 > F# is fun
[0; 1; 3; 6; 10]

reduce uses the first item as the starting value (and fails on an empty list); scan is like fold but keeps every intermediate result.

More useful functions

printfn "%A" (List.zip [1; 2; 3] ["a"; "b"; "c"])
printfn "%A" (List.distinct [1; 2; 2; 3; 1])
printfn "%A" (List.chunkBySize 2 [1 .. 5])
printfn "%A" (List.pairwise [1; 2; 3])
printfn "%A" (List.partition (fun n -> n % 2 = 0) [1 .. 6])
printfn "%A" (List.collect (fun n -> [n; n * 10]) [1; 2])
printfn "%A" (List.indexed ["a"; "b"])
[(1, "a"); (2, "b"); (3, "c")]
[1; 2; 3]
[[1; 2]; [3; 4]; [5]]
[(1, 2); (2, 3)]
([2; 4; 6], [1; 3; 5])
[1; 10; 2; 20]
[(0, "a"); (1, "b")]

Practice

Given the words of "the cat and the hat and the bat", print each distinct word with its count, most frequent first.

Show solution
"the cat and the hat and the bat".Split ' '
|> Array.toList
|> List.countBy id
|> List.sortByDescending snd
|> List.iter (fun (word, count) -> printfn "%s: %d" word count)
the: 3
and: 2
cat: 1
hat: 1
bat: 1

id is the identity function (fun x -> x). List.sortByDescending is stable, so words with the same count keep their first-seen order.