/ CLOJURE, SEQUENCE, STREAM

Feedback on Learning Clojure: comparing with Java streams

Coming from a Java background, I’m currently trying to learn the Clojure programming language, with the help of online resources and mentorship. Some weeks ago, I tried to wire things together by trying to find equivalent methods to those available in Java streams.

While I managed to get things working, writing working code and writing idiomatic code are two very different things. I was fortunate to have a good degree of feedback from different sources: Hacker News, Reddit and on this very blog. I’d like to thank everyone who contributed in a positive way, and gave me more insight into Clojure. This post is a sum up of the most common feedback I received.

This is the 7th post in the Learning Clojure focus series.Other posts include:

  1. Decoding Clojure code, getting your feet wet
  2. Learning Clojure: coping with dynamic typing
  3. Learning Clojure: the arrow and doto macros
  4. Learning Clojure: dynamic dispatch
  5. Learning Clojure: dependent types and contract-based programming
  6. Learning Clojure: comparing with Java streams
  7. Feedback on Learning Clojure: comparing with Java streams (this post)
  8. Learning Clojure: transducers

Idiomatic keyword map

Let’s start with a simple, but it seems very widespread, usage regarding dictionary access.

In order to retrieve a value by its key inside a dictionary, I originally used a named function. Then, I went on to use an anonymous function:

(map #(:name %) justice-league)

Actually, the anonymous function is boiler-platey in Clojure. It can be replaced with plain keyword access:

(map :name justice-league)

This returns the same result, but with a much terser syntax.

Filtering out nil values

To filter out nil values, I inverted with (not) the available (nil?) check:

(filter #(not (nil? %)) [nil
                         {:vehicles [::Bat-Mobile, ::Bat-Plane]}
                         {:vehicles [::Invisible-Plane]}
                         nil
                         nil
                         nil])

That yields:

({:vehicles [:sandbox.function/Bat-Mobile :sandbox.function/Bat-Plane]}
 {:vehicles [:sandbox.function/Invisible-Plane]})

But Clojure provides an out-of-the-box function that does exactly the same, named (some?). It’s defined as the following:

Returns true if x is not nil, false otherwise.

Replacing the initial functions combination with (some?) is pretty straightforward:

(filter some? [nil
               {:vehicles [::Bat-Mobile, ::Bat-Plane]}
               {:vehicles [::Invisible-Plane]}
               nil
               nil
               nil])

In conclusion, it’s always better to directly call a function - if there’s one available, instead of inverting another one.

Keeping things together

The next improvement arises when only dictionary values from a single key need to be kept. In the last post, I used (filter) and (map) sequentially:

(->> [nil
      {:vehicles [::Bat-Mobile, ::Bat-Plane]}
      {:vehicles [::Invisible-Plane]}
      nil
      nil
      nil]
  (filter some?)
  (map :vehicles))

The output is the following:

([:sandbox.function/Bat-Mobile :sandbox.function/Bat-Plane]
 [:sandbox.function/Invisible-Plane])

Clojure provides an out-of-the-box function called (keep) that behaves similarly:

Returns a lazy sequence of the non-nil results of (f item).

— keep
https://clojuredocs.org/clojure.core/keep

In essence, (keep) applies the mapping function f to only non-nil values. Hence it’s easy to update the above code with it:

(keep :vehicles [nil
                 {:vehicles [::Bat-Mobile, ::Bat-Plane]}
                 {:vehicles [::Invisible-Plane]}
                 nil
                 nil
                 nil])

Mapcat for the win!

Finally, we need to flatten the resulting collection. This is the code I used previously:

(->> [nil
      {:vehicles [::Bat-Mobile, ::Bat-Plane]}
      {:vehicles [::Invisible-Plane]}
      nil
      nil
      nil]
  (keep :vehicles)
  (flatten))

This returns:

(:sandbox.function/Bat-Mobile
 :sandbox.function/Bat-Plane
 :sandbox.function/Invisible-Plane)

Interestingly enough, the following is the equivalent of the above snippet:

(->> [nil
      {:vehicles [::Bat-Mobile, ::Bat-Plane]}
      {:vehicles [::Invisible-Plane]}
      nil
      nil
      nil]
  (keep :vehicles)
  (apply concat))

Returns a lazy seq representing the concatenation of the elements in the supplied colls.

— concat
https://clojuredocs.org/clojure.core/concat

Alternatively, Clojure offers a (mapcat) function, a combination of a mapping function and applying (concat):

Returns the result of applying concat to the result of applying map to f and colls. Thus function f should return a collection.

— mapcat
https://clojuredocs.org/clojure.core/mapcat

Hence, the above code can make use of it, as in the following:

(mapcat :vehicles [nil
                   {:vehicles [::Bat-Mobile, ::Bat-Plane]}
                   {:vehicles [::Invisible-Plane]}
                   nil
                   nil
                   nil])
nil values are automatically filtered out because of applying (concat)

Final result

All in all, the "original code":

(->> justice-league
  (map #(:vehicles %))
  (filter #(not (nil? %)))
  (flatten))

can be replaced with this simple one-liner:

(mapcat :vehicles justice-league)

Conclusion

In my previous post, I wrote that Clojure syntax is pretty limited. Some commenters were not in agreement with this choice of words. Actually, I can only confirm: the basic building blocks are few.

On the opposite side, there is a whole lot of available out-of-the-box macros and functions. Knowing them can make the difference between a verbose hard-to-read code, and an idiomatic one.

Again, many thanks to everyone who gave me feedback. I hope to continue learning Clojure, don’t hesitate to steer me in the right direction if the need be!

Nicolas Fränkel

Nicolas Fränkel

Nicolas Fränkel is a technologist focusing on cloud-native technologies, DevOps, CI/CD pipelines, and system observability. His focus revolves around creating technical content, delivering talks, and engaging with developer communities to promote the adoption of modern software practices. With a strong background in software, he has worked extensively with the JVM, applying his expertise across various industries. In addition to his technical work, he is the author of several books and regularly shares insights through his blog and open-source contributions.

Read More
Feedback on Learning Clojure: comparing with Java streams
Share this