Is there a shorthand way to update a specific struct field in racket?
You know what? This is a really good idea. In fact, there have been a few cases in which I wanted this functionality, but I didn't have it. The bad news is that nothing of this sort is provided by Racket. The good news is that Racket has macros!
I present to you define-struct-updaters
!
(require (for-syntax racket/list
racket/struct-info
racket/syntax
syntax/parse))
(define-syntax (define-struct-updaters stx)
(syntax-parse stx
[(_ name:id)
; this gets compile-time information about the struct
(define struct-info (extract-struct-info (syntax-local-value #'name)))
; we can use it to get the constructor, predicate, and accessor functions
(define/with-syntax make-name (second struct-info))
(define/with-syntax name? (third struct-info))
(define accessors (reverse (fourth struct-info)))
(define/with-syntax (name-field ...) accessors)
; we need to generate setter and updater identifiers from the accessors
; we also need to figure out where to actually put the new value in the argument list
(define/with-syntax ([name-field-set name-field-update
(name-field-pre ...) (name-field-post ...)]
...)
(for/list ([accessor (in-list accessors)]
[index (in-naturals)])
(define setter (format-id stx "~a-set" accessor #:source stx))
(define updater (format-id stx "~a-update" accessor #:source stx))
(define-values (pre current+post) (split-at accessors index))
(list setter updater pre (rest current+post))))
; now we just need to generate the actual function code
#'(begin
(define/contract (name-field-set instance value)
(-> name? any/c name?)
(make-name (name-field-pre instance) ...
value
(name-field-post instance) ...))
...
(define/contract (name-field-update instance updater)
(-> name? (-> any/c any/c) name?)
(make-name (name-field-pre instance) ...
(updater (name-field instance))
(name-field-post instance) ...))
...)]))
If you're not familiar with macros, it can look a little intimidating, but it's actually not a complicated macro. Fortunately, you don't need to understand how it works to use it. Here's how you'd do that:
(struct point (x y) #:transparent)
(define-struct-updaters point)
Now you can use all the relevant functional setters and updaters as you'd please.
> (point-x-set (point 1 2) 5)
(point 5 2)
> (point-y-update (point 1 2) add1)
(point 1 3)
I believe there have been some theoretical plans to redesign the Racket struct system, and I think this would be a valuable addition. Until then, feel free to use this solution. I’ve made the code in this answer available as the struct-update
package, which can be installed using raco pkg install struct-update
.
Alexis's macro is fantastic, and Greg's rightly pointed out struct-copy
and match+struct*
, but since you specifically mentioned lenses in your example I'll point out that there is now a lens package for Racket (disclaimer: I wrote a lot of it). It provides struct/lens
and define-struct-lenses
macros for your use case:
> (struct/lens foo (a b c))
> (lens-view foo-a-lens (foo 1 2 3))
1
> (lens-set foo-a-lens (foo 1 2 3) 'a)
(foo 'a 2 3)
> (lens-transform foo-a-lens (foo 1 2 3) number->string)
(foo "1" 2 3)
define-struct-lenses
lets you define the lenses seperately from the structs:
> (struct foo (a b c))
> (define-struct-lenses foo)
The above is equivalent to (struct/lens foo (a b c))
. If you're only operating on structs in isolation from other kinds of structs, using define-struct-updaters
is simpler. But if you have a lot of nested data structures of various flavors, the ability to compose lenses makes them a powerful tool for the job.
I like Alexis' macro! It has more of the "lens" flavor you wanted.
I also want to point out struct-copy
. Given:
#lang racket
(struct my-struct (f1 f2 f3 f4) #:transparent)
(define s (my-struct 1 2 3 4))
You can use struct-copy
to set a value:
(struct-copy my-struct s [f2 200])
;;=> (my-struct 1 200 3 4)
Or to update a value:
(struct-copy my-struct s [f2 (* 100 (my-struct-f2 s))])
;;=> (my-struct 1 200 3 4)
Update: Thinking about this more, here are a few more ideas.
You could also update using match
's struct*
pattern:
(match s
[(struct* my-struct ([f2 f2]))
(struct-copy my-struct s [f2 (* 100 f2)])])
Of course, that's very verbose. On the other hand the struct*
pattern makes it easy to define a macro using the simpler
define-syntax-rule
:
;; Given a structure type and an instance of it, a field-id, and a
;; function, return a new structure instance where the field is the
;; value of applying the function to the original value.
(define-syntax-rule (struct-update struct-type st field-id fn)
(match st
[(struct* struct-type ([field-id v]))
(struct-copy struct-type st [field-id (fn v)])]))
(struct-update my-struct s f2 (curry * 100))
;;=> (my-struct 1 200 3 4)
Of course, setting is the special case where you give update a
const
function:
(struct-update my-struct s f2 (const 42))
;;=> (my-struct 1 42 3 4)
Finally, this is like struct-update
, but returns an updater function, in the spirit of Alexis' macro:
(define-syntax-rule (struct-updater struct-type field-id)
(λ (st fn)
(struct-update struct-type st field-id fn)))
(define update-f2 (struct-updater my-struct f2))
(update-f2 s (curry * 100))
;;=> (my-struct 1 200 3 4)
I'm not saying that any of this is idiomatic or efficient. But it's possible. :)