[racket] Functional Updates for Structs
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
On Tue, Aug 27, 2013 at 12:01 PM, Matthias Felleisen
<matthias at ccs.neu.edu> wrote:
>
> 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
>
>
>
>
>
>
>
> On Aug 27, 2013, at 11:33 AM, Danny Gratzer <danny.gratzer at gmail.com> wrote:
>
>> 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.
>
>
> ____________________
> Racket Users list:
> http://lists.racket-lang.org/users
--
Jay McCarthy <jay at cs.byu.edu>
Assistant Professor / Brigham Young University
http://faculty.cs.byu.edu/~jay
"The glory of God is Intelligence" - D&C 93