1.2.2 Lenses on Key-Value Data
Many Racket data structures hold values that correspond to a given key. Lenses for accessing elements of these structures by their keys are provided.
1.2.2.1 Hash Tables
The Lens Reference has additional information on hash lenses.
Racket hash tables are simple key-value associations, and as a result, they only have one primitive lens constructor, hash-ref-lens. Given a key, it produces a lens which views the value associated with the key:
> (lens-transform (hash-ref-lens 'a) (hash 'a "Hello") (λ (s) (string-append s ", world!"))) '#hash((a . "Hello, world!"))
Note that hash-ref-lens’s signature differs from that of hash-ref in an important way: it does not accept a “failure result” if the key is missing from the hash. Instead, the lens always throws an error:
> (lens-view (hash-ref-lens 'not-a-key) (hash)) hash-ref: no value found for key
key: 'not-a-key
This may seem inconvenient, but this limitation is by design—
(let ([l (hash-ref-lens 'not-a-key "default")] [h (hash)]) (lens-set l h (lens-view l h)))
If hash-ref-lens accepted a default value, then the above expression would produce a new hash that was not equal? to the original target. Enforcing this property makes lenses easier to reason about, just as ensuring purity makes functions easier to reason about.
Of course, sometimes breaking purity is the easiest way to solve a problem, and similarly, sometimes breaking the lens laws is okay (though it should be avoided if possible). We could, if we wished, define our own hash lens that accepts a default value:
> (define (hash-ref-lens/default key failure-result) (make-lens (λ (h) (hash-ref h key failure-result)) (λ (h v) (hash-set h key v))))
With this custom, “naughty” lens, we can actually perform the example from above:
> (let ([l (hash-ref-lens/default 'not-a-key "default")] [h (hash)]) (lens-set l h (lens-view l h))) '#hash((not-a-key . "default"))
In addition to hash-ref-lens, hash-ref-nested-lens is provided, which assists in fetching values from nested hashes. It is defined in terms of hash-ref-lens and lens-compose, so it is just a shorter way of expressing the same concept:
> (lens-set (hash-ref-nested-lens 'a 'b 'c) (hash 'a (hash 'b (hash 'c "foo"))) "bar") '#hash((a . #hash((b . #hash((c . "bar"))))))
1.2.2.2 Dictionaries
The Lens Reference has additional information on dictionary lenses.
Racket dictionaries provide a generic interface for many kinds of key-value data-structures. They encompass hash tables, association lists, user-defined dictionaries, and even integer-keyed structures like vectors.
In practice, dictionary lenses work identically to lenses on hashes. The dict-ref-lens lens constructor creates a lens with a view that is the value associated with the lens’s key.
> (lens-transform (dict-ref-lens 'b) '((a . 1) (b . 2)) (λ (x) (* x 2))) '((a . 1) (b . 4))