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:
- Decoding Clojure code, getting your feet wet
- Learning Clojure: coping with dynamic typing
- Learning Clojure: the arrow and doto macros
- Learning Clojure: dynamic dispatch
- Learning Clojure: dependent types and contract-based programming
- Learning Clojure: comparing with Java streams
- Feedback on Learning Clojure: comparing with Java streams (this post)
- 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).
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.
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.
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!