1
1
Fork
You've already forked schemacs-lens
0
Functional lenses for R7RS Scheme
  • Scheme 100%
2026年06月08日 07:04:08 +03:00
schemacs copy schemacs/test.sld 2026年06月08日 06:59:42 +03:00
guix.scm Use local directory as origin for guix.scm 2026年06月08日 06:59:57 +03:00
LICENSE rename gypsum->schemacs 2026年06月07日 19:01:06 +03:00
README.md README.md: add command for Guix 2026年06月08日 07:04:08 +03:00

schemacs-lens

Functional lenses for R7RS Scheme, inspired by Haskell. Functional lenses provide a way to combine getters and setters into a first-class value that which be easily composed together to inspect and update arbitrarily complex data structures.

In Scheme (and other Lisp-family languages), programmers typically make use of pattern matching to extract important information from data structures. However pattern matching can occasionally be a bit too cumbersome for selecting data that is somewhat regular in structure, such as objects structured according to the XML or JSON protocols. In other, more popular languages like Python or JavaScript, you would simply write an expression such as:

 // In JavaScript, you would write code like so:

 var result =
 input["people"].findBy("first_name", "Alice")["salary"];
 costs.monthly.chart.scale = {x: 1.25, y: 1.33};

What can be done with one line of code in JavaScript may take three or four lines of pattern matching code in Scheme, one line for each indirection through the data.

An alternative to pattern matching is to make use of a "Meta-Object Protocol" (MOP) implementation and map your data structures to objects, then access the fields of these objects using ordinary object method composition, just like how Python or JavaScript work. Various MOPs exist for Scheme, some may even provide easy ways to wrap hash tables and vectors into objects that can easily keys or indicies.

Yet another alternative is functional lenses (this library). It works by defining few primitive "unit lenses", and then more complex lenses can be constructed by composing together these unit lenses. The resulting code looks much more similar to something with which a JavaScript or Python programmer would be comfortable using. The above JavaScript example could hypothetically be rewritten to look more like the following:

 ;; With Scheme functional lenses, the above JavaScript example
 ;; *could* perhaps be written in this way:
 (define result
 (view input 'people (find-by 'first-name "Alice") 'salary))
 (lens-set '((x . 1.25) (y . 1.33)) costs 'monthly 'chart 'scale))

There are also similarities in functionaltiy between lenses and SRFI-17 "Generalized set!", in which a set! is redefined as a macro which inspects its first argument and uses it to construct an updating procedure. Then there are well-defined mechanisms, such as defining "accessors" within a MOP framework, to parameterize the set! expression so that Scheme programmers may declare their own set! expressions for arbitrary data structures without needing to write their own macros.

However functional lenses, as the name implies, do not use macros, rather they are entirely functional. In fact, it is possible to declare purely functional lenses as well, in which data structures are not mutatated but replaced with slightly altered copies (more on this below).

Building and testing

This library has been used with the following scheme implementations:

Unit tests currently only pass on Guile, and also in the Gambit interpreter (the Gambit compiler crashes when using lenses). Stklos mostly passes all unit tests with a few not passing. MIT/GNU Scheme does not provide the SRFI-64 testing framework, but running each individual tests by hand seems to produce expected results.

It should not be too difficult to port to other R7RS Scheme implementation as well.

To spawn Guile with schemacs-lens available using Guix

guix shell -f guix.scm guile@3.0.11 -- guile

To run the test suite in Guile:

scheme@(guile-user)> (load "schemacs/lens-tests.scm")

Simple Example

In the simplest form, you can use lenses to view, update, or set with lens-set items within "records", which are data structures such as pairs (cons cells), lists, vectors, and hash tables. You can use literal integers, strings, or symbols as keys. Lenses "focus" on an element of the record, and you can return that element or updated it. Lenses "compose", meaning you can apply several lenses from right to left in a single statement, and each lens will focus on the record that was focused by the previous lens.

Lets take this vector v as an example:

