/ CLOJURE, SEQUENCE, STREAM

Learning Clojure: comparing with Java streams

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:

  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 (this post)
  7. Feedback on Learning Clojure: comparing with Java streams
  8. Learning Clojure: transducers

Clojure sequence counterparts

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

Java Clojure

map()

(map)

filter()

(filter)

limit()

(take)

(take-last)

skip()

(drop)

distinct()

(distinct)

sort()

(sort)

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). The extract-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 parameters

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)

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.

seq also works on String, native Java arrays (of reference types) and any objects that implement Iterable.
— ClojureDocs
https://clojuredocs.org/clojure.core/seq
iseq

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

To go further:

Nicolas Fränkel

Nicolas Fränkel

Developer Advocate with 15+ years experience consulting for many different customers, in a wide range of contexts (such as telecoms, banking, insurances, large retail and public sector). Usually working on Java/Java EE and Spring technologies, but with focused interests like Rich Internet Applications, Testing, CI/CD and DevOps. Also double as a trainer and triples as a book author.

Read More
Learning Clojure: comparing with Java streams
Share this