Explore practical Clojure examples and exercises designed for Java developers transitioning to functional programming. Learn through hands-on coding and interactive REPL sessions.
Welcome to the practical section of our Clojure guide, where we will solidify your understanding of Clojure’s fundamental syntax and concepts through hands-on examples and exercises. As experienced Java developers, you already have a strong foundation in programming. This section will leverage that knowledge to help you transition smoothly into Clojure’s functional programming paradigm.
Let’s start by exploring how to create simple functions and manipulate data in Clojure. We’ll compare these examples with Java to highlight the differences and similarities.
In Java, you define a method within a class. In Clojure, functions are first-class citizens and can be defined using the defn
keyword.
Java Example:
public class Example {
public static int add(int a, int b) {
return a + b;
}
}
Clojure Equivalent:
(defn add [a b]
(+ a b))
defn
keyword is used to define a function named add
that takes two parameters, a
and b
, and returns their sum using the +
operator.Clojure provides powerful data structures like lists, vectors, maps, and sets. Let’s see how we can manipulate these collections.
Java Example:
import java.util.Arrays;
import java.util.List;
public class Example {
public static List<Integer> doubleValues(List<Integer> numbers) {
return numbers.stream()
.map(n -> n * 2)
.collect(Collectors.toList());
}
}
Clojure Equivalent:
(defn double-values [numbers]
(map #(* 2 %) numbers))
map
function applies a given function to each element of the collection. Here, #(* 2 %)
is an anonymous function that doubles each number.Try It Yourself: Modify the double-values
function to filter out odd numbers before doubling them.
Now, let’s practice creating and using different data types in Clojure. We’ll explore lists, vectors, maps, and sets.
Lists and vectors are sequential collections in Clojure. Lists are linked lists, while vectors are indexed collections.
Exercise: Create a list of numbers and a vector of strings. Write a function to concatenate the vector elements into a single string.
Solution:
(def numbers '(1 2 3 4 5))
(def words ["Hello" "world" "from" "Clojure"])
(defn concatenate-words [words]
(clojure.string/join " " words))
(concatenate-words words) ; => "Hello world from Clojure"
clojure.string/join
function concatenates the elements of the vector words
into a single string with spaces.Maps are key-value pairs, and sets are collections of unique elements.
Exercise: Create a map of student names to their grades and a set of subjects. Write a function to add a new student to the map.
Solution:
(def students {"Alice" 90, "Bob" 85})
(def subjects #{"Math" "Science" "History"})
(defn add-student [students name grade]
(assoc students name grade))
(add-student students "Charlie" 92) ; => {"Alice" 90, "Bob" 85, "Charlie" 92}
assoc
function adds a new key-value pair to the map.Try It Yourself: Modify the add-student
function to update a student’s grade if they already exist in the map.
The REPL (Read-Eval-Print Loop) is a powerful tool for experimenting with Clojure code. Let’s use it to explore some interactive exercises.
Start the REPL and try evaluating simple expressions. For example:
(+ 1 2 3) ; => 6
Define a function in the REPL and test it with different inputs.
(defn greet [name]
(str "Hello, " name "!"))
(greet "Alice") ; => "Hello, Alice!"
str
function concatenates strings.Try It Yourself: Define a function in the REPL that takes a list of names and returns a greeting for each name.
Let’s compare some common tasks in Clojure and Java to understand how Clojure’s functional approach simplifies code.
Java Example:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class Example {
public static List<Integer> filterEvenNumbers(List<Integer> numbers) {
return numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
}
}
Clojure Equivalent:
(defn filter-even-numbers [numbers]
(filter even? numbers))
filter
function in Clojure takes a predicate function (even?
) and a collection, returning a new collection of elements that satisfy the predicate.Java Example:
import java.util.Arrays;
import java.util.List;
public class Example {
public static int sumNumbers(List<Integer> numbers) {
return numbers.stream()
.mapToInt(Integer::intValue)
.sum();
}
}
Clojure Equivalent:
(defn sum-numbers [numbers]
(reduce + numbers))
reduce
function applies a binary function (+
) cumulatively to the elements of the collection, reducing it to a single value.Try It Yourself: Modify the sum-numbers
function to calculate the product of the numbers instead of the sum.
To better understand Clojure’s data flow and immutability, let’s visualize these concepts using diagrams.
graph TD; A[Input Collection] -->|map| B[Transformed Collection]; B -->|filter| C[Filtered Collection]; C -->|reduce| D[Reduced Value];
map
, filter
, and reduce
.graph TD; A[Original Collection] -->|assoc| B[New Collection]; A -->|dissoc| C[Another New Collection];
Let’s reinforce your learning with some exercises and practice problems.
Write a function that takes a list of numbers and returns a list of their squares, excluding negative numbers.
Solution:
(defn square-non-negative [numbers]
(->> numbers
(filter (fn [n] (>= n 0)))
(map (fn [n] (* n n)))))
->>
macro threads the collection through the functions filter
and map
.Create a function that simulates a simple bank account. It should take an initial balance and a list of transactions (positive for deposits, negative for withdrawals) and return the final balance.
Solution:
(defn calculate-balance [initial-balance transactions]
(reduce + initial-balance transactions))
(calculate-balance 100 [20 -10 50 -30]) ; => 130
reduce
function accumulates the transactions starting from the initial balance.Try It Yourself: Modify the calculate-balance
function to reject transactions that would result in a negative balance.
In this section, we’ve explored practical examples and exercises to reinforce your understanding of Clojure’s syntax and concepts. We’ve compared Clojure with Java to highlight the benefits of functional programming, such as concise code and powerful data manipulation capabilities.
Key Takeaways:
map
, filter
, and reduce
simplify data processing tasks.Now that we’ve explored these practical examples, let’s continue to build on this foundation as we delve deeper into Clojure’s advanced features and capabilities.