Writing a Java function in Clojure
A function that we had to write in Java on a project that I worked on recently needed to indicate whether there was a gap in a series of data points or not.
If there were gaps at the beginning or end of the sequence then that was fine but gaps in the middle of the sequence were not.
null, 1, 2, 3 => no gaps 1, 2, 3, null => no gaps 1, null, 2, 3 => gaps
The Java version looked a bit like this:
public boolean hasGaps(List<BigInteger> values) { Iterator<BigInteger> fromHead = values.iterator(); while (fromHead.hasNext() && fromHead.next() == null) { fromHead.remove(); } Collections.reverse(values); Iterator<BigInteger> fromTail = values.iterator(); while (fromTail.hasNext() && fromTail.next() == null) { fromTail.remove(); } return values.contains(null); }
We take the initial list and then remove all the null values from the beginning of it, then reverse the list and remove all the values from the end.
We then check if there's a null value and if there is then it would indicate there is indeed a gap in the list.
To write this function in Clojure we can start off by using the 'drop-while' function to get rid of the trailing nil values.
I started off with this attempt:
(defn has-gaps? [list] let [no-nils] [drop-while #(= % nil) list] no-nils)
Unfortunately that gives us the following error!
Can't take value of a macro: #'clojure.core/let (NO_SOURCE_FILE:16)
It thinks we're trying to pass around the 'let' macro instead of evaluating it – I forgot to put in the brackets around the 'let'!
I fixed that with this next version:
(defn has-gaps? [list] (let [no-nils] [drop-while nil? list] no-nils))
But again, no love:
java.lang.IllegalArgumentException: let requires an even number of forms in binding vector (NO_SOURCE_FILE:23)
The way I understand it the 'let' macro takes in a vector of bindings as its first argument and what I've done here is pass in two vectors instead of one.
In the bindings vector we need to ensure that there are an even number of forms so that each symbol can be bound to an expression.
I fixed this by putting the two vectors defined above into another vector:
(defn has-gaps? [list] (let [[no-nils] [(drop-while nil? list)]] no-nils))
We can simplify that further so that we don't have nested vectors:
(defn has-gaps? [list] (let [no-nils (drop-while nil? list)] no-nils))
The next step was to make 'no-nils' a function so that I could make use of that function when the list was reversed as well:
(defn has-gaps? [list] (let [no-nils (fn [x] (drop-while nil? x))] (no-nils list)))
I then wrote the rest of the function to reverse the list and then check the remaining list for nil:
(defn has-gaps? [list] (let [[no-nils] [(fn [x] (drop-while nil? x))] [nils-removed] [(fn [x] ((comp no-nils reverse no-nils) x))]] (some nil? (nils-removed list))))
The 'comp' function can be used to compose a set of functions which is what I needed.
It seemed like the 'nils-removed' function wasn't really necessary so I inlined that:
(defn has-gaps? [list] (let [no-nils (fn [x] (drop-while nil? x))] (some nil? ((comp no-nils reverse no-nils) list))))
The function can now be used like this:
user=> (has-gaps? '(1 2 3)) nil user=> (has-gaps? '(nil 1 2 3)) nil user=> (has-gaps? '(1 2 3 nil)) nil user=> (has-gaps? '(1 2 nil 3)) true
I'd be intrigued to know if there's a better way to do this.
[...] This post was mentioned on Twitter by Mark Needham, ajlopez. ajlopez said: RT @markhneedham: a simple java -> clojure function example http://bit.ly/6dZB0N [...]
Tweets that mention Writing a Java function in Clojure at Mark Needham -- Topsy.com
23 Nov 09 at 9:08 pm
Partition by threes and "some" for a (x nil x)
Stuart Halloway
23 Nov 09 at 11:15 pm
(defn has-gaps? [s]
(->>
s
(drop-while nil?)
(drop-while #(not? (nil?)))
(drop-while nil?)
seq
boolean))
Stuart Halloway
23 Nov 09 at 11:45 pm
Left the % out of the previous one (iPhone sucks as IDE)
#(not? (nil? %))
Stuart Halloway
23 Nov 09 at 11:48 pm
@Stuart
cool solution and almost ok: not instead not?
First I was puzzled about ->>, it's unknown in the release clojure 1.0.0, but in snapshot clojure-1.0.0-2009-10-28 it's working:
user=> (->> 1 (+ 3) (+ 4))
8
Woudn't it be more readable to use empty? false? instead of seq boolean. I would create a more general trim utils function like (if it's not already in the contrib lib):
(defn trim [s t]
(->>
s
(drop-while #(= t %))
reverse
(drop-while #(= t %))
reverse))
and use it like:
(defn has-nil-gaps? [s]
(some nil? (trim s nil)))
Stefan Bistram
26 Nov 09 at 2:42 am
Stuart, the partition by threes approach won't work if there are consecutive nils inside the list, or am I missing something?
stand
26 Nov 09 at 5:17 am
Using Collections.reverse() in the original java solution is bad. Using a ListIterator from the end of the list should make it at least almost twice as fast.
Dimitris Andreou
26 Nov 09 at 9:17 pm
You could make your java code a bit more lispish by using subList instead of iterator. Something like:
public boolean hasGaps(List values) {
if (values.size() < 3) return false;
boolean rv = false;
List sub = values.subList(1, values.size()-1);
while(! rv && sub.size() > 0) {
rv = (sub.get(0) == null);
sub = sub.subList(1, sub.size());
}
return rv;
}
Similar in Clojure:
(defn has-gaps [v]
(cond (< (count v) 3) nil
:else (some (fn[x] (nil? x)) (subvec (vec v) 1 (dec (count v))))))
Steve
29 Nov 09 at 8:01 am
I see, your test cases really like:
null*, 1, 2, 3, null* => no gaps
where null* is any number of nulls and not just a single null at the beginning and end. Your original code makes more sense to me now.
Steve
29 Nov 09 at 8:43 am