Tom MacWright

tom@macwright.com

Simple undo and redo in ClojureScript

I previously wrote about implementing undo & redo in JavaScript using a library called Immutable.js. That’s the approach I use for daily work as part of JavaScript applications.

Mutability in JavaScript

But JavaScript’s Array and Object types aren’t designed to be immutable, so it’s necessary to use a library like Immutable.js or be extremely careful about methods that cause mutation. Using Immutable.js gives you the power of immutable objects, but they aren’t the same as normal JavaScript objects: you need to use Immutable.fromJS() to convert JS values to immutable values, and value.toJS() to convert them back.

The new version of JavaScript, ES6, introduces the const keyword, but it doesn’t make objects immutable, only their variable binding. JavaScript also has an Object.freeze method to guard against mutation, but it’s shallow: sub-objects are still mutable.

In my opinion, this is one of the more unfortunate parts of JavaScript. Immutability is a powerful feature that I think makes code easier to understand, test, and modify. Immutable.js is an excellent library, but using a library for basic datastructures is awkward: in order to use any third-party module with your pleasant immutable datatypes, you’ll need to convert them to plain JavaScript objects, run a function, and then convert them back. For basic types - the fundamental building blocks of code - it would be great to have better defaults.

Which brings us to ClojureScript

ClojureScript

ClojureScript is a Lisp-like language that compiles to JavaScript. Unlike writing code with CoffeeScript or Babel, code in ClojureScript is entirely a new environment: function calls, datatypes, and the basic semantics of the language are different from JavaScript: they’re Clojure’s behaviors, not JavaScript’s.

ClojureScript’s basic datatypes like vectors and hash-maps are implemented on top of JavaScript: they’re more similar to the data structures used by Immutable.js than they are to native Array and Object types. Under the hood, they do a lot of the same tricks for for performance. ClojureScript’s types are designed with immutability as a basic attribute, so the language doesn’t have JavaScript’s problem of immutability-as-an-addon.

So I decided to reimplement my undo/redo example in ClojureScript. Note that this is my first ClojureScript project, so the code might not be idiomatic: I’d love suggestions and PRs for clarity: it’s open source as undo-redo-cljs.

The principles are the same as before:

  • Data is immutable. It is never mutated in-place.
  • Changes to data are encapsulated into operations that take a previous version and return a new one.
  • History is represented as a list of states, with past on one end, the present on the other, and an index that can back up into ‘undo states’.
  • Modifying data causes any future states to be thrown away.

Operations are functions that create new versions

All operations in ClojureScript return a new version of data:

user=> (conj [1 2] 3)
[1 2 3]

History is a list with an index

As simple as that: generally the start of the array is the starting state the historyIndex indicates where in history you are.

;; our app state is composed of a vector
;; of history entries and an index that points
;; to the current version
(defonce app-state (atom {
  :historyIndex 0
  :history [[]]
}))

For instance, in the middle of execution, history might look like

;; you've created one step in history and drawn dot1
{
  :historyIndex 1
  :history [[], [dot1]]
}
;; so
;; (get-in @app-state [:history (:historyIndex @app-state)]))
;; will return [dot1]

;; and if you undo you get to the state
{
  :historyIndex 0
  :history [[], [dot1]]
}
;; so
;; (get-in @app-state [:history (:historyIndex @app-state)]))
;; will return []

Operations append new versions to the list of history

In order to increment historyIndex, push a new version on the stack, and run an operation, we write a helper function like this:

;; create a new version of dots by passing in a function that
;; takes the vector of dots and modifies it in some way
(defn new-version [creator]
  ;; delete any future
  (delete-future)
  ;; point the history index at this new version.
  ;; swap! is a function that actually replaces a value
  ;; with a new value: we use it to replace the immutable
  ;; app-state variable with a new value when the state changes
  (swap!
    app-state update-in [:historyIndex] inc)
  ;; then create the new history entry. creator is the function
  ;; passed in by the caller of new-version, conj conjoins
  ;; the list old-history with the new history entry, and peek
  ;; selects the most recent history entry to pass to the creator
  ;; method
  (swap!
    app-state update-in [:history]
      (fn [old-history]
        (conj old-history (creator (peek old-history))))))

Here’s that method that deletes the future if a user makes a change after undoing changes.

;; remove any future history entries. if the user makes a change,
;; goes back in history, and then does something else, we should get
;; rid of the alternate future but truncating the :history list
;; to length :historyIndex
(defn delete-future []
  ;; since clojurescript's subvec function will trigger an index error
  ;; if truncate the list to an invalid length, we check that the future
  ;; needs to be delete first
  (if
    (not=
      (count (:history @app-state))
      ;; since vectors are zero-indexed and we're comparing
      ;; with length, we have to decrement the historyIndex
      ;; by 1
      (dec (:historyIndex @app-state)))
    (swap!
      app-state update-in [:history] (fn [old-history]
        ;; subvec returns a slice of history from 0 to :historyIndex
        (subvec old-history 0 (inc (:historyIndex @app-state)))))))

