10 Clojure Interview Questions and Answers
Prepare for your next technical interview with this guide on Clojure, covering core concepts and practical applications.
Prepare for your next technical interview with this guide on Clojure, covering core concepts and practical applications.
Clojure, a modern, functional, and dynamic dialect of Lisp, has gained traction for its simplicity and powerful concurrency capabilities. Leveraging the Java Virtual Machine (JVM), Clojure offers seamless Java interoperability, making it a versatile choice for a variety of applications, from web development to data analysis. Its emphasis on immutability and functional programming paradigms helps developers write more predictable and maintainable code.
This article provides a curated selection of interview questions designed to test your understanding of Clojure’s core concepts and practical applications. By working through these questions, you will deepen your knowledge and be better prepared to demonstrate your proficiency in Clojure during technical interviews.
In Clojure, you can write a function that takes a list of numbers and returns a new list with each number squared using the map
function. This function applies a given operation to each item in a collection and returns a new collection with the results.
Example:
(defn square-list [numbers] (map #(* % %) numbers)) (square-list [1 2 3 4 5]) ;; (1 4 9 16 25)
Recursion is a common technique in Clojure for solving problems that can be broken down into smaller subproblems. The factorial of a number is a classic example. The base case for recursion is when n is 0 or 1, where the factorial is 1. For any other positive integer n, the factorial is n multiplied by the factorial of (n-1).
Here is a simple implementation of a recursive function to calculate the factorial of a number:
(defn factorial [n] (if (<= n 1) 1 (* n (factorial (dec n))))) (factorial 5) ; 120
Lazy sequences in Clojure are not computed until needed, allowing for the creation of potentially infinite sequences without performance issues. To generate an infinite lazy sequence of Fibonacci numbers, use the lazy-seq
function with recursion.
Example:
(defn fib-seq ([] (fib-seq 0 1)) ([a b] (lazy-seq (cons a (fib-seq b (+ a b))))))
A protocol in Clojure defines a set of functions that can be implemented by different types, providing polymorphism. This is similar to interfaces in other languages.
Example of defining a protocol and implementations:
(defprotocol Greet (greet [this])) (defrecord Person [name] Greet (greet [this] (str "Hello, " (:name this) "!"))) (defrecord Robot [id] Greet (greet [this] (str "Greetings, robot " (:id this) "."))) (let [p (->Person "Alice") r (->Robot 101)] (println (greet p)) ; Output: Hello, Alice! (println (greet r))) ; Output: Greetings, robot 101.
In this example, the Greet protocol is defined with a single function greet
. Two records, Person and Robot, implement this protocol. The greet
function is then used with instances of these records to demonstrate polymorphism.
Error handling in Clojure is done using try
, catch
, and finally
blocks. When dealing with file I/O, it’s important to handle potential errors like file not found or read permission issues. The try
block wraps the code that might throw an exception, the catch
block handles the exception, and the finally
block executes code that should run regardless of an exception.
Example function that reads from a file and handles potential errors:
(defn read-file [filename] (try (with-open [rdr (clojure.java.io/reader filename)] (reduce str (line-seq rdr))) (catch Exception e (str "Error reading file: " (.getMessage e)))))
In this function, with-open
ensures the file reader is closed after the operation. The try
block attempts to read the file, and if an exception occurs, the catch
block captures it and returns an error message.
Macros in Clojure allow you to write code that writes code, enabling you to create new syntactic constructs. They are useful for tasks like timing the execution of an expression.
Example of a macro that times the execution of an expression:
(defmacro time-execution [expr] `(let [start# (System/nanoTime) result# ~expr end# (System/nanoTime)] (println "Execution time:" (/ (- end# start#) 1e6) "ms") result#)) (time-execution (Thread/sleep 1000))
In this example, the time-execution macro takes an expression expr
as an argument. It records the start time before evaluating the expression and the end time after the expression has been evaluated. The difference between the end time and the start time is then printed in milliseconds. The result of the expression is also returned.
Data transformation in Clojure often involves converting nested maps into a flat structure. This simplifies data processing and makes it easier to work with complex data structures. The transformation can be achieved using recursion and the assoc
function to build the flat map.
Example:
(defn flatten-map [m] (letfn [(flatten-helper [m prefix] (reduce-kv (fn [acc k v] (let [new-key (if prefix (str prefix "." k) (str k))] (if (map? v) (merge acc (flatten-helper v new-key)) (assoc acc new-key v)))) {} m))] (flatten-helper m nil))) (flatten-map {:a {:b 1 :c {:d 2}} :e 3}) ;; => {"a.b" 1, "a.c.d" 2, "e" 3}
Futures in Clojure allow for asynchronous computations, enabling tasks to be executed in separate threads and results retrieved later. This improves performance by parallelizing tasks.
Example demonstrating the use of futures:
(defn async-computation [] (future (Thread/sleep 2000) ; Simulate a long-running task (println "Computation done!") 42)) (def my-future (async-computation)) (println "Doing other work...") ; Wait for the future to complete and get the result (println "Result from future:" @my-future)
In this example, the async-computation
function creates a future that simulates a long-running task by sleeping for 2 seconds. While the future is running, the program continues to execute other tasks. The result of the future is retrieved using the @
dereference operator.
A nested map in Clojure contains other maps as its values. Flattening it involves converting it into a single-level map where the keys are concatenated to represent the hierarchy of the original structure.
Example:
(defn flatten-map [m] (letfn [(flatten-helper [prefix m] (reduce-kv (fn [acc k v] (let [new-key (if prefix (str prefix "." k) (str k))] (if (map? v) (merge acc (flatten-helper new-key v)) (assoc acc new-key v)))) {} m))] (flatten-helper nil m))) (flatten-map {:a {:b 1 :c {:d 2}} :e 3}) ;; => {"a.b" 1, "a.c.d" 2, "e" 3}
In this example, the flatten-map
function uses a helper function flatten-helper
to recursively traverse the nested map. It concatenates keys using a dot (.) as a separator and accumulates the results in a single-level map.
In Clojure, handling exceptions can be done using the try
and catch
blocks. This allows us to manage errors gracefully without crashing the program. To safely divide two numbers and return nil if division by zero occurs, we can use these constructs to catch the ArithmeticException
that is thrown when attempting to divide by zero.
(defn safe-divide [num denom] (try (/ num denom) (catch ArithmeticException e nil))) (safe-divide 10 2) ; 5 (safe-divide 10 0) ; nil