exupero's blog
RSSApps

Lens transducer

In the previous post I showed the transducer that taught me how to write transducers. Here I'll finish our discussion of transducers with an experimental transducer that operates on nested values.

Occasionally it can be useful to transduce over a field within a map or the Nth position of a vector, but it's awkward to extract the values, transduce over a new sequence, then merge the transduced sequence back into the items in the original sequence. To handle this more fluently, I've toyed with a transducer like this:

(defn lens
  ([k xf]
   (lens k #(assoc %1 k %2) xf))
  ([getter setter xf]
   (let [rf' (xf (completing (fn [result item] item)))]
     (fn [rf]
       (fn
         ([] (rf))
         ([result] (rf result))
         ([result item]
          (let [value (getter item)
                new-value (rf' nil value)
                new-item (setter item new-value)]
            (rf result new-item))))))))

The twist in lens is the alternate reducing function rf', which does nothing other than return the final item, the one that's passed through the transducer xf. This allows us to get an item out of a collection, run it through a transducer pipeline, then put the result back into the original collection, which is then itself reduced.

Here's an example which increments the field :x:

(def data
  [{:x 1}
   {:x 2}
   {:x 3}
   {:x 4}
   {:x 5}
   {:x 6}])
(sequence
  (lens :x (map inc))
  data)
({:x 2} {:x 3} {:x 4} {:x 5} {:x 6} {:x 7})

When the transduction doesn't return a value, :x becomes nil:

(sequence
  (lens :x (filter even?))
  data)
({:x nil} {:x 2} {:x nil} {:x 4} {:x nil} {:x 6})

The same results could have been found with a simple map operation. Where lens helps most is when you want to carry some state from one item to the next, such as when partitioning:

(sequence
  (lens :x (partition-all 2))
  data)
({:x nil} {:x [1 2]} {:x nil} {:x [3 4]} {:x nil} {:x [5 6]})

Because rf' returns nil when it doesn't get a final value, whenever xf keeps a value in its internal state, :x ends up without a value. Using a transducing form of (partition n step pad coll) may be more meaningful:

(require '[net.cgrand.xforms :as xf])
(sequence
  (lens :x (xf/partition 2 1))
  data)
({:x nil} {:x [1 2]} {:x [2 3]} {:x [3 4]} {:x [4 5]} {:x [5 6]})

Or possibly some default value should be supplied:

(defn lens
  ([k xf default]
   (lens k #(assoc %1 k %2) xf default))
  ([getter setter xf default]
   (let [rf' (xf (completing (fn [result item] item)))]
     (fn [rf]
       (fn
         ([] (rf))
         ([result] (rf result))
         ([result item]
          (let [value (getter item)
                new-value (rf' default value)
                new-item (setter item new-value)]
            (rf result new-item))))))))
(sequence
  (lens :x (filter even?) 0)
  data)
({:x 0} {:x 2} {:x 0} {:x 4} {:x 0} {:x 6})

If you find a use for lens, or improvements to it, please email me.