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

前端 未结 3 1819
执念已碎
执念已碎 2021-02-12 23:40

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:01

    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.

    0 讨论(0)
  • 2021-02-13 00:02

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

    0 讨论(0)
  • 2021-02-13 00:08

    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.

    0 讨论(0)
提交回复
热议问题