Explore the diverse data types and structures in Clojure, including scalar and collection types, and learn how to manipulate them effectively.
Clojure, a modern Lisp dialect, offers a rich set of data types and structures that are both powerful and flexible. As experienced Java developers, you’ll find some familiar concepts, but Clojure’s approach to data is fundamentally different due to its functional nature and emphasis on immutability. In this section, we’ll explore Clojure’s scalar types, collection types, and how to work with nested data structures. We’ll also cover accessing and updating collections using Clojure’s idiomatic functions.
Clojure provides several scalar types that are similar to Java’s primitive types but with some unique characteristics. Let’s dive into the primary scalar types in Clojure: numbers, strings, keywords, and symbols.
Clojure supports a variety of numeric types, including integers, floating-point numbers, and ratios. Unlike Java, Clojure’s numbers are immutable and automatically promote to larger types when necessary.
;; Integer
(def my-int 42)
;; Floating-point
(def my-float 3.14)
;; Ratio
(def my-ratio 22/7)
;; BigInt
(def my-bigint 12345678901234567890N)
;; Arithmetic operations
(+ my-int my-float) ; => 45.14
(* my-ratio 2) ; => 44/7
In Clojure, you don’t need to worry about integer overflow as you do in Java, thanks to automatic type promotion.
Strings in Clojure are immutable sequences of characters, similar to Java’s String
class. You can use standard string operations such as concatenation and substring extraction.
(def my-string "Hello, Clojure!")
;; String concatenation
(str my-string " How are you?") ; => "Hello, Clojure! How are you?"
;; Substring
(subs my-string 7 14) ; => "Clojure"
Keywords are unique to Clojure and are often used as identifiers or keys in maps. They are immutable and interned, meaning they are stored in a way that makes them efficient for comparison.
(def my-keyword :name)
;; Using keywords as map keys
(def person {:name "Alice" :age 30})
;; Accessing map values
(get person :name) ; => "Alice"
Symbols are used to refer to variables or functions. They are similar to Java’s identifiers but are more flexible due to Clojure’s dynamic nature.
(def my-symbol 'x)
;; Using symbols to refer to variables
(def x 10)
(eval my-symbol) ; => 10
Clojure’s collection types are one of its most powerful features, providing immutable and persistent data structures. Let’s explore the primary collection types: lists, vectors, maps, and sets.
Lists in Clojure are linked lists, optimized for sequential access. They are similar to Java’s LinkedList
but immutable.
(def my-list '(1 2 3 4 5))
;; Accessing elements
(first my-list) ; => 1
(rest my-list) ; => (2 3 4 5)
;; Adding elements
(cons 0 my-list) ; => (0 1 2 3 4 5)
Vectors are indexed collections, similar to Java’s ArrayList
, but immutable. They provide efficient random access and are often used when order matters.
(def my-vector [1 2 3 4 5])
;; Accessing elements
(nth my-vector 2) ; => 3
;; Adding elements
(conj my-vector 6) ; => [1 2 3 4 5 6]
Maps are key-value pairs, similar to Java’s HashMap
, but immutable. They are often used for associative data.
(def my-map {:name "Alice" :age 30})
;; Accessing values
(get my-map :name) ; => "Alice"
;; Adding or updating entries
(assoc my-map :city "New York") ; => {:name "Alice", :age 30, :city "New York"}
Sets are collections of unique values, similar to Java’s HashSet
, but immutable. They are useful for membership tests.
(def my-set #{1 2 3 4 5})
;; Checking membership
(contains? my-set 3) ; => true
;; Adding elements
(conj my-set 6) ; => #{1 2 3 4 5 6}
Clojure’s collections can be nested, allowing you to create complex data structures. This is particularly useful for representing hierarchical data.
(def nested-map {:person {:name "Alice" :address {:city "New York" :zip 10001}}})
;; Accessing nested values
(get-in nested-map [:person :address :city]) ; => "New York"
;; Updating nested values
(assoc-in nested-map [:person :address :city] "Los Angeles")
; => {:person {:name "Alice", :address {:city "Los Angeles", :zip 10001}}}
Clojure provides a rich set of functions for accessing and updating collections. Let’s explore some of the most commonly used functions.
get
: Retrieve a value from a map or vector.first
: Get the first element of a list or vector.rest
: Get all but the first element of a list or vector.nth
: Access an element by index in a vector.(def my-map {:name "Alice" :age 30})
(get my-map :name) ; => "Alice"
(def my-vector [1 2 3 4 5])
(nth my-vector 2) ; => 3
assoc
: Add or update a key-value pair in a map.conj
: Add an element to a collection.dissoc
: Remove a key-value pair from a map.(def my-map {:name "Alice" :age 30})
(assoc my-map :city "New York") ; => {:name "Alice", :age 30, :city "New York"}
(def my-set #{1 2 3})
(conj my-set 4) ; => #{1 2 3 4}
(dissoc my-map :age) ; => {:name "Alice"}
To better understand the relationships and operations on Clojure’s data structures, let’s use some diagrams.
Diagram 1: Overview of Clojure’s Collection Types
For further reading on Clojure’s data types and structures, consider the following resources:
To reinforce your understanding of Clojure’s data types and structures, consider the following questions:
assoc
and dissoc
.In this section, we’ve explored the rich variety of data types and structures available in Clojure. From scalar types like numbers and strings to complex nested data structures, Clojure provides powerful tools for managing data in a functional way. By understanding these concepts, you can leverage Clojure’s strengths to build scalable and efficient applications.
Now that we’ve explored how immutable data structures work in Clojure, let’s apply these concepts to manage state effectively in your applications.