1.1 Introduction to Lenses
The lens library defines lenses , tools for extracting values from potentially-nested data structures. Lenses are most useful when writing in a functional style, such as the style employed by How to Design Programs, in which data structures are immutable and side-effects are kept to a minimum.
1.1.1 What are lenses?
A lens is a value that composes a getter and a setter function
to produce a bidirectional view into a data structure. This definition is intentionally broad—
> (define p (cons 1 2)) > p '(1 . 2)
> (car p) 1
> (cdr p) 2
With these three primitives, it’s very easy to create new pairs and subsequently extract values from them. However, it’s a little bit harder to update a single field in an existing pair. In a traditional Scheme, this could be accomplished by using set-car! or set-cdr!, but these mutate the original pair. To remain functional, we want to produce an entirely new pair with one of the fields updated.
Fortunately, this is quite easy to implement in Racket:
> (define (set-car p v) (cons v (cdr p)))
> (define (set-cdr p v) (cons (car p) v)) > (set-car (cons 1 2) 'x) '(x . 2)
> (set-cdr (cons 1 2) 'y) '(1 . y)
Both car-lens and cdr-lens, are provided by lens out of the box, along with some other shorthand lenses. For the full list, see Pair lenses.
This means that each field now has a pair of getters and setters: car/set-car and cdr/set-cdr. A lens just wraps up each of these pairs of functions into a single value, so instead of having four functions, we would just have two lenses: car-lens and cdr-lens. In fact, using the functions we’ve just written, we can implement these lenses ourselves.
> (define car-lens (make-lens car set-car)) > (define cdr-lens (make-lens cdr set-cdr))
To use a lens’s getter function, use lens-view. To use the setter function, use lens-set:
> (lens-view car-lens (cons 1 2)) 1
> (lens-set car-lens (cons 1 2) 'x) '(x . 2)
This, of course, isn’t very useful, since we could just use the functions on their own. One extra thing we do get for free when using lenses is lens-transform. This allows you to provide a procedure which will update the “view” based on its existing value. For example, we could increment one element in a pair:
> (lens-transform cdr-lens (cons 1 2) add1) '(1 . 3)
While that’s kind of cool, it still probably isn’t enough to justify using lenses instead of just using functions.
1.1.2 Why use lenses?
So far, lenses just seem like a way to group getters and setters, and as we’ve seen, that’s really all they are. However, on their own, this wouldn’t be very useful. Using (car p) is a lot easier than using (lens-view car-lens p).
Using plain functions starts to get a lot harder, though, once you start nesting data structures. For example, consider a tree constructed by nesting pairs inside of pairs:
> (define tree (cons (cons 'a 'b) (cons 'c 'd)))
Now, getting at a nested value gets much harder. It’s necessary to nest calls to get at the right value:
> (cdr (car tree)) 'b
Still, this isn’t too bad. However, what if we want to set a value? We could use our set-car and set-cdr functions from earlier, but if we try, we’ll find they don’t work quite right:
> (set-cdr (car tree) 'x) '(a . x)
Oops. We wanted to get back the whole tree, but we just got back one of the internal nodes because we used set-cdr on that node. In order to actually do what we want, we’d need to add a lot more complexity:
> (set-car tree (set-cdr (car tree) 'x)) '((a . x) c . d)
That’s what we need to do just for one level of nesting—
1.1.2.1 Lens composition
For more ways to construct compound lenses, see Joining and Composing Lenses.
In order to solve this problem, we can use lens composition, which is similar to function composition but extended to lenses. Just as we can create a compound getter function with the expression (compose cdr car), we can create a compound lens with the expression (lens-compose cdr-lens car-lens). With this, we produce an entirely new lens that can be used with lens-view, lens-set, and lens-transform, all of which do what you would expect:
> (define cdar-lens (lens-compose cdr-lens car-lens)) > (lens-view cdar-lens tree) 'b
> (lens-set cdar-lens tree 'x) '((a . x) c . d)
> (lens-transform cdar-lens tree symbol->string) '((a . "b") c . d)
Now the reason lenses are useful may begin to crystallize: they make it possible to not just get at but to actually functionally update and transform values within deeply-nested data structures. Since they are composable, it is easy to create lenses that can traverse any set of structures with nothing but a small set of primitives. This library provides those primitives.