On Sun, Jan 18, 2009 at 2:55 PM, DavidH <[email protected]> wrote:
>
> In the code I'm writing, I seem to run into a certain pattern a lot.
> Often enough anyway that I thought there must be a different way of
> doing it - otherwise there would be a macro or function for it.
>
> The gist is that a function is applied to each member of a sequence,
> like map, except that the function also takes a changing state. The
> change to the state is described by a second function.
>
> Here's a function describing the pattern.
>
> (defn stateful-map [fn-item fn-state start-state start-items]
> (loop [state start-state
> items start-items
> result nil]
> (if (nil? items)
> (reverse result)
> (let [item (first items)
> new-item (fn-item state item)
> new-state (fn-state state item)]
> (recur new-state (rest items) (cons new-item result))))))
>
> So the question is, is there a better way of doing this?
In Clojure, the use of 'reverse' is an indication there's probably a
better way to do what you're doing. Specifically, instead of building
a list that must be reversed, you may choose to either build a vector
or a lazy seq. Since this function claims to want to be like 'map',
using lazy-cons to build a seq would be my first option.
Also, when a function takes a seq (as this does with start-items),
it's usually a good idea to assume users may pass you a collection and
want it to be treated the same as nil. In other words, for cases like
this 'empty?' may be a better choice than 'nil?'. In the following
re-write, I need the opposite sense, so I use 'seq':
(defn stateful-map [fn-item fn-state state [item :as items]]
(when (seq items)
(lazy-cons (fn-item state item)
(let [new-state (fn-state state item)]
(stateful-map fn-item fn-state new-state (rest items))))))
Anytime you use loop/recur or lazy-cons, there's a good chance you can
do it better with high-order functions (map, filter, reduce, etc.)
However, there are relatively few of these that pass any state from
iteration to the next. I believe it's currently only 'reduce' and
'iterate'. Of these, only 'iterate' produces a lazy seq.
So let's try with 'reduce' first, and since I don't know how to
produce a lazy seq with it, this time let's try a vector, called
'rtn-vec' below:
(defn stateful-map [fn-item fn-state start-state items]
(first (reduce (fn [[rtn-vec state] item]
[(conj rtn-vec (fn-item state item))
(fn-state state item)])
[[] start-state]
items)))
The 'iterate' function always produces a lazy seq, but since it's
always inifinite and returns all the values it produces for each
iteration, it can be messy to use in cases like this:
(defn stateful-map [fn-item fn-state start-state items]
(map second (take-while #(nth % 2)
(rest (iterate (fn [[state old-rtn [item & items]]]
[(fn-state state item)
(fn-item state item)
items])
[start-state nil items])))))
That's not even right, as it ends one item too early -- I find it
unlikely to be worth fixing.
One final option comes to mind, and that's the relatively new function
'reductions', from clojure-contrib. It also passes state from one
iteration to the next, like 'reduce', but produces a lazy seq:
(use '[clojure.contrib.seq-utils :only (reductions)])
(defn stateful-map [fn-item fn-state start-state items]
(map second (rest
(reductions (fn [[state rtn] item]
[(fn-state state item) (fn-item state item)])
[start-state nil]
items))))
I don't think that's a pattern I've ever needed, but there are a few
ways to get the functionality you want. Looking at all of them
together, I think I like the lazy-cons solution best.
--Chouser
--~--~---------~--~----~------------~-------~--~----~
You received this message because you are subscribed to the Google Groups
"Clojure" group.
To post to this group, send email to [email protected]
To unsubscribe from this group, send email to
[email protected]
For more options, visit this group at
http://groups.google.com/group/clojure?hl=en
-~----------~----~----~----~------~----~------~--~---