Подтвердить что ты не робот

Есть ли сокращенный способ обновления определенного поля структуры в ракетке?

Предположим, что у меня есть структура со многими полями:

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

Если мне нужно обновить новую структуру с f2, я должен перефразировать все остальные поля:

(define s (my-struct 1 2 3 4))
(my-struct (my-struct-f1 s)
           (do-something-on (my-struct-f2 s))
           (my-struct-f3 s)
           (my-struct-f4 s))

Это избыточно и будет источником ошибок, если я обновляю количество полей или изменю их порядок.

Мне действительно интересно, есть ли такой способ, я могу обновить определенное поле для структуры, например:

(my-struct-f2-update (my-struct 1 2 3 4)
                     (lambda (f2) (* f2 2)))
;; => (my-struct 1 4 3 4)

Или я могу просто установить их в новое значение как:

(define s (my-struct 1 2 3 4)
(my-struct-f2-set s (* (my-struct-f2 s) 2))
;; => (my-struct 1 4 3 4)

Обратите внимание, что здесь нет мутации s; my-struct-f2-update и my-struct-f2-set должны просто вернуть копию s с обновленным полем f2.

В Haskell я знаю библиотеку "объектив", которая выполняет эту работу. Мне просто интересно, есть ли подобные способы, которые я могу принять для ракетки. Спасибо.

4b9b3361

Ответ 1

Знаешь что? Это действительно хорошая идея. На самом деле, было несколько случаев, когда я хотел эту функциональность, но у меня ее не было. Плохая новость заключается в том, что Racket ничего подобного не предлагает. Хорошей новостью является то, что у Racket есть макросы!

Представляю вам 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) ...))
         ...)]))

Если вы не знакомы с макросами, это может выглядеть немного запугивающим, но на самом деле это не сложный макрос. К счастью, вам не нужно понимать, как это работает. Вот как вы это сделаете:

(struct point (x y) #:transparent)
(define-struct-updaters point)

Теперь вы можете использовать все соответствующие функциональные сеттеры и обновления, как вам было угодно.

> (point-x-set (point 1 2) 5)
(point 5 2)
> (point-y-update (point 1 2) add1)
(point 1 3)

Я считаю, что были некоторые теоретические планы по реорганизации системы Racket struct, и я думаю, что это было бы ценным дополнением. До тех пор не стесняйтесь использовать это решение.

Из-за его полезности я могу попытаться включить это в пакет, чтобы сделать его более легко используемым, и в этом случае я отредактирую этот ответ с этой информацией.

Ответ 2

Мне нравится макрос Алексиса! Это больше, чем вам хотелось.

Я также хочу указать struct-copy. Дано:

#lang racket
(struct my-struct (f1 f2 f3 f4) #:transparent)
(define s (my-struct 1 2 3 4))

Вы можете использовать struct-copy для установки значения:

(struct-copy my-struct s [f2 200])
;;=> (my-struct 1 200 3 4)

Или обновить значение:

(struct-copy my-struct s [f2 (* 100 (my-struct-f2 s))])
;;=> (my-struct 1 200 3 4)

Обновление: Думая об этом больше, вот еще несколько идей.

Вы также можете обновить с помощью шаблона match struct*:

(match s
  [(struct* my-struct ([f2 f2]))
   (struct-copy my-struct s [f2 (* 100 f2)])])

Конечно, это очень много. С другой стороны, struct* шаблона позволяет легко определить макрос, используя более простой 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)

Конечно, настройка - это особый случай, когда вы даете обновление const:

(struct-update my-struct s f2 (const 42))
;;=> (my-struct 1 42 3 4)

Наконец, это похоже на struct-update, но возвращает функцию обновления в духе макроса Алексиса:

(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)

Я не говорю, что все это идиоматично или эффективно. Но это возможно.:)

Ответ 3

Макрос Алексис фантастичен, и Грег правильно указал struct-copy и match+struct*, но поскольку вы конкретно упомянули линзы в своем примере, я укажу, что теперь есть пакет объективов для Racket (отказ от ответственности: я написал много его). Он предоставляет макросы struct/lens и define-struct-lenses для вашего использования:
> (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 позволяет определить объективы отдельно от структур:

> (struct foo (a b c))
> (define-struct-lenses foo)

Вышеупомянутое эквивалентно (struct/lens foo (a b c)). Если вы работаете только с структурами, изолированными от других типов структур, использование define-struct-updaters проще. Но если у вас много вложенных структур данных различных вкусов, способность создавать линзы делает их мощным инструментом для работы.