(define v (vector 'a 'b (vector 'c 10) (cons 'e 'f)))

Use the view operator to inspect records:

(view v 0) ;; displays a
(view v 2 0) ;; displays c
(view v 3 =>car) ;; displays e

Use the lens-set operator to update the record:

(lens-set "hello" v 0)
 ;; displays #("hello" b #(c 10) (e . f))
(lens-set "world" v 2 0)
 ;; displays #("hello" b #("world" 10) (e . f))
(lens-set "scheme" v 3 =>cdr)
 ;; displays #("hello" b #("world" 10) (e . "scheme"))

Use the update operator to update the record

The update operator applies a single value to an updater procedure, but unlike update&view, the updater for update returns a single value to update the record.

(update (lambda (x) (* x 10)) v 2 1)
 ;; displays #("hello" b #("world" 110) (e . "scheme"))

Use the update&view operator to update the record

The update&view operation applies a procedure that update a value within the record that is focused upon by the lens. It is important to remember that the update&view procedure must take one value (the focused value) and return 2 values, the second value being an arbitrary other value which you may wish to derive from the focused value.

(update
 (lambda (x)
 (let ((x (+ 1 x)))
 (values x (string-append "count = " (number->string x)))))
 v 2 1)
 ;; displays 2 values:
 ;; 1. #("hello" b #("world" 11) (e . "scheme"))
 ;; 2. "count = 11"

Use the update& operator to update the record

Sometimes you define an updating procedure that returns two values that you can use with various lenses, but you want to use it with update&view without wrapping your updating procedure to return only 1 values. The update& operator takes an ordinary updater that returns 2 values and discards the second value, returning only the updated record.

(define (my-updater val)
 (cond
 ((string? val) (values val (string-length val)))
 ((number? val) (values (+ 1 val) #f))))
(update&view my-updater v 0)
 ;; displays 2 values:
 ;; 1. #("hello" b #("world" 110) (e . "scheme"))
 ;; 2. 5, the length of the string at index 0
(update& my-updater v 2 1)
 ;; displays #("hello" b #("world" 112) (e . "scheme"))
;; We can use `my-updater` with both `update&view` and `update&`
;; without adapting `my-updater` for use with `update`.

Use the lens-swap opreator to return the previous value

Somtimes you want to set a new value for the focus of the record, but return the old value. The swap procedure does this by returning two values: the updated record, and the previously focused value.

(lens-swap 5 v 2 1)
 ;; displays 2 values:
 ;; 1. #(hello" b #(world 5) (e . "scheme"))
 ;; 2. 112

A more complete example

Here is a larger example, showing the imports, and how to construct data strucutres that can be inspected and updated with lenses.

(import
 (scheme base)
 (only (scheme write) display write newline)
 (schemacs lens) ;; <----- import declaration --------
 )
(cond-expand
 ;; Lenses uses R7RS vectors, but hash tables is implementation
 ;; specific. SRFIs 69 and 43 are the older vector and hash table
 ;; standard APIs, SRFIs 133 and 125 are the newer vector and
 ;; hash table standard APIs.
 ((or guile gambit)
 (import
 (only (srfi 69) alist->hash-table) ; old hash tables
 (only (srfi 43) vector) ; old vectors
 ))
 ;; Otherwise, we use SRFI-133 for vectors, SRFI-124 for hash tables
 (else
 (import
 (only (srfi 125) alist->hash-table) ; hash tables
 (only (srfi 133) vector) ; vectors
 ))
 )
(define (main)
 ;; ==== EXAMPLES ====
 ;; (The "hashtab" example data structure is defined below)
 
 ;; --- Use the `view` procedure to view elements ---
 (display (view hashtab "zero")) (newline)
 ;; displays 0
 (display (view hashtab "AB" "A")) (newline)
 ;; displays "go-left"
 (display (view hashtab "AB" "vec" 0 "C")) (newline)
 ;; displays "go-up"
 (display (view hashtab "AB" "vec" 2)) (newline)
 ;; displays #f
 ;; --- Use the "lens-set" procedure to update elements ---
 ;;
 ;; The order of the arguments are:
 ;;
 ;; 1. the value to be inserted into the data structure
 ;; 2. the data structure to be updated
 ;; 3. zero or more lenses which can update the data structure
 ;;
 ;; In Python or JavaScript you would write:
 ;; 
 ;; hashtab["AB"]["vec"][2] = [20,30,40]
 ;;
 ;; With lenses, you write:
 (lens-set '(20 30 40)
 hashtab "AB" "vec" 2)
 ;; hashtab["two"] = 222
 (lens-set 222
 hashtab "two")
 ;; print(hashtab["two"])
 (display (view hashtab "two")) (newline)
 ;; displays 222
 ;; print(hashtab["AB"]["vec"][2])
 (display (view hashtab "AB" "vec" 2)) (newline)
 ;; displays (20 30 40)
 ;; --- Use the "update" procedure to update elements and return a value ---
 (let-values
 (((hashtab sum)
 (update (lambda (items)
 (values (cons 10 items) (apply + items)))
 hashtab "AB" "vec" 2))
 )
 (display sum) (newline)
 ;; displays 90
 (display (view hashtab "AB" "vec" 2)) (newline)
 ;; displays (10 20 30 40)
 ))
(define hashtab
 (alist->hash-table
 `(("zero" . 0)
 ("one" . 1)
 ("two" . 2)
 ("AB" .
 ,(alist->hash-table
 `(("A" . "go-left")
 ("B" . "go-right")
 ("vec" .
 ,(vector
 (alist->hash-table ;; at index 0
 `(("C" . "go-up")
 ("D" . "go-down")))
 (alist->hash-table ;; at index 1
 '(("E" . "turn-around")
 ("F" . "jump")))
 #f ;; at index 2
 ))))))))
(main)

Purely functional lenses

As mentioned earlier, it is possible to declare purely functional lenses as well, in which data structures are not mutatated but replaced with slightly altered copies (more on this below). This can sometimes make your code much easier to optimize for compilers that perform an SSA pass, because by using pure-functional lenses, more of your code is essentially already in SSA-form to begin with.

Here is an example of a purely function lens for car and cdr which does not use set-car! or set-cdr!, rather it replaces the cons cell with an updated copy:

(define =>car
 (unit-lens
 (lambda (cell) ;; ---------- getter ----------
 (car cell)
 )
 (lambda (cell new-value) ;; ---------- setter ----------
 (cons new-value (cdr cell))
 )
 (lambda (updt cell) ;; ----- in-place update ------
 (let-values
 (((new-car result) (updt (car cell))))
 (values (cons new-car (cdr cell)) result)
 ))
 '=>car ;; <- you can optionally label the lens for better error messages
 ))

By convention, lenses that are not pure, that is lenses which mutate the data structure when applied to update or lens-set, should have a bang (!) suffixed to their name. There are a few examples of this:

  • vector-index! mutates a vector at the given index
  • hash-key! mutates a hash table at the given key

There are also vector-index*! and hash-key*! which are "non-canonical" versions of the above two lenses, read the section called "Canonical lenses" below for more about this.

Using "endofunctors" endo-update, endo-set, and endo-view

These operators constructs an "endofunctor" from a LENS. An endofunctor is a term borrowed from category theory and Haskell, it is a function or procedure that takes a single value in one "category" (the category of Scheme objects) and returns a single argument in the same category, and returns a single updated value of the same type. Endofunctors are easily composable with higher-order functions, such as the apply-chain procedure from SRFI 235 "Combinators".

(define v (vector 1 2 3 4))
(apply-chain
 (endo-update (lambda (x) (* 100 x)) 3)
 (endo-set 9 v 1)
 (endo-set 8 v 2)
 (endo-set 7 v 3)
 v)
 ;; displays #(1 9 8 700)

The endo-view operator is so useful that it is shortened to endo. One interesting property of endo-view is that applying many endo-view in a chain using apply-chain performs the same function as a lens composed of those lenses. For example:

((apply-chain (endo =>a) (endo =>b) (endo =>c)) record)
;; performs the same function as:
((endo (lens =>a =>b =>c)) record)
;; and also performs the same function as:
(view record (lens =>a =>b =>c))

The endo-view operator can also be used to construct updater functions. For example, if you want to update a list so that it becomes the cdr of the list, you can use the =>cdr lens. You can then apply this lens as an endo-view using the update on =>find-tail to remove an element from a list.

(update
 (endo =>cdr) ;; if a tail is found update it with the cdr of the tail
 '(0 2 4 6 7 8 10 12)
 (=>find-tail odd?) ;; finds a list starting with an odd number
 )
 ;; removes the first odd numberfrom the list
 ;; '(0 2 4 6 8 10 12)

Use lens-set! and lens-swap! with mutating lenses

Ordinarily lens-set returns only the updated record, and lens-swap returns two values (the update record, and the previous value prior to update). Sometimes, if you are going to be mutating a record which you have bound to a variable, you may not be interested in receiving the updated record. Perhaps sometimes you want to update and return only the value that was stored. For this you can use lens-set! and lens-swap!:

(lens-set! 15 v 2 0)
 ;; displays 10
 ;; The values that was just stored at index 1
 ;; It does not return the vector, though the updated vector
 ;; would look like this:
 ;; #(hello" b #(world 15) (e . "scheme"))
(lens-swap! 5 v 1)
 ;; displays b
 ;; It does not return the vector, though the updated vector
 ;; would look like this:
 ;; #(hello 5 #(world 15) (e . "scheme"))

Warning! the lens-set! and lens-swap! operators do not compose well. With lens-set (without the !) the record in is the record out slightly changed, so you can compose many lens-set operations to perform many updates on a record. However with lens-set! (with the !) you cannot do this since the record is not returned. Be careful to only use lens-set! and lens-swap! at the top level of a begin procedure statement or similar places, using them with apply-chain would simply do the wrong thing.

Lenses for lists

There are a few useful, pre-defined lenses for working with list objects:

  • =>car and =>cdr are pure canonical lenses for pair objects (a.k.a. "cons cells"). If you use these lenses to update a pair that does not exist, the pair is created with the updated value on one side of the pair and #f on the other side.

  • =>find-tail: when used with view this procedure is identical to find-tail library from SRFI-1, which is in turn similar to the member function except you supply your own predicate to search for elements. When used with update or lens-set, the pair found is updated. You can cut-off a list by updating it to '(), you can delete the element by updating it to the cdr of the found pair, you can insert however many more elements you choose.

  • =>find is similar to find from SRFI-1. It does the same thing as:

    (lens (=>find-tail some-predicate) =>car)
    

    ...that is, it focuses on the car of the found pair.

  • =>bring is similar to =>find, except when updating the list, the updated pair is moved to the head of the list. This is useful if you want to perform repeated updates on a list for which ordering is not important. Updating an element brings it to the head of the list, updating it again happens in O(1) time because it is at the head of the list.

  • =>assoc, =>assv, and =>assq are similar to the standard Scheme APIs assoc, assv, and assq respectively, and these lenses behave identically to the Scheme standard APIs when applied to view. These are for viewing and updating the values of an assocaition list.

  • =>assoc-by is similar to =>assoc except you must supply your own predicate, rather than the default on the equal? procedure, to search for keys. You must also supply a finding lens, either =>find or =>bring, which can change the updating behavior. Here is a simple example:

    (define numlist '((zero . 0) (one . 1) (two . 2) (three . 3) (four . 4)))
    (define (x100 n) (* n 100))
    (set! numlist
     (update x100 numlist (=>assoc-by =>find eq? 'two)))
    ;; using `=>find` with `=>assoc-by` updates the element in place
    numlist; -> '((zero . 0) (one . 1) (two . 200) (three . 3) (four . 4))
    (set! numlist
     (update x100 numlist (=>assoc-by =>bring eq? 'four)))
    ;; using `=>bring` with `=>assoc-by` updates the element and
    ;; brings it to the head of the list.
    numlist; -> '((four . 400) (zero . 0) (one . 1) (two . 200) (three . 3))
    

Record types

Unfortunately, there are not yet any macros that automatically derive lenses for record types, so you must define the record type lenses by hand.

By convention, lens symbols are always prefixed with =>. Non-canoncial lenses (discussed later) are suffixed with a asterisk (*), and mutating lenses are suffixed with a bang (!). Since record-unit-lenses are typically mutating, it is most correct to name them suffixed with !. However we will make our color lenses canonical later, so they will be suffixed with *! in this example:

(import
 (scheme base)
 (only (scheme write) display write newline)
 (schemacs lens)
 )
(define-record-type <color-type>
 (make<color> r g b)
 color-type?
 (r red set!red)
 (g green set!green)
 (b blue set!blue))
;; Here we define the lenses:
(define =>red*! (record-unit-lens red set!red))
(define =>green*! (record-unit-lens green set!green))
(define =>blue*! (record-unit-lens blue set!blue))
(define *yellow*
 (let ((c (make<color> 0 0 0)))
 (lens-set 255 c =>red*!)
 (lens-set 255 c =>green*!)
 c))

Using literals and constants as lenses

You may have noticed that the view, lens-set, and update operators can take integers to focus on vectors, and string and symbols focus on hash tables. However for record types like our <color-type> in the example in the previous section, we must declare a lens object using record-unit-lens. This is because the lens operators recognize primitive values like integers and strings and automatically constructs an actual lens object to use these values.

In this table, we can see how primitive values are automatically converted to lens objects:

type ex. value as a lens object
integer? 123 (=>vector-index*! 123)
string? "hello" (=>hash-key*! "hello")
symbol? 'hello (=>hash-key*! 'hello)

What if we want the symbols 'red, 'green, and 'blue to automatically refer to the respective fields of our <color-type> from the example in the previous section? To do this, we can use the declare-rule/index->lens API at the top level of our program, typically right after defining the record-unit-lenss. Once we have done this, the lens operators view, lens-set, and update can automatically construct a lens such as =>red*! or =>green*! from symbols.

To declare a new translation rule, pass two arguments:

  1. the type of index you want to use as a lens, must be 'symbol, 'string, or 'integer.

  2. a predicate that returns #T for records applied to it, for example our color-type? predicate.

  3. a lens constructor that constructs a lens using the symbol, string, or integer.

(define (symbol->color-lens sym)
 ;; This predicate converts a symbol to one of the color
 ;; component lenses we defined earlier.
 (cond
 ((or (eq? sym 'r) (eq? sym 'red)) =>red)
 ((or (eq? sym 'g) (eq? sym 'green)) =>green)
 ((or (eq? sym 'b) (eq? sym 'blue)) =>blue)
 (else (error "not a color field name, must be 'red, 'green, or 'blue" sym))
 ))
(declare-rule/index->lens
 'symbol ;; 1. the type of value that can be used as an index
 color-type? ;; 2. the predicate to identify the record
 symbol->color-lens ;; 3. the procedure to convert a symbol to a lens
 )
;; Now we can use ordinary symbols to access fields of a `<color-type>`.
(define (main)
 (lens-set 0 my-painting =>bg-color 'red)
 ;; In this example, we can compose a lens object `=>bg-color` with
 ;; a lens symbol `'red`.
 )

Remember that declare-rule/index->lens really should be evaluated at the top level of a Scheme program or library, and it is best to decalre these rules right after the define-record-type form and the record-unit-lens lenses.

Inspecting lens index rules with get-rule-declaration/index->lens

To inspect what kind of lens indexing rules have been declared, you can use the get-rule-declaration/index->lens procedure. This procedure returns the indexing rules that have been declared for a particular index type. It takes two arguments:

  • INDEX-TYPE: which must be one of 'INTEGER, 'STRING, or 'SYMBOL
  • RECORD: a record object that you want to inspect with an index.

For example if you have a vector-like object "vec32" and want to see if it is possilbe to index this object with an integer-valued index, you can evaluate:

(get-rule-declaration/index->lens 'integer vec32)

...and if that returns non-null, then it is OK to index your "vec32" with the expression:

(view vec32 5)

Canonical lenses

The hash key lenses do not create new keys if they do not already exist. Also, if a structure such as a hash table becomes empty after an update, the empty hash table remains rather than being deleted and replaced with #f.

However it is possible to create "canonical" lenses that construct new data structures (where possible) if lens-set or update is called on a structure, and remove structures that have become empty after an update. By convention, if a lens has both a canonical and non-canonical variant, the non-canonicalized lenses are suffixed with *. Lenses can be both canonical and non-pure, i.e. also mutate data on update. By convetion, lenses that are both canonical and mutating should be named with a symbol suffixed with *!, such as:

  • =>vector-index*!: a non-canonical lens that mutates a vector.
  • =>hash-key*!: a non-canonical lens that mutates a hash table.
  • =>hash-key! : a canonical lens that mutates a hash table.
  • =>hash-key: a canonical lens that returns a new table if updated

To construct a canonical lens, you must supply two procedures to be composed with the lens:

  1. a procedure to construct a new container when that container is #f.

  2. a predicate to determine if the container is empty, so that if the container is empty after an update, the whole container can be replaced with #f.

In the following example, we canonicalize the <color-type> from the previous example such that non-existent colors construct a black color.

(define (new-color) (make<color> 0 0 0))
(define (black? c) (or (not c) (= 0 (red c) (green c) (blue c))))
(define =>red! (=>canonical =>red*! new-color black?))
(define =>green! (=>canonical =>green*! new-color black?))
(define =>blue! (=>canonical =>blue*! new-color black?))
;; --- Now, setting on `#f` applies value to a `new-color` ---
(let*((cyan (lens-set 255 #f =>green!)) ;; can set on #f, no problem
 (cyan (lens-set 255 cyan =>blue!)))
 (display cyan)(newline) ;; #<<color-type> r: 0 g: 255 b: 255>
 ;; Setting the color to black with our canonical lenses will return
 ;; #f in place of a color.
 
 (let*((cyan (lens-set 0 cyan =>green!))
 (cyan (lens-set 0 cyan =>blue!)))
 (display cyan)(newline)
 ;; displays #f
 )

The =>self lens

In functional programming, it is frequently useful to have an "identity" function. The =>self lens is the identity lens, it operates on the data structure itself. Here is an example of where it might be useful:

;; Suppose "dim" is a symbol that is defined as one of '1D, '2D, or '3D.
(define origin-point
 ;; The "origin-point" is a number if "dim" is 1D, but if
 ;; "dim" is 2D or 3D, the "origin-point" is a vector.
 (cond
 ((eq? dim '1D) 64.0)
 ((eq? dim '2D) #(8.0 8.0))
 ((eq? dim '3D) #(4.0 4.0 4.0))
 (else (error "invalid dimensionality" dim))
 )
(define =>x
 ;; We define the lens "=>x" to access the first cell of the vector
 ;; if "dim" is 2D or 3D, but if "dim" is 1D the lens is "=>self".
 (cond
 ((eq dim '1D) =>self) ;; <------------------ using "=>self" here
 (else  (=>vector-index*! 0))))
(display (view origin-point =>x))
 ;; displays 64.0 if dim is 1D, 8.0 if dim is 2D, or 4.0 if dim is 3D.

Using the =>guard lens

Sometimes you want to construct a lens that behaves differently depending on the object being inspected. For example, if you want to construct a lens called =>initial which updates the first element of a vector, the car of list, or the empty string "" key of a hash table, you can use =>guard to do this. The guard takes a list of "rules", each rule is a list where the first element is a predicate, and every element after it are lenses to be applied to view, update, or lens-set.

(define =>initial
 ;; This one lens can update the "initial" values of three different
 ;; data types.
 (=>guard
 (list vector? (=>vector-index*! 0))
 (list hash-table? (=>hash-key*! ""))
 (list (lambda (pair) (or (pair? pair) (null? pair))) =>car)))
(lens-set "Hello" (vector #f #f #f) =>initial)
 ;; displays #("Hello" #f #f)
(hash-table->alist
 (lens-set "Hello" (make-hash-table) =>initial))
 ;; displays (("" . "Hello"))
(lens-set "Hello" '() =>initial)
 ;; displays ("Hello" . #f)

Using =>on-update with lens-set

Suppose you want to define a lens that always sorts a vector every time an element is inserted. There are other situations where you may want to always perform some mutating action on an object whenever the object is updated. This is the purpose of the primitive =>on-update lens.

(lens-set 100
 vec (=>on-update (=>vector-index*! n) vector-sort))

Debugging

Sometimes you simple do not have access to useful debugging tools, your only recourse is to do "print debugging" where you write lots of (display ...) or (write ...) statements everywhere and analyze the log of your program after it has run. To help with this, the =>trace lens is provided.

You can compose the =>trace lens with any other lens to produce logging information whenever the lens is used to access or updates data.

(define example-vector
 (vector #f #f 10 20 30 40 50 60 70 80 90 100 110 #f #f))
(define vec-min-index 2)
(define vec-max-index 13)
(define vec-median-index
 (truncate-quotient (- vec-min-index vec-max-index) 2))
(define =>median (=>vector-index*! vec-median-index))
 ;; --- Hmm... this lens seems to be broken. ---
;; Lets inspect the lens viewing operation with "=>trace"
(display (view example-vector (=>trace =>median)))

The log output of the above code would then look something like this:

(begin view on lens (lens (vector-index*! -5)):
 #:record-in-focus (#(#f #f 10 20 30 40 50 60 70 80 90 100 110 #f #f)))
(error in view on lens (lens (vector-index*! -5)):
 #:error-object ("in procedure vector-ref: Argument out-of-range" -5)
 #:record-in-focus (#(#f #f 10 20 30 40 50 60 70 80 90 100 110 #f #f)))
Error in procedure vector-ref: Argument out of range: -5

As you can see, this report:

 begin view on lens (lens (vector-index*! -5))

... shows that our vector index lens is being constructed with a negative value.

Difference between update and generalized set!

As stated earlier, the SRFI-17 "Generalized set!" also provides a simpler way to update data structures more similar to how languages like JavaScript or Python would do. But unlike generalized set!, the mechanism for updating is accomplished with procedures (functions) rather than through the use of a macro such as set!.

One other difference is that the update operator is derived from the update&view operator, and when update&view is applied to lenses must return two values:

  1. the updated data structure, or a replacement data structure if you are operating on a pure/immutable data type,

  2. an arbitrary second value.

This allows us to return additional information about the data structure as it is being updated. For example, we could express a depth-first search (DFS) algorithm which returns the path to an element which succeeds when applied to a predicate, we can return this path even when updating the lens. And this DFS lens can still be composed with other lenses in calls to view, lens-set, or update.

(define (=>depth-first-search select?)
 ;; This lens finds the first node in a tree for which the `SELECT?`
 ;; predicate return #t. The full path to the node is returned.
 ;; Updates return the path to the element updated as the second
 ;; value.
 (let*((getter ;; ----------------------------------------------
 ;; Returns the first node succeeding on application of
 ;; `SELECT?` The path is applied to `select?` but is not
 ;; returned.
 (lambda (node)
 (call/cc
 (lambda (return)
 (let loop ((node node) (path '()))
 (cond
 ((select? node path) (return node))
 ((hash-table? node)
 (hash-table-for-each
 (lambda (key node) (loop node (cons key path)))
 node)
 #f)
 ((vector? node)
 (let ((len (vector-length node)))
 (let next ((i 0))
 (cond
 ((< i len)
 (loop node (cons i path))
 (next (+ 1 i)))
 (else #f)))))
 (else #f)))))
 ))
 (updater ;; --------------------------------------------------
 ;; The updater function takes an updating procedure to alter
 ;; the value to which this lens is referring. Since this is a
 ;; depth-first search, the updating procedure is applied to
 ;; the first element selected by the `SELECT?` predicate. The
 ;; updating procedure must return 2 values: 1. the updated
 ;; node, and 2. some arbitrary other value. When many lenses
 ;; are composed in sequence for the `UPDATE` procedure, the
 ;; last lens in the sequence will return this arbitrary
 ;; value.
 ;;
 ;; In this example, the arbitrary second value returned is
 ;; the second return value of `UPDATER` CONS'ed to the path
 ;; of selected node, or #f if nothing is selected.
 (lambda (updater node)
 (call/cc
 (lambda (return)
 (let loop ((node node) (path '()))
 (cond
 ((select? node path)
 ;; ********* THIS IS INTERESTING! *********
 ;; This is what makes lenses different from
 ;; generalized `SET!`:
 (let-values (((node result) (updater node)))
 (return node (cons result path)))
 ;; Notice how we can define our lens to not only
 ;; return the result of `UPDATER` but also return
 ;; the "path". When this lens is used with
 ;; `LENS-SET` this information is discarded, but if
 ;; you use `UPDATE` the lens you define can choose
 ;; to return additional information that you might
 ;; find useful.
 )
 ((hash-table? node)
 (hash-table-for-each
 (lambda (key subnode)
 (let-values
 (((subnode result)
 (loop node (cons key path))))
 (cond
 (result
 (hash-table-set! node key subnode)
 (return node result))
 (else (values)))))
 node))
 ((vector? node)
 (let ((len (vector-length node)))
 (let next ((i 0))
 (cond
 ((< i len)
 (loop (vector-ref node i) (cons i path))
 (next (+ 1 i)))))))
 (else (values node #f))))))
 ))
 (setter ;; --------------------------------------------------------
 ;; We use the `DEFAULT-UNIT-LENS-SETTER` procedure that is
 ;; provided by `(SCHEMACS LENS)` to derive the setter automatically.
 ;; The default setter uses the updater and discards the second of
 ;; the returned VALUES.
 (default-unit-lens-setter updater))
 )
 ;; Use the `UNIT-LENS` procedure provided by `(SCHEMACS LENS)` to
 ;; construct a lens. This constructs a data structure associating
 ;; a getter, setter, and updater procedure all together.
 (unit-lens getter setter updater `(=>depth-first-search ,select?))
 ))
(define =>DFS =>depth-first-search)
(define (select-by-key-symbol sym)
 (lambda (o path) (and (pair? path) (eq? sym (car path)))))
(define (select-by-key-string name)
 (lambda (o path) (and (pair? path) (string=? name (car path)))))
(define ht
 (alist->hash-table
 (list
 (cons 'zero 0) (cons 'one 1) (cons 'two 2)
 (cons 'elements
 (alist->hash-table
 (list
 (cons 'earth "solid")
 (cons 'wind "gas")
 (cons 'fire "plasma")
 (cons 'water "liquid")
 (cons 'periodic-table
 (vector
 #f "Hydrogen" "Helium" "Lithium" "Berylium" "Boron"
 "Carbon" "Nitrogen" "Oxygen" "Flourine" "Neon"))))
 ))))
(view ht (=>DFS (select-by-key-symbol 'periodic-table)) 1)
 ;; displays "Hydrogen"
(view ht (=>DFS (select-by-key-symbol 'elements)) 'water)
 ;; displays "liquid"

Concluding remarks

I personally have found the use of lenses to be an incredibly elegant and useful solution to many problems in Scheme involving large and complex data structures, such as objects structured as XML or JSON. I have come to prefer using lenses to using pattern matching, or going through the trouble of using a Meta-Object Protocol. I am publishing it here in the hopes that other people will find it as useful as I have.

This README is the only documentation for this library, but the source code is well-commented. To learn more about the available lens library APIs and how to use them, please refer to the comments in the source code.

I hope to keep this library as portable as possible, and have made an effort to write all code using only R7RS and the SRFIs. I am also interested in porting this library to other Scheme implementations. If you are interested in porting this code, and you manage to figure out which cond-expand expressions you need to write to make this library compile or run on another Scheme, please feel free to submit a pull request to me, either on CodeBerg, or as a Git patch attached to an e-mail.