Collection Building Idiom

7 views
Skip to first unread message

Mark McGranaghan

unread,
Sep 18, 2008, 8:49:54 PM9/18/08
to Clojure
Hi all,

I was wondering if there was an idiomatic functional way to accomplish
the following:

We have some parameters, and we want to build a collection based on
those parameters. The collection could have as many as n items, and
whether and what items get added for each of the n possibilities will
depend on the parameters.

In Ruby, this might look like:
def build_coll(a, b)
coll = []
if a > 2
coll << (a * b)
end
if b > 2
coll << (a + b)
end
if a > b
coll << (a - b)
end
coll
end

build_coll(3, 1)

I'd like to accomplish this using a purely functional style. One way
to do this would be to reduce a collection of test functions with an
initial empty collection and a reduce function that adds the result of
the test element if the test passes:

(defn build-coll [a b]
(reduce
(fn [coll f] (if-let result (f a b) (conj coll result) coll))
[]
[(fn [a b] (if (> a 2) (* a b)))
(fn [a b] (if (> b 2) (+ a b)))
(fn [a b] (if (> a b) (- a b)))]))

(build-coll 3 1)

Does anyone have any better ideas about how to go about this?

Thanks,
- Mark McGranaghan

Parth Malwankar

unread,
Sep 19, 2008, 3:20:50 AM9/19/08
to Clojure
Hi Mark,

Not sure if this is better but here is another way to do it.

(defn simple-build-coll [a b]
(vec
(filter #(not (nil? %))
(list
(when (> a 2) (* a b))
(when (> b 2) (+ a b))
(when (> a b) (- a b))))))

user=> (simple-build-coll 3 1)
[3 2]

The vec is just to convert the collection to a vector if
thats whats being used.

If this is commonly needed in the program I would probably write
a macro to do this. Its would make the program much more
readable .. not the macro but the usage of it :)

(defn reverse-pairs [l]
(apply concat (reverse (partition 2 l))))

(defmacro conj-if-aux [coll & preds_xs]
(if preds_xs
`(if ~(first preds_xs)
(conj (conj-if-aux ~coll ~@(rest (rest preds_xs))) ~(second
preds_xs))
(conj-if-aux ~coll ~@(rest (rest preds_xs))))
coll))

(defmacro conj-if [coll & preds_xs]
`(conj-if-aux ~coll ~@(reverse-pairs preds_xs)))

The main logic is in conj-if-aux.
I use reverse-pairs and conj-if just to maintain the
correct order in the resulting collection (for lists and vectors).
If thats not important conj-if-aux should suffice.

reverse-pairs maintains the pairs of predicate and exprs
in the reversed list.
user=> (reverse-pairs [:a :b :c :d :e :f])
(:e :f :c :d :a :b)

Usage would be:

(defn mac-build-coll [a b]
(conj-if [9 10]
(> a 2) (* a b)
(> b 2) (+ a b)
(> a b) (- a b)))

(defn mac-build-coll2 [a b]
(conj-if '(9 10)
(> a 2) (* a b)
(> b 2) (+ a b)
(> a b) (- a b)))

user=> (mac-build-coll 3 1)
[9 10 3 2]
user=> (mac-build-coll2 3 1)
(2 3 9 10)
user=>

Note that (1) the type of collection is maintained by
conj here (either a list or a vector) and (2) the order in
which elements are added depend on the the type of collection.
Conj appends to head of list.
9,10 are used for the initial collection (either vector or list).

Parth



>
> Thanks,
> - Mark McGranaghan

Rich Hickey

unread,
Sep 19, 2008, 8:25:44 AM9/19/08
to Clojure
For one-off multi-step initialization it's perfectly acceptable to use
let:

(defn build-coll [a b]
(let [c []
c (if (> a 2) (conj c (* a b)) c)
c (if (> b 2) (conj c (+ a b)) c)
c (if (> a b) (conj c (- a b)) c)]
c))

Here each c is a new immutable local that hides the previous, but the
effect feels a lot like build-by-assignment.

If this is something you need to do often, a simple macro will remove
any repetition. This macro expands into a let like the one above:

(defmacro conj-cond [coll & test-expr+]
(let [c (gensym)]
`(let [~c ~coll
~@(mapcat (fn [[test expr]]
`(~c (if ~test (conj ~c ~expr) ~c)))
(partition 2 test-expr+))]
~c)))

Now you could write it like this:

(defn build-coll [a b]
(conj-cond []
(> a 2) (* a b)
(> b 2) (+ a b)
(> a b) (- a b)))

Rich

Mark McGranaghan

unread,
Sep 19, 2008, 10:55:17 AM9/19/08
to clo...@googlegroups.com
Thanks Rich,

This is exactly the "there must be a simple way to do this that I am
overlooking" answer that I was hoping for.

- Mark

Reply all
Reply to author
Forward
0 new messages