[racket] Functional Updates for Structs

72 views
Skip to first unread message

Danny Gratzer

unread,
Aug 27, 2013, 12:37:23 AM8/27/13
to us...@racket-lang.org
Hello,

I'm writing some code to manipulate a few structs and in doing so have run into a rub. There doesn't seem to be a convenient way to do functional updates for structs.

Normally I'd use struct-copy, but I have neither the field nor the type name at compile time, currently I have a macro that looks like this

    (wire some-struct-accessor ; source
            some-proc                ; pipe
            (lambda (s v) (struct-copy type-name s [field-name v]))) ; sink

which just looks clunky when compared to the functional accessor. My research turned up a thread[1] from 2010 that proposed 

    (type-name/field struct val)

as an updater counterpart to 

    (type-name-field struct)

but that doesn't seem to have gone anywhere.

I figured I'd ask to see if I'm about to reinvent the wheel here before I just write some code to generate my update functions.

Matthias Felleisen

unread,
Aug 27, 2013, 9:59:27 AM8/27/13
to Danny Gratzer, us...@racket-lang.org

Danny,

this is indeed an Achilles' heel in our world. Can you give us some more context? If you don't have access to the name of the struct type, how would you access the fields of a struct? How do you know anything about it?

For better or worse, Racket has chosen a slightly more static approach to structs than other scripting languages, from which you get a bit more safety and a bit more hassle.

-- Matthias
> ____________________
> Racket Users list:
> http://lists.racket-lang.org/users


____________________
Racket Users list:
http://lists.racket-lang.org/users

Danny Gratzer

unread,
Aug 27, 2013, 11:33:44 AM8/27/13
to Matthias Felleisen, users
It's part of a DSL I'm writing, the idea being a user gives an accessor to some struct, a function to transform the accessed data, an updater to store the transformed data into another struct. I've nicknamed these source, pipe, and sink procedures respectively.

The end goal is a DSL that looks like

    (create-trans
        (wire source through pipe into sink) ...)

The context for this is robotics programming where source and sink structs model input and the robot respectively. If anyone's familiar with LabView, it's not entirely dissimilar. 

I'm trying to make the DSL pleasant to use by avoiding mentioning anything about type or field names, just accessors and updaters. So I have no trouble extracting the needed data, but I there aren't any nice functional updaters generated. I could write something like

    (create-trans output-type
       (wire source through pipe into field-name) ...)

which isn't too bad, but it's a bit asymmetric: source can be some arbitrarily complex function but field-name is very limited. Probably I should just suck it up and accept a bit of ugliness. 

Matthias Felleisen

unread,
Aug 27, 2013, 2:01:42 PM8/27/13
to Danny Gratzer, users

Here is a solution:

1. define a macro that defines structs with additional capabilities, e.g., functional updaters

2. replace field-name with output-type and then use the extended capability to retrieve the appropriate functionality on the fly

But as I am typing this, I am beginning to think that you really want objects not structs for your task. So perhaps the real trick is to use objects and methods because then you can exploit dynamic dispatch to get the desired functionality. Here is a sketch that is NOT working code:

(define-syntax
(my-kind-of-struct name (field1 ...))
... (define name% (class (init-field field1 ...) (define/public (name-field1 x) field1) ... (define/public (set-name-field1 new-x) (new name% [field1 new-x][field2 field2] ...)) ...) ...)

Your macro can now expand to just

(send unknown-object set-name-fieldN ...)

and you don't need to know the struct type. I bet that this will work more easily than structs plus other macrology.

-- Matthias

Jay McCarthy

unread,
Aug 27, 2013, 5:31:51 PM8/27/13
to Matthias Felleisen, dev, users
Related to this issue of functional updaters, I've been working on a
new struct mechanism based on my old "super structs" [1].

"super structs" was an attempt to give a lot more control of the names
created by the struct macro and give a way to use keyword arguments in
constructors. I was hoping to eventually support auto values more like
optional arguments, but didn't do it.

The new system, records [2], is an attempt to give a lot more
information about the structure statically so that functional updating
(including down to sub-types) is easier. Part of the way it does this
is by removing all but a single name from the output of the macro and
using keywords instead for everything. Here's the test suite to give a
feel for it:

(let ()
(record posn (#:x #:y))

;; Creation
(define p0 (posn #:x 1 #:y 3))
;; Identification
(check-true (posn #:? p0))
(check-false (posn #:? 3))
;; Accessor
(check-equal? (posn p0 #:x) 1)
(check-equal? (posn p0 #:y) 3)
;; Updating
(define p1 (posn p0 #:x 2))
(check-equal? (posn p1 #:x) 2)
(check-equal? (posn p1 #:y) 3)
(define p2 (posn p0 #:y 4))
(check-equal? (posn p2 #:x) 1)
(check-equal? (posn p2 #:y) 4)
(define p3 (posn p0 #:y 4 #:x 2))
(check-equal? (posn p3 #:x) 2)
(check-equal? (posn p3 #:y) 4)

;; Matching
(check-equal? (match p1 [(posn #:x x #:y y) (list x y)])
(list 2 3))
(check-equal? (match p1 [(posn #:y y #:x x) (list x y)])
(list 2 3))
(check-equal? (match p1 [(posn #:x x) (list x 3)])
(list 2 3))
(check-equal? (match p1 [(posn) (list 2 3)])
(list 2 3)))

(let ()
(record posn ([#:x #:mutable] #:y))

(define p0 (posn #:x 1 #:y 3))
(check-equal? (posn p0 #:x) 1)
;; Mutation
(posn p0 #:x 2 #:!)
(check-equal? (posn p0 #:x) 2))

(let ()
;; Whole record is mutable
(record posn (#:x #:y) #:mutable)

(define p0 (posn #:x 1 #:y 3))
(check-equal? (posn p0 #:x) 1)
(check-equal? (posn p0 #:y) 3)
(posn p0 #:x 2 #:!)
(posn p0 #:y 4 #:!)
(check-equal? (posn p0 #:x) 2)
(check-equal? (posn p0 #:y) 4)
(posn p0 #:x 1 #:y 3 #:!)
(check-equal? (posn p0 #:x) 1)
(check-equal? (posn p0 #:y) 3))

One of the issues with this approach is that parents and children
can't have fields with the same names because they exist in the same
global keyword scope.

Similarly, the normal scoping mechanisms of Racket would be
ineffective at protecting internal fields and, more importantly,
hidden mutation to them. I'm imaging a "record-close" operation that
produces a new macro with restricted access, for instance:

(record posn (#:x #:y))
(record-close posn-with-no-y #:base posn (#:x))

Ironing out all the details of this new approach is a lot of work and
I'm not totally convinced myself if it is a good idea. It's along
enough that it would be valuable to hear anyone's take on its flaws or
merits.

Finally, budding macrologists may be interested in some of the
implementation tricks :)

Jay

1. https://github.com/jeapostrophe/exp/blob/master/sstruct-tests.rkt
2. https://github.com/jeapostrophe/exp/blob/master/record.rkt
--
Jay McCarthy <j...@cs.byu.edu>
Assistant Professor / Brigham Young University
http://faculty.cs.byu.edu/~jay

"The glory of God is Intelligence" - D&C 93
Reply all
Reply to author
Forward
0 new messages