Skip to content

Modeling Access to Global State

blancas edited this page Feb 2, 2013 · 20 revisions

An alternative to accessing local state is to pass data around as arguments to functions. This helps to keep functions free of side-effects but involves more work, it can make the code more complex, and it doesn't scale well as the data increases. The Reader monad offers the ability to access data, supplied by its caller, at any point within the flow of control, while hiding away the boilerplate of passing things around. In exchange for boxing values inside the Reader data type, we get the simplicity of global data from pure functions.

The following code illustrates the mechanics of the Reader construct and use. There are two functions that trivially supply text for printing. A third one collects the text and prints it with a given margin, whose value is to be read as if it were global, though all these functions are pure. The code that runs the reader monad provides the value of the margin.

(defn text1 [] (reader "The quick fox"))
(defn text2 [] (reader "jumped over the lazy dog"))

(defn line []
  "makes a line with two parts and a margin"
  (monad [part1 (text1)
          part2 (text2)
          margin ask]
    (let [sp (clojure.string/join (repeat margin \space))]
      (reader (str sp part1 part2)))))

The monad macro provides the sequencing of Reader values and their bindings to the local variables. In this case, the variable part1 gets the string boxed in the Reader by function text1, and part2 gets the text from function text2. The variable margin gets the result of ask which is a predefined function that reads the Reader's environment value, in this case 8. Function line, in turn, makes a reader off the margin and the two lines of text. That is the Reader to be run by run-reader.

run-reader takes a Reader instance and the initial value for its environment. Callers can only supply a single value, but this may be any Clojure data structure. To complete the above example, here we call run-reader passing the Reader value return by line and the value 8 as the environment, which is meant to provide the line's margin. The result is the string created by str and returned as a Reader value.

(println (run-reader (line) 8))
;;        The quick foxjumped over the lazy dog

The monad macro will always return values of the same kind it works with. In the above example in function line, it uses Reader values (containing strings) and the environment value. Then it uses the reader constructor to return the result. Note that run-reader uses the result of line called with no arguments. Thus it is not apparent how ask is able to provide the value 8 to the variable margin. The answer is that the reader constructor makes high-order functions, so the one returned by line is the one that gets the environment value.