Explore Clojure's powerful collection manipulation functions, including conj, assoc, dissoc, update, merge, into, get, contains?, and keys/vals. Learn how these functions interact with lists, vectors, maps, and sets, with examples and comparisons to Java.
In Clojure, collections are immutable and persistent, meaning that any operation on a collection returns a new collection rather than modifying the original. This immutability is a cornerstone of functional programming, offering benefits such as thread safety and easier reasoning about code. For Java developers, accustomed to mutable collections, this shift can be both challenging and liberating. In this section, we’ll explore Clojure’s collection manipulation functions, drawing parallels to Java where applicable, and providing examples to illustrate their use.
Clojure provides several core collection types:
HashMap
.HashSet
.Each collection type has specific functions optimized for its structure, but many functions are polymorphic, meaning they can operate on multiple types of collections.
Let’s delve into some of the most commonly used functions for manipulating collections in Clojure.
conj
The conj
function is used to add elements to a collection. Its behavior varies slightly depending on the collection type:
;; Adding to a list
(def my-list '(1 2 3))
(def new-list (conj my-list 0))
;; new-list => (0 1 2 3)
;; Adding to a vector
(def my-vector [1 2 3])
(def new-vector (conj my-vector 4))
;; new-vector => [1 2 3 4]
;; Adding to a set
(def my-set #{1 2 3})
(def new-set (conj my-set 4))
;; new-set => #{1 2 3 4}
In Java, adding elements to collections typically involves methods like add
or put
, which mutate the collection. In Clojure, conj
returns a new collection, preserving immutability.
assoc
The assoc
function is used to associate a key with a value in a map or to update an element at a specific index in a vector.
;; Associating a key with a value in a map
(def my-map {:a 1 :b 2})
(def new-map (assoc my-map :c 3))
;; new-map => {:a 1, :b 2, :c 3}
;; Updating an element in a vector
(def my-vector [1 2 3])
(def updated-vector (assoc my-vector 1 10))
;; updated-vector => [1 10 3]
In Java, updating a map involves using put
, which modifies the map in place. Clojure’s assoc
returns a new map with the updated key-value pair.
dissoc
The dissoc
function removes a key from a map.
(def my-map {:a 1 :b 2 :c 3})
(def smaller-map (dissoc my-map :b))
;; smaller-map => {:a 1, :c 3}
Java’s remove
method on maps is similar, but again, it mutates the original map, whereas dissoc
returns a new map.
update
The update
function applies a function to the value associated with a key in a map.
(def my-map {:a 1 :b 2})
(def updated-map (update my-map :b inc))
;; updated-map => {:a 1, :b 3}
This is akin to retrieving a value from a map, modifying it, and then putting it back, but update
does this in a single, atomic operation.
merge
The merge
function combines multiple maps into one.
(def map1 {:a 1 :b 2})
(def map2 {:b 3 :c 4})
(def merged-map (merge map1 map2))
;; merged-map => {:a 1, :b 3, :c 4}
In Java, merging maps typically involves iterating over one map and inserting its entries into another. Clojure’s merge
simplifies this process.
into
The into
function is used to add all elements from one collection into another.
(def vector1 [1 2 3])
(def vector2 [4 5 6])
(def combined-vector (into vector1 vector2))
;; combined-vector => [1 2 3 4 5 6]
This is similar to Java’s addAll
method for collections, but into
works with any Clojure collection type.
get
The get
function retrieves the value associated with a key in a map or an index in a vector.
(def my-map {:a 1 :b 2})
(def value (get my-map :a))
;; value => 1
(def my-vector [10 20 30])
(def element (get my-vector 1))
;; element => 20
In Java, you would use get
on a map or get
on a list, but Clojure’s get
is polymorphic and works across different collection types.
contains?
The contains?
function checks if a map contains a specific key or if a set contains a specific element.
(def my-map {:a 1 :b 2})
(def has-key (contains? my-map :a))
;; has-key => true
(def my-set #{1 2 3})
(def has-element (contains? my-set 2))
;; has-element => true
Java’s containsKey
and contains
methods are similar, but contains?
is more versatile.
keys
and vals
The keys
and vals
functions return the keys and values of a map, respectively.
(def my-map {:a 1 :b 2 :c 3})
(def map-keys (keys my-map))
;; map-keys => (:a :b :c)
(def map-vals (vals my-map))
;; map-vals => (1 2 3)
In Java, you would use keySet
and values
methods on a map to achieve similar results.
Let’s compare how some of these operations differ between Clojure and Java:
// Java: Adding elements to a list
List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3));
list.add(0, 0); // Modifies the list in place
// Clojure: Adding elements to a list
(def my-list '(1 2 3))
(def new-list (conj my-list 0)) ; Returns a new list
// Java: Updating a map
Map<String, Integer> map = new HashMap<>();
map.put("a", 1);
map.put("b", 2);
map.put("c", 3); // Modifies the map in place
// Clojure: Updating a map
(def my-map {:a 1 :b 2})
(def new-map (assoc my-map :c 3)) ; Returns a new map
Experiment with the following code snippets to deepen your understanding of Clojure’s collection manipulation functions:
conj
: Try adding multiple elements at once to a vector or list.assoc
with vectors: Update multiple indices in a vector.merge
: Merge more than two maps and observe the behavior when keys overlap.into
: Use into
to combine different types of collections, such as a set into a vector.Below is a diagram illustrating how conj
behaves differently with lists and vectors:
graph TD; A["Original List: (1 2 3)"] -->|conj 0| B["New List: (0 1 2 3)"]; C[Original Vector: [1 2 3]] -->|conj 4| D[New Vector: [1 2 3 4]];
Diagram 1: The conj
function adds elements to the front of a list and the end of a vector.
assoc
.conj
, assoc
, and merge
allow for expressive and concise manipulation of collections.For further reading, explore the Official Clojure Documentation and ClojureDocs.