In general, one learns by comparing to what one already knows: I’m learning Clojure that way. Coming from a Java background, I naturally want to use streaming features.
This is the 6th 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 (this post)
- Feedback on Learning Clojure: comparing with Java streams
- Learning Clojure: transducers
Clojure sequence counterparts
So, what would be the Clojure counterparts of Java’s functions filter()
, map()
, etc.?
Java | Clojure |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Obviously, those are pretty similar. Let’s play with those functions, using a simple data set:
(def justice-league [
{:name "Superman"
:secret-id "Clark Kent"
:strength 100
:move [::flight, ::run]}
{:name "Batman"
:secret-id "Bruce Wayne"
:strength 20
:move [::glide, ::drive, ::pilot]
:vehicles [::Bat-Mobile, ::Bat-Plane]}
{:name "Wonder Woman"
:secret-id "Diana Prince"
:strength 90
:move [::run]
:vehicles [::Invisible-Plane]}
{:name "Flash"
:secret-id "Barry Allen"
:strength 10
:move [::run]
}
{:name "Green Lantern"
:secret-id "Hal Jordan"
:strength 20
:move [::flight]}
{:name "Aquaman"
:secret-id "Arthur Curry"
:strength 40
:move [::swim]}])
Let’s start with a very simple example: get the names of the team members.
(defn extract-name (1)
[hero] (2)
"Get the name out of a hero map"
(get hero :name)) (3)
(map (4)
(fn [hero] (extract-name hero)) (5)
justice-league)
1 | Defines a dedicated (extract-name) function |
2 | hero map parameter to extract from |
3 | Function (get) gets the key (2_nd_ paramter) from the map (1_st_ parameter) |
4 | The (map) function is equivalent to Java’s map() method on streams |
5 | Anonymous function to map a hero to its :name key |
As expected, this yields:
=> ("Superman" "Batman" "Wonder Woman" "Flash" "Green Lantern" "Aquaman")
Although it works, this is a crude first draft.
Idiomatic improvements
A couple of refinements are in order.
- Dictionary access
-
The
(get)
function can be replaced with Clojure idiomatic dictionary access: instead of(get dic :a-key)
, one can write(:a-key dic)
. Theextract-name
function can be rewritten as:(defn extract-name [hero] "Get the name out of a hero map" (:name hero))
- Anonymous function
-
There’s a lot of boilerplate code invoked to extract the name. In Java, one would just write a lambda instead of a full-fledged function:
justiceLeague.stream().map(hero -> hero.get(":name"));
Clojure also allows such constructs. Instead of writing anonymous functions using the full
(fn)
syntax, it’s possible to use an abridged#()
syntax. Let’s migrate the anonymous function inside of(map)
to the later form:(map #(extract-name %) justice-league)
The
%
references the single parameter.Multiple parametersIn case of multiple parameters passed to the anonymous function, they are referenced with
%1
,%2
,…%n
.
At this point, having a dedicated name extracting function is overkill. It can safely be removed in favor of an anonymous function.
(map #(:name %) justice-league)
Composing functions
The next step is to compose functions.
Let’s filter out heroes who are not strong enough:
(filter #(< 30 (:strength %)) justice-league)
This yields the whole structure for each item, but suppose I’m only interested in the names.
I need to first execute the (filter)
function and afterwards the (map)
one:
(map #(:name %) (filter #(< 30 (:strength %)) justice-league))
The output is:
=> ("Superman" "Wonder Woman" "Aquaman")
That’s a bit unwieldy, and can get worse with the number of functions composed. With the help of the arrow macro seen in an earlier post, it’s easy to rewrite the above in a more readable way:
(->> justice-league
(filter #(< 30 (:strength %)))
(map #(:name %)))
Flat map and it’s a wrap
Java streams also offer a flatMap()
method, so that a List<List<?>>
can be transformed into a List<?>
.
From the above data, let’s get all vehicles available to the Justice League.
As seen above, this is achieved with the (map)
function:
(->> justice-league
(map #(:vehicles %)))
This returns a list of lists and nil
values:
=> (nil [:sandbox.function/Bat-Mobile :sandbox.function/Bat-Plane] [:sandbox.function/Invisible-Plane] nil nil nil)
First, nil
values have to be removed:
(->> justice-league
(map #(:vehicles %))
(filter #(not (nil? %))))
=> ([:sandbox.function/Bat-Mobile :sandbox.function/Bat-Plane] [:sandbox.function/Invisible-Plane])
In Clojure, the equivalent function of flatMap()
is (flatten)
:
(->> justice-league
(map #(:vehicles %))
(filter #(not (nil? %)))
(flatten))
The final result is:
=> (:sandbox.function/Bat-Mobile :sandbox.function/Bat-Plane :sandbox.function/Invisible-Plane)
Sequences
All those functions are cool, but what data structure sits behind them?
Having a look at the code, every function calls the (seq)
function.
This transforms the collection passed as an argument into a clojure.lang.ISeq
and transforms it further.
— ClojureDocs
https://clojuredocs.org/clojure.core/seq |
ISeq
is an immutable data structure.
It’s another way to look at an ordered collection.
Instead of indexed access like List
, it provides access to:
- the first item via
first()
- the
ISeq
minus the first item withnext()
The type returned is actually not Will invoke the body only the first time
— ClojureDocs
https://clojuredocs.org/clojure.core/lazy-seq |
Conclusion
The exact same functionalities provided in Java streams are also available in Clojure. As for every language, learning the syntax is only a fraction of the work, and Clojure’s syntax is pretty limited. Real proficiency can only be reached by knowing the API.