Search code examples
javaclojureleiningen

Issue with UnmodifiableRandomAccessList and "contains?" function


I'm trying to contribute to an open-source application called stensil. It is written in Clojure.

stensil is a templating engine that works on top of .docx. The templating engine declares a bunch of custom functions to be used in a template. A full list of functions is quite short and can be seen here.

What I need is to add another custom function that checks if a list contains an element. Following existing examples, I did this:

(defmethod call-fn "contains" [_ item items] (contains? items item))

To test this, I created a .docx file that contains the following snippet:

{%= contains(5, data) %}

and a .json file that contains this:

{
  "data": [9, 4, 1, 6, 3]
}

Then I run the application like this:

java -jar stencil-core-0.3.8-standalone.jar template.docx sample.json

Unfortunately, when used, the function I defined doesn't work, throwing the following exception:

java.lang.IllegalArgumentException: contains? not supported on type: java.util.Collections$UnmodifiableRandomAccessList

It appears that items is of type java.util.Collections$UnmodifiableRandomAccessList. Indeed, if I add a (println items) to the function I get this:

#object[java.util.Collections$UnmodifiableRandomAccessList 0x778ca8ef [9, 4, 1, 6, 3]]

I tried doing it differently by using some function. This answer suggests it should work most of the time. Unfortunately both variants below return nil:

(defmethod call-fn "contains" [_ item items] (some #{item} items))
(defmethod call-fn "contains" [_ item items] (some #(= item %) items))

I've never written a line of Clojure in my life before, and a bit lost now. What am I doing wrong? Would it make sense to convert the list from UnmodifiableRandomAccessList to something that some or contains? can work with? If yes, how do I perform such conversion on items variable?

Upd. Tried using .contains as suggested by cfrick in an answer, like this:

(defmethod call-fn "contains" [_ item items] (.contains items item))

This doesn't raise a runtime error, yet the output is always false now. println debugging reveals this:

(defmethod call-fn "contains" [_ item items]
  (println item)
  (println items)
  (.contains items item))
1
#object[java.util.Collections$UnmodifiableRandomAccessList 0x778ca8ef [9, 4, 1, 6, 3]]

Why?


Solution

  • The answers above are all correct, because contains? can not be used on lists for this purpose and also we are trying to compare different types.

    The problem in this specific example is that the number in the first argument is a java.lang.Long, whereas the list in the second argument contains java.math.BigDecimal instances. This is because the default JSON parser stencil uses in standalone mode deserializes numbers as BigDecimal.

    A quick and dirty solution is to use strings everywhere.

    You can also define a variation of the function that works specifically with numbers:

    (defmethod call-fn "ncontains" [_ item items]
      (boolean (some #{(double item)} (map double items))))
    

    Or coerce all numbers to the same type:

    (defmethod call-fn "contains" [_ item items]
      (letfn [(norm [x] (if (number? x) (double x) x))]
        (boolean (some #{(norm item)} (map norm items)))))