Skip to content

Search content

From F# to Clojure > 2: Immutable Data Without Static Types

From F# to Clojure Part 2
A typed F# record card unfolds into reusable Clojure vector, map, and set shapes.

F# and Clojure both make immutable data the comfortable path. The larger shift is from F# records and statically checked fields to a small set of generic persistent collections.

Table of contents

On this page

Collection Literals

IntentF#Clojure
Persistent indexed dataNo direct core equivalent[:read :eval :print]
Linked list[ "read"; "eval"; "print" ]'(:read :eval :print)
Key-value dataMap [ "name", "Ada" ]{:name "Ada" :active? true}
Unique valuesset [ "clojure"; "lisp" ]#{:clojure :lisp}

Vectors are the usual default when order matters. Lists are especially important because Clojure code itself is represented with lists.

Reading Nested Data

Keywords can look themselves up in a map:

profile.Name
// "Ada"

profile.Skills |> List.item 1
// "Clojure"F#
(:name profile)
;; => "Ada"

(get-in profile [:skills 1])
;; => "Clojure"Clojure

Use get for one lookup and get-in for a path through nested maps and vectors. A missing key returns nil; there is no F# compiler error or automatic Option, so validate maps at system boundaries when their shape matters.

Updating Without Mutation

F# record-copy syntax and Clojure’s assoc both return new values. update and conj generalize the idea across Clojure’s collection types:

let activated = { profile with Active = true }
let experienced =
    { activated with Skills = activated.Skills @ [ "Java" ] }F#
(def activated (assoc profile :active? true))
(def experienced (update activated :skills conj "Java"))

(:active? profile)
;; => false

(:active? experienced)
;; => true

(:skills experienced)
;; => ["Lisp" "Clojure" "Java"]Clojure

conj adds a value in the natural place for a collection: at the end of a vector, at the front of a list, and anywhere in a set.

Try It

Restock the apples while keeping the original inventory:

let inventory = Map [ "apples", 2; "pears", 3 ]
let restocked =
    inventory
    |> Map.change "apples" (Option.map (fun count -> count + 4))

inventory["apples"]
// 2

restocked["apples"]
// 6F#
(def inventory {:apples 2 :pears 3})
(def restocked (update inventory :apples + 4))

(:apples inventory)
;; => 2

(:apples restocked)
;; => 6Clojure

The old value remains available, so functions can safely share data without coordinating who is allowed to change it.


Share this post

Previous in series
From F# to Clojure > 1: Forms and the REPL
Next in series
From F# to Clojure > 3: Functions, Bindings, and Destructuring