On Sun, Jan 18, 2009 at 2:55 PM, DavidH <davidh...@gmail.com> 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 clojure@googlegroups.com
To unsubscribe from this group, send email to 
clojure+unsubscr...@googlegroups.com
For more options, visit this group at 
http://groups.google.com/group/clojure?hl=en
-~----------~----~----~----~------~----~------~--~---

Reply via email to