Transducers

 

Before We Start

:dependencies [[org.clojure/clojure "1.7.0-alpha1"]
               [org.clojure/core.async "0.1.338.0-5c5012-alpha"]]

Reduce

https://clojure.github.io/clojure/clojure.core-api.html#clojure.core/reduce

Reduce

function

Usage: (reduce f col)

             (reduce f val col)

Overly simple example:

(reduce + [1 2 3 4 5])

Reducing Function

This time less trivially:

(defn word-counts 
  [running-counts next-word]
  (update-in running-counts [next-word] 
    (fnil inc 0)))

user=> (word-counts {"a" 1} "a")
{"a" 2}

user=> (word-counts {"a" 1} "b")
{"b" 1, "a" 1}

user=> (reduce word-counts {} (str/split "that man is not that great , man" #" "))
{"," 1, "great" 1, "not" 1, "is" 1, "man" 2, "that" 2}

A reducing function takes the prior reduction, the next element, and returns a new redution

More Reducing Functions

(defn word-counts 
  [running-counts next-word]
  (update-in running-counts [next-word] 
    (fnil inc 0)))

Rich calls this fn signature
  whatever, input -> whatever

But this doesn't tell the full story, because whatever in the input can be a totally different thing than the whatever in the output.

Reducing FNs Are General

CAN EXPRESS ALL SORTS OF THINGS LIKE MAP AND FILTER

(defn myfilter 
  [predicate] 
  (fn [running next] 
    (if (predicate next) 
      (conj running next) 
      running)))

user=> (reduce (myfilter #(= 2 %)) [] [1 2 3 4 3 2 1])
[2 2]

Write map as an exercise.

But At The End of the Day, We're coupled!

(defn myfilter 
  [predicate] 
  (fn [running next] 
    (if (predicate next) 
      (conj running next) 
      running)))

 

Do We Have To Be?

(defn myfilter2
  [predicate] 
  (fn [reducing]
    (fn [running next] 
      (if (predicate next) 
        (reducing running next) 
        running))))

Back To Rich's function signatures:

A *transducer* is 
  reducing fn -> reducing fn
Or as he puts it
  (whatever, input -> whatever) -> (whatever, input -> whatever)

Simple Transducer Example

This satisfies the function signature Rich gave.

(defn lower-words 
  [reducing]
  (fn [whatever input]
    (reducing whatever (.toLowerCase input))))

(require '[clojure.string :as str])

user=> (reduce (lower-words word-counts) {} 
         (str/split "That man is not that great , man" #" "))
{"," 1, "great" 1, "not" 1, "is" 1, "man" 2, "that" 2}

Wait If these All Share a signature...

Then yes! We can comp them!

(reduce 
  ((comp
     lower-words (myfilter2 #(= "that" %))) 
    word-counts)
  {} (str/split "That man is not that great , man" #" "))


{"that" 2}

Are We There?

Yes.

 

Also note that we aren't coupled to a data type, that's in the reducing fn.  So we can generally apply this to all kinds of stuff.

Now With Transduce

(transduce (map #(.toLowerCase %)) 
           word-counts {} 
           (str/split "That man is not that great , man" #" "))
(time 
  (doseq [x (range 1000)] 
    (->> (str/split "That man is not that great , man" #" ") 
         (map #(.toLowerCase %))
         (reduce word-counts {}))))
"Elapsed time: 57.015 msecs"

(time 
  (doseq [x (range 1000)] 
    (transduce (map #(.toLowerCase %)) 
               word-counts {} 
               (str/split "That man is not that great , man" #" "))))

"Elapsed time: 47.94 msecs"

Also a performance gain (maybe)

Also works with core.async

(require '[clojure.core.async :as async])

(let [channel (async/chan 2 (map #(.toLowerCase %)))] 
  (async/go 
    (async/>! channel "Hello World!") 
    (prn (async/<! channel))))

#<ManyToManyChannel clojure.core.async.impl.channels.ManyToManyChannel@75acd47>
"hello world!"

You TRy One...

Write FizzBuzz with transducers.  This is not exactly an intuitive fit, but you can write a FizzBuzz, a Fizz, and a Buzz, and cond them for this.

 

 

 

Transducers

By Philip Doctor

Transducers

  • 1,704