1

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?

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
oldhomemovie
  • 14,621
  • 13
  • 64
  • 99

2 Answers2

4

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)))))
erdos
  • 3,135
  • 2
  • 16
  • 27
  • Thank you! I went with mapping entire list to strings & submitted a PR. Would you personally prefer coercing numbers as a more optional solution for PR? – oldhomemovie Sep 16 '20 at 20:18
  • Wouldn't it be easier to just convert the item to a BigDecimal? – cfrick Sep 16 '20 at 20:20
2

contains? would not do what you want anyway - it is for things that support lookup (e.g. you can check, if a list has an index - not if a value is in there).

From the docs:

Returns true if key is present in the given collection, otherwise returns false. Note that for numerically indexed collections like vectors and Java arrays, this tests if the numeric key is within the range of indexes. 'contains?' operates constant or logarithmic time; it will not perform a linear search for a value. See also 'some'.

You can fall back to what Java has to offer here Collection.contains:

Returns true if this collection contains the specified element. More formally, returns true if and only if this collection contains at least one element e such that (o==null ? e==null : o.equals(e)).

So you can do:

(.contains items item)

Further exploration:

user=> (.contains (java.util.Collections/unmodifiableList [42]) 42)
true
user=> (.contains (java.util.Collections/unmodifiableList [42]) 64)
false
user=> (.contains [42] 42)
true
user=> (.contains [42] 64)
false
cfrick
  • 35,203
  • 6
  • 56
  • 68
  • The code no longer raises a runtime error, yet the output is always "false" for some reason. Any idea why? I've update the question using `.contains` from your example (see Upd. section at the end) – oldhomemovie Sep 16 '20 at 19:01
  • A naive explanation would be that the objects in your list and item are actually different, but they print the same. I assume, the real problem is really just with numbers and you are not using them to over-simplify the actual problem? – cfrick Sep 16 '20 at 19:31
  • cfrick, I've tried changing the numbers in my json to a list of strings, e.g. `["hello", "world"]`, and having a `{%= contains("world", data) %}` – still get false in return. Really confused by this – indeed it seems like the list contains values that are not equal to the `item` – oldhomemovie Sep 16 '20 at 19:50
  • You could try to debug with something like `(run! (comp println bean) items)` – cfrick Sep 16 '20 at 19:57