Interview

10 Clojure Interview Questions and Answers

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.

Clojure Interview Questions and Answers

1. Write a function that takes a list of numbers and returns a new list with each number squared.

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)

2. Implement a recursive function to calculate the factorial of a number.

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

3. Write a function that generates an infinite lazy sequence of Fibonacci numbers.

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))))))

4. Define a protocol and a couple of implementations for it. Show how to use the protocol with different types.

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.

5. Write a function that handles potential errors when reading from a file and returns either the content or an error message.

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.

6. Write a macro that times the execution of an expression and prints the time taken.

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.

7. Write a function that performs a complex data transformation, such as converting nested maps into a flat structure.

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}

8. Write a function that demonstrates the use of futures.

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.

9. Write a function that converts a nested map into a flat map with concatenated keys.

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.

10. Write a function that safely divides two numbers and returns nil if division by zero occurs.

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
Previous

10 FIX Protocol Interview Questions and Answers

Back to Interview
Next

10 Kendo UI Interview Questions and Answers