/traversy

Multilenses for Clojure

Primary LanguageClojureOtherNOASSERTION

Traversy

Build Status

Clojars Project

An experimental encoding of multilenses in Clojure.

What are multilenses?

Simply put, multilenses are generalisations of sequence and update-in. Traversy's view and update accept a lens that determines how values are extracted or updated.

update-in provides a way to apply a function within a nested map:

(-> {:x 2 :y 4} (update-in [:x] inc))
=> {:x 3 :y 4}

This works great so long as the value you want to update can be addressed by a single path. However, there is no function for updating every value in a map in clojure.core. Here's how it looks with the all-values lens:

(require ['traversy.lens :refer :all])
    
(-> {:x 2 :y 4} (update all-values inc))
=> {:x 3 :y 5}

The same lens can also be used for viewing the foci:

(-> {:x 2 :y 4} (view all-values))
=> (2 4)

Lenses can be easily composed, so it's easy to build one that suits your particular data structure:

(-> [{:x 1 :y 2} {:x 2 :y 7}] (update (*> each all-values) inc))
=> ({:x 2 :y 3} {:x 3 :y 8})

And as viewing also composes:

(-> [{:x 1 :y 2} {:x 2 :y 7}] (view (*> each all-values)))
=> (1 2 2 7)

As lenses are first class, once you have one that suits your needs, you can name it and put it in a var.

Usage

See the examples.

There is API documentation, which describes the operations and provided lenses. This was generated by Codox.

Background

At the 2014 Clojure eXchange I gave a talk about Lenses in general, and Traversy specifically.

Laws

Lenses follow some rules that make them behave intuitively. The first two rules are the Traversal Laws. The final rule governs the relationship between update and view.

An update has no effect if passed the identity function:

(-> x (update l identity)) === x

Fusing two updates together is the same as applying them separately:

(-> x (update l f1) (update l f2)) === (-> (update l (comp f2 f1)))

update then view is the same as view then map:

(-> x (update l f) (view l x)) === (->> x (view l) (map f))

These should hold for any lens l that applies to a data structure x.

The second rule can be violated when the foci of a lens change after an update. An example of this is when only is used with a predicate and function that interact.

These two expressions should have the same value, but as incrementing an odd number makes it even, the second update in the first example has no targets:

(-> [1 2 3] (update (only odd?) inc) (update (only odd?) inc)) => [2 2 4]
(-> [1 2 3] (update (only odd?) (comp inc inc))) => [3 2 5]

Careful when doing this - and please document any lenses that have this behaviour as unstable. Traversy comes with three unstable lenses: only, maybe and conditionally.

FAQs

Aren't these just degenerate Lenses?

Yes! In fact, they're degenerate Traversals, with the Foldable and Functor instances and without the generality of traversing using arbitrary Applicatives.

Will updates preserve the structure of the target?

Yes. Whether you focus on a map, a set, a vector or a sequence, the structure of the target will remain the same after an update.

Can I compose these Lenses with ordinary function composition?

No. Unlike Haskell Lenses, these are not represented as functions. You can, however, use combine (variadic form *>) and both (variadic form +>) to compose lenses.

Can I use Traversy with ClojureScript?

Yup!

How do I run the tests?

Clojure: lein test

ClojureScript: lein test-cljs (you'll need phantomjs)

both: lein test-all

Is this stable enough to use in production?

Traversy is in production use on the project it originated from, but the API may yet change.