tags:

views:

1388

answers:

4

I have a java.util.HashMap object m (a return value from a call to Java code) and I'd like to get a new map with an additional key-value pair.

If m were a Clojure map, I could use:

(assoc m "key" "value")

But trying that on a HashMap gives:

java.lang.ClassCastException: java.util.HashMap cannot be cast to clojure.lang.Associative

No luck with seq either:

(assoc (seq m) "key" "value")

java.lang.ClassCastException: clojure.lang.IteratorSeq cannot be cast to clojure.lang.Associative

The only way I managed to do it was to use HashMap's own put, but that returns void so I have to explicitly return m:

(do (. m put "key" "value") m)

This is not idiomatic Clojure code, plus I'm modifying m instead of creating a new map.

Any ideas on how to work with a HashMap in a more Clojure-ish way?

Thanks.

+6  A: 

Clojure makes the java Collections seq-able, so you can directly use the Clojure sequence functions on the java.util.HashMap.

But assoc expects a clojure.lang.Associative so you'll have to first convert the java.util.HashMap to that:

(assoc (zipmap (.keySet m) (.values m)) "key" "value")

Edit: simpler solution:

(assoc (into {} m) "key" "value")
Siddhartha Reddy
But then you don't have a hashmap, you have a clojure map, which seems like it defeats the point he's shooting for.
Runevault
So (assoc (into {} m) "key" "value") instead of (assoc m "key" "value"). Beautiful! Thanks.
Frederic Daoud
I believe that if you need a HashMap again you can just create a new one. (java.util.HashMap. m)
Eric Normand
+1  A: 

This is some code I wrote using hashmaps when I was trying to compare memory characteristics of the clojure version vs java's (but used from clojure)

(import '(java.util Hashtable))
(defn frequencies2 [coll]
    (let [mydict (new Hashtable)]
      (reduce (fn [counts x]
         (let [y (.toLowerCase x)]
           (if (.get mydict y)
      (.put mydict y (+ (.get mydict y) 1))
      (.put mydict y 1)))) coll) mydict))

This is to take some collection and return how many times each different thing (say a word in a string) is reused.

Runevault
+10  A: 

If you're interfacing with Java code, you might have to bite the bullet and do it the Java way, using .put. This is not necessarily a mortal sin; Clojure gives you things like do and . specifically so you can work with Java code easily.

assoc only works on Clojure data structures because a lot of work has gone into making it very cheap to create new (immutable) copies of them with slight alterations. Java HashMaps are not intended to work in the same way. You'd have to keep cloning them every time you make an alteration, which may be expensive.

If you really want to get out of Java mutation-land (e.g. maybe you're keeping these HashMaps around for a long time and don't want Java calls all over the place, or you need to serialize them via print and read, or you want to work with them in a thread-safe way using the Clojure STM) you can convert between Java HashMaps and Clojure hash-maps easily enough, because Clojure data structures implement the right Java interfaces so they can talk to each other.

user> (java.util.HashMap. {:foo :bar})
#<HashMap {:foo=:bar}>

user> (into {} (java.util.HashMap. {:foo :bar}))
{:foo :bar}

If you want a do-like thing that returns the object you're working on once you're done working on it, you can use doto. In fact, a Java HashMap is used as the example in the official documentation for this function, which is another indication that it's not the end of the world if you use Java objects (judiciously).

clojure.core/doto
([x & forms])
Macro
  Evaluates x then calls all of the methods and functions with the
  value of x supplied at the front of the given arguments.  The forms
  are evaluated in order.  Returns x.

  (doto (new java.util.HashMap) (.put "a" 1) (.put "b" 2))

Some possible strategies:

  1. Limit your mutation and side-effects to a single function if you can. If your function always returns the same value given the same inputs, it can do whatever it wants internally. Sometimes mutating an array or map is the most efficient or easiest way to implement an algorithm. You will still enjoy the benefits of functional programming as long as you don't "leak" side-effects to the rest of the world.

  2. If your objects are going to be around for a while or they need to play nicely with other Clojure code, try to get them into Clojure data structures as soon as you can, and cast them back into Java HashMaps at the last second (when feeding them back to Java).

Brian Carper
Thanks for the detailed explanation. Very useful and interesting. I learned the tip on "doto" instead of "do", more elegant way of calling void methods on Java objects and getting them back at the end. You'd think I'd realize this, since I'm reading Stu's book ;-)
Frederic Daoud
+2  A: 

It's totally OK to use the java hash map in the traditional way.
(do (. m put "key" "value") m)
This is not idiomatic Clojure code, plus I'm modifying m instead of creating a new map.

You are modifying a data structure that really is intended to be modified. Java's hash map lacks the structural sharing that allows Clojures map's to be efficiently copied. The generally idiomatic way of doing this is to use java-interop functions to work with the java structures in the typical java way, or to cleanly convert them into Clojure structures and work with them in the functional Clojure way. Unless of course it makes life easier and results in better code; then all bets are off.

Arthur Ulfeldt