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 -~----------~----~----~----~------~----~------~--~---