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

前端 未结 3 1659
轮回少年
轮回少年 2021-02-12 23:30

Suppose I have a struct with many fields:

(struct my-struct (f1 f2 f3 f4))

If I am to return a new struct with f2 updated, I have

3条回答
  •  迷失自我
    2021-02-13 00:04

    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.

提交回复
热议问题