Search code examples
clojureclojure-java-interop

Extending Clojure's Associative Abstraction to Java Library Types


I have an application (several actually) which decode JSON data in a Map using Jackson. Data appears to either be in a Map or ArrayList (in the case of JSON arrays.) The data that comes in on these streams is unstructured, so this won't be changing.

I own some Clojure code which accesses nested properties in these objects. Ideally I'd like to extend the Associative abstraction to these Java types so that get-in works on them. Something like the following:

(extend-protocol clojure.lang.Associative
  java.util.Map
    (containsKey [this k] (.containsKey this k))
    (entryAt [this k] (when (.containsKey this k)
                  (clojure.lang.MapEntry/create k (.get this k))))
java.util.ArrayList
  (containsKey [this k] (< (.size this) k))
  (entryAt [this k] (when (.containsKey this k)
                  (clojure.lang.MapEntry/create k (.get this k)))))

There are two problems with this; The first being that Associative is not a protocol (if it were this appears it would work). The second being that the types are already defined so I cannot add Associative with deftype.

I'm pretty new to the JVM interop part of Clojure. Is there a method I'm not seeing? Or is there a protocol which wraps Associative and will work with get-in that I've missed?

Thanks SO!


Solution

  • The answer is that half of the extension you want to do is already done, and the other half cannot be done. The get-in function calls get, which calls clojure.lang.RT/get, which calls clojure.lang.RT/getFrom, which calls java.util.Map/get if the first argument is a Map. So if you have any Java Map, then get-in works (I'm borrowing this example directly from the doto docstring):

    (let [m (doto (new java.util.HashMap) (.put "a" 1) (.put "b" 2))]
      [(get-in m ["b"])
       (get-in m ["a"])])
    ;;=> [2 1]
    

    However, Clojure does not have a get implementation for Lists that support RandomAccess. You could write your own get that does:

    (ns sandbox.core
      (:refer-clojure :exclude [get])
      (:import (clojure.lang RT)
               (java.util ArrayList List RandomAccess)))
    
    (defn get
      ([m k]
       (get m k nil))
      ([m k not-found]
       (if (and (every? #(instance? % m) [List RandomAccess]) (integer? k))
         (let [^List m m
               k (int k)]
           (if (and (<= 0 k) (< k (.size m)))
             (.get m k)
             not-found))
         (RT/get map key not-found))))
    

    Example:

    (get (ArrayList. [:foo :bar :baz]) 2)
    ;;=> :bar
    

    Then you could copy the implementation of get-in so it would use your custom get function.

    I'm pretty sure this isn't what you want, though, because then every bit of code you write would have to use your get-in rather than Clojure's get-in, and any other code that already uses Clojure's get would still not work with ArrayLists. I don't think there's really a good solution to your problem, unfortunately.