Like in the JavaScript solution, the historyIndex decides whether you can undo or redo changes.

Here’s our code for getting the current dots:

;; a shortcut to ge the current dots at the current history
;; state
(defn current-dots []
  (get-in @app-state [:history (:historyIndex @app-state)]))

And for adding a new dot when a user clicks the background:

(events/listen!
  ;; dots is the grey background div.
  ;; when it's clicked, a function is called
  ;; that adds a new dot to the list
  (domina/by-id "dots") :click
  (fn [evt]
    (new-version (fn [old-history]
      ;; conj is like JavaScript's concat method
      (conj old-history {
        :x  (:offsetX evt)
        :y  (:offsetY evt)
        ;; notice that this is a call to JavaScript
        ;; directly: it's equivalent to (new Date()).getTime()
        :id (.getTime (js/Date.))
      })))
    ;; and then draw the dots including this new one
    (draw (current-dots))))

Finally here’s the method for drawing dots:

(defn draw [dots]
  ;; remove all existing dots from the board
  ;; so we can redraw them
  (domina/destroy! (domina/by-class "dot"))
  ;; ClojureScript's map method, which works like Array.map
  ;; in JavaScript, is lazy. Until the results are actually read,
  ;; the operations aren't completed. So we use the doall method
  ;; to force the map to execute. This could probably be expressed
  ;; in a more idiomtic way: suggestions welcome!
  (doall
    (map
      (fn [dot]
        ;; append a div element for each dot, concatenating a string
        ;; of HTML using the str method, and retrieving properties like
        ;; (:x dot), which in JS would be dot.x or dot.get('x') with
        ;; Immutable.js objects
        (domina/append!
          (domina/by-id "dots")
          (str "<div id=" (:id dot) " "
            "style='left:" (:x dot) "px;"
            "top:" (:y dot) "px"
            "' class='dot'></div>"))
        ;; this method makes the behavior such that if you click
        ;; a dot, it removes it. this listens to the click event
        ;; on each dot, selecting the div by its id
        (events/listen!
           (domina/by-id (str (:id dot))) :click
           ;; we use partial here like we'd use closures or
           ;; currying in JS: partial pre-fills the id argument to
           ;; the function with a value, and waits until
           ;; an event calls the function again with the click event
           ;; value
           (partial (fn [id evt]
               ;; prevent this event from falling through to the
               ;; element under the dot
               (events/stop-propagation evt)
               ;; create a new version of the dots
               ;; vector, filtering out the dot whose id
               ;; matches this dot's
               (new-version (fn [old-history]
                 (filterv
                   (fn [dot] (not= id (:id dot)))
                   old-history)))
               ;; redraw the picture without the deleted dot
               (draw (current-dots)))
             (:id dot))))
    dots)))

Finally, moving forward and backward through history, here’s undo & redo.

(events/listen!
  (domina/by-id "redo") :click
    (fn [evt]
      ;; move the historyIndex forward
      (swap! app-state update-in [:historyIndex]
        (fn [old-index]
          ;; make sure that if we're already at the end of
          ;; history, we don't go forward
          (min (inc old-index) (dec (count (:history @app-state))))))
    (draw (current-dots))))

(events/listen!
  (domina/by-id "undo") :click
    (fn [evt]
      (swap! app-state update-in [:historyIndex]
        (fn [old-index]
          ;; make sure that if we're already at the beginning of
          ;; history, we don't go backward
          (max (dec old-index) 0)))
    (draw (current-dots))))

Here’s the finished example: click on the gray rectangle to draw a dot, click a dot to delete it, and click undo or redo to travel through history.

code for the completed example: tmcw/undo-redo-cljs

Discussion

This is my first completed ClojureScript project, so the code isn’t perfect and it took days to write. Writing ClojureScript was a really interesting experience: I’d recommend it.

  • Figuring out that map calls were lazy took me many hours
  • ClojureScript’s error reporting happens on a lot of levels: runtime errors with tracebacks in nicely-sourcemapped ClojureScript, compiler errors reported beautifully, tracebacks in ugly uncommented JavaScript, occasional silent failure. It’s not a perfect system, but the extremely helpful errors show that someone’s dedicating time to making it better.
  • Lots of ClojureScript documentation is focused on Om or Reagent. I kept this example framework-free to follow the example of its JavaScript version, so it only uses domina for DOM manipulation helpers and event binding.
  • The cljs cheatsheet was incredibly helpful.
  • Next time I need to install vim-fireplace: as I slowly learned, a lot of documentation is embedded in code and can be pulled out by an editor or IDE extension.
  • Immutable.js is incredibly similar to Clojure’s datastructures: even the method names line up, like Clojure’s update-in and Immutable.js’s updateIn. This isn’t a coincidence: Immutable.js is directly inspired by Clojure’s persistent datastructures.
  • Learning is hard and always worth it.