4 public class NodeCounter {8 public static int countNodes (Object data) {10 if (!(data instanceof Iterable)) return 1;5 int result = 1;8 for (Object e : (Iterable)data) {6 result += countNodes(e);0 }3 return result;0 }0 }------44This is actually slightly better than the loop/recur version in Clojure, despite the types everywhere, probably because of the for-each loop being a bit more abstract than loop/recur (a while with an explicit iterator would perhaps be a fairer comparison; and map + reduce is clearly much more abstract still) and the use of recursion. It would surely blow up to 60 or even 70 if we added explicit stack management to traverse the tree non-recursively and used a while loop with an explicit iterator.
I'm curious (general Clojure question) about your use of the quoted
form. The Clojure docs state that this results in an unevaluated form,
but I had trouble finding more details on this. F.E. I'd like to run
count-nodes against a compiled fn that I've previously defined, but I
could not get an existing fn into quoted form - and (count-nodes a-fn)
always returns 1. Is using a macro the only way to get an expression's
unevaluated form?
Do you (or anyone) believe some general guidelines could be gathered
from running count-nodes against all of the individual fns in a
project. F.E. a number > X(?) for a fn means you might be using an
imperative style and should be looking at breaking up the fn or
looking for ways to use map/reduce/other idiomatic Clojure fns to
simplify your code?
Every time I engage a company for contract work I wonder what I'm in
for ( the same goes when I dive in to another open source project). I
think it's possible that viewing this metric by fn and by file would
give some insight.
Fun!
> The count-nodes function itself has a code complexity of 22 by the same
> measure:
> user=> (count-nodes '(defn count-nodes [data] (if
> (clojure.contrib.core/seqable? data) (+ 1 (reduce + 0 (map count-nodes
> data))) 1)))
Well, speaking of higher-order functions and golf...
(count-nodes
'(defn count-nodes [data]
(count (tree-seq clojure.contrib.core/seqable? seq data))))
; 12
Unfortunately I don't think contrib's sequable is quite what we want:
(count-nodes "abcde")
; 6
This is a bit closer I think:
(count-nodes
'(defn count-nodes [data]
(count (tree-seq (partial instance? clojure.lang.Seqable) seq data))))
; 15
...though preferring 'partial' over #() is of debatable merit.
--Chouser
This sounds like a neat idea to me. Maybe the way to get the
'complexity' is to calculate it at definition, this macro doesn't work
for obvious reasons:
(defmacro defnc
[n & body]
`(let [f# (defn ~n ~@body)]
(alter-meta! (var ~n) assoc :complexity (count-nodes ~body))
f#))
But I think something similar could do the trick?
(count-nodes "abcde"); 6
On the other hand, size of the generated byte code is moderately
interesting (as is runtime performance in time and heap).
I do *love* the fact that the analysis can be done in Clojure itself.
Can you imagine doing that in Java? Maybe that (how many lines to
analyze the source in the language itself) should be the power
meta-metric. Lisps clearly win big here.