Learning Clojure: comparing with Java streams

This part is the 5th in a serie dedicated to learning the Clojure JVM language. Previous posts include:

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.

So, what would be the Clojure counterparts of Java’s functions filter(), map(), etc.?















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)
1Defines a dedicated (extract-name) function
2hero map parameter to extract from
3Function (get) gets the key (2_nd_ paramter) from the map (1_st_ parameter)
4The (map) function is equivalent to Java’s map() method on streams
5Anonymous 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.

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). The extract-name function can be rewritten as:

(defn extract-name
 "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.

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

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))
=> ("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 %)))

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? %)))
=> (:sandbox.function/Bat-Mobile :sandbox.function/Bat-Plane :sandbox.function/Invisible-Plane)

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.

seq also works on String, native Java arrays (of reference types) and any objects that implement Iterable.
— ClojureDocs

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 with next()

The type returned is actually not ISeq but ILazySeq. The former inherits from the later, and adds caching capabilities:

Will invoke the body only the first time seq is called, and will cache the result and return it on all subsequent seq calls.
— ClojureDocs

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.