Chris
Chris

Reputation: 1128

Mapping mutable methods onto Java objects in Clojure

Using Clojure, suppose t represents a Java object, and I have a collection of [ts].

How can I map .setter to [ts]? I'm having trouble using (map #(.setter %1 %2) ts [vals]). When I access the associated getters afterwards, I return a list of nils.

Upvotes: 1

Views: 261

Answers (2)

John Wiseman
John Wiseman

Reputation: 3137

It sounds like the setter method isn't returning the modified t object. If you wrote setter, you could modify it so that it does, or you will just have to hold on to your original ts (but also make sure to use dorun to squeeze out the laziness of map):

(let [ts ...]
  (dorun (map #(.setter %1 %2) ts [vals]))
  (println "Modified ts:" ts))

An alternative, if it would be much more convenient for you to have the map return the collection, is to do something like this:

(dorun (map #(do (.setter %1 %2) %1) ts [vals]))

Upvotes: 2

kamituel
kamituel

Reputation: 35950

As an example of ArrayList Java class. I create three of those, and then I add "s1" to the first one, "s2" to the second and "s3" to the third. This is my "setter".

Then I read the first value of each, and I expect to get "s1", "s2", "s3" - so that's a getter in this example.

(let [ts [(java.util.ArrayList. 1)
          (java.util.ArrayList. 1)
          (java.util.ArrayList. 1)]]
  ; Add one element to each of the ArrayList's
  (doall (map #(.add %1 %2) ts ["s1" "s2" "s3"]))

  ; Verify that elements are indeed added.
  (doall (map #(.get %1 0) ts)))

This example works as expected - the latter mapv returns ("s1" "s2" "s3").

Why the similar approach would not work for you? Well, my strong suspiction is that's because you use map which returns a lazy sequence.

map returns a lazy sequence, which means that it will not evaluate unless you try to get/use the values produced by it - and you usually don't need a return value from a setter. This means that your setter would be never called.

That's why I've used doall - it will take a lazy sequence and realize it, meaning that each element will get computed (in your case - each setter will get called).

My example, when I use only map and not doall when setting elements, would fail:

(let [ts [(java.util.ArrayList. 1)
          (java.util.ArrayList. 1)
          (java.util.ArrayList. 1)]]
  ; Use a lazy map here - without doall
  (map #(.add %1 %2) ts ["s1" "s2" "s3"])
  (doall (map #(.get %1 0) ts)))

; java.lang.IndexOutOfBoundsException: Index: 0, Size: 0

It fails indeed, because no element got added.

Alternatively, you can use a mapv variant that returns a vector and hence is always not lazy:

(let [ts [(java.util.ArrayList. 1)
          (java.util.ArrayList. 1)
          (java.util.ArrayList. 1)]]
  ; Use a lazy map, but realize sequence using doall
  (doall (map #(.add %1 %2) ts ["s1" "s2" "s3"]))
  (mapv #(.get %1 0) ts))

As pointed out by @John Wiseman in the comment, though, mapv is probably a worse choice because it's not always clear that it's used to force realization, while doall makes it clear and obvious.


A word about lazy sequences:

A lazy seq is a seq whose members aren't computed until you try to access them.

(source)

It's a good thing to learn more about lazy seqs, as they're a powerful tool used all over in Clojure codebases. One somewhat counter-intuitive thing they bring to the table is an ability to create infinite sequences, i.e. (range) creates an infinite sequence of all numbers starting with 0. So it's perfectly legal to do:

(map #(* % %) (range))

It will create an infinite lazy sequence of squares of all the numbers!

Upvotes: 2

Related Questions