Transducers

Before we finish up with our core.async portion of the book, it would be unwise of me not to mention what is coming up in Clojure 1.7 as well as how this affects core.async.

At the time of this writing, Clojure's latest release is 1.7.0-alpha5—and even though it is an alpha release, a lot of people—myself included—are already using it in production.

As such, a final version could be just around the corner and perhaps by the time you read this, 1.7 final will be out already.

One of the big changes in this upcoming release is the introduction of transducers. We will not cover the nuts and bolts of it here but rather focus on what it means at a high-level with examples using both Clojure sequences and core.async channels.

If you would like to know more I recommend Carin Meier's Green Eggs and Transducers blog post (http://gigasquidsoftware.com/blog/2014/09/06/green-eggs-and-transducers/). It's a great place to start.

Additionally, the official Clojure documentation site on the subject is another useful resource (http://clojure.org/transducers).

Let's get started by creating a new leiningen project:

$ lein new core-async-transducers

Now, open your project.clj file and make sure you have the right dependencies:

...
  :dependencies [[org.clojure/clojure "1.7.0-alpha5"]
                 [org.clojure/core.async "0.1.346.0-17112a-alpha"]]
...

Next, fire up a REPL session in the project root and require core.async, which we will be using shortly:

$ lein repl
user> (require '[clojure.core.async :refer [go chan map< filter< into >! <! go-loop close! pipe]])

We will start with a familiar example:

(->> (range 10)
     (map inc)           ;; creates a new sequence
     (filter even?)      ;; creates a new sequence
     (prn "result is "))
;; "result is " (2 4 6 8 10)

The preceding snippet is straightforward and highlights an interesting property of what happens when we apply combinators to Clojure sequences: each combinator creates an intermediate sequence.

In the previous example, we ended up with three in total: the one created by range, the one created by map, and finally the one created by filter. Most of the time, this won't really be an issue but for large sequences this means a lot of unnecessary allocation.

Starting in Clojure 1.7, the previous example can be written like so:

(def xform
  (comp (map inc)
        (filter even?)))  ;; no intermediate sequence created

(->> (range 10)
     (sequence xform)
     (prn "result is "))
;; "result is " (2 4 6 8 10)

The Clojure documentation describes transducers as composable algorithmic transformations. Let's see why that is.

In the new version, a whole range of the core sequence combinators, such as map and filter, have gained an extra arity: if you don't pass it a collection, it instead returns a transducer.

In the previous example, (map inc) returns a transducer that knows how to apply the function inc to elements of a sequence. Similarly, (filter even?) returns a transducer that will eventually filter elements of a sequence. Neither of them do anything yet, they simply return functions.

This is interesting because transducers are composable. We build larger and more complex transducers by using simple function composition:

(def xform
  (comp (map inc)
        (filter even?)))

Once we have our transducer ready, we can apply it to a collection in a few different ways. For this example, we chose sequence as it will return a lazy sequence of the applications of the given transducer to the input sequence:

(->> (range 10)
     (sequence xform)
     (prn "result is "))
;; "result is " (2 4 6 8 10)

As previously highlighted, this code does not create intermediate sequences; transducers extract the very core of the algorithmic transformation at hand and abstracts it away from having to deal with sequences directly.

Transducers and core.async

We might now be asking ourselves "What do transducers have to do with core.async?"

It turns out that once we're able to extract the core of these transformations and put them together using simple function composition, there is nothing stopping us from using transducers with data structures other than sequences!

Let's revisit our first example using standard core.async functions:

(def result (chan 10))

(def transformed
  (->> result
       (map< inc)      ;; creates a new channel
       (filter< even?) ;; creates a new channel
       (into [])))     


(go
  (prn "result is " (<! transformed)))

(go
  (doseq [n (range 10)]
    (>! result n))
  (close! result))

;; "result is " [2 4 6 8 10] 

This code should look familiar by now: it's the core.async equivalent of the sequence-only version shown earlier. As before, we have unnecessary allocations here as well, except that this time we're allocating channels.

With the new support for transducers, core.async can take advantage of the same transformation defined earlier:

(def result (chan 10))

(def xform 
     (comp (map inc)
           (filter even?)))  ;; no intermediate channels created

(def transformed (->> (pipe result (chan 10 xform))
                      (into [])))


(go
  (prn "result is " (<! transformed)))

(go
  (doseq [n (range 10)]
    (>! result n))
  (close! result))

;; "result is " [2 4 6 8 10]

The code remains largely unchanged except we now use the same xform transformation defined earlier when creating a new channel. It's important to note that we did not have to use core.async combinators—in fact a lot of these combinators have been deprecated and will be removed in future versions of core.async.

The functions map and filter used to define xform are the same ones we used previously, that is, they are core Clojure functions.

This is the next big advantage of using transducers: by removing the underlying data structure from the equation via transducers, libraries such as core.async can reuse Clojure's core combinators to prevent unnecessary allocation and code duplication.

It's not too far fetched to imagine other frameworks like RxClojure could take advantage of transducers as well. All of them would be able to use the same core function across substantially different data structures and contexts: sequences, channels, and Obervables.

Tip

The concept of extracting the essence of computations disregarding their underlying data structures is an exciting topic and has been seen before in the Haskell community, although they deal with lists specifically.

Two papers worth mentioning on the subject are Stream Fusion [11] by Duncan Coutts, Roman Leshchinskiy and Don Stewart and Transforming programs to eliminate trees [12] by Philip Wadler. There are some overlaps so the reader might find these interesting.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.93.64