Is there a shorthand way to update a specific struct field in racket?

浪子不回头ぞ 提交于 2019-12-03 23:49:17

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.

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. :)

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.

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!