Reputation: 155
Apologies if this has been asked before - I'm trying to build a macro similar to struct
and I'm a bit stuck.
The basic idea is to be able to provide a hash object to my syntax rule that should then write getters and setters for every key in the hash. For example:
(hasheq 'name (hasheq 'S "user")
'isOnline (hasheq 'B #t)
'bio (hasheq 'M (hasheq 'firstName (hasheq 'S "Sally")
'lastName (hasheq 'S "Wallace"))))
Calling my macro:
(dynamo-model-make-accessors user <my-hash-object>)
Should result in the following generated methods:
user-name
user-isOnline
user-bio
user-bio-firstName
user-bio-lastName
This is what I have so far.
(define-syntax (dynamo-model-make-accessors stx)
(syntax-case stx ()
[(_ prefix fields)
#`(begin
#,@(for/list ([kv (hash->list (eval (syntax->datum #'fields)))])
(with-syntax* ([key (car kv)]
[getter (format-id #'prefix "~a-~a" #'prefix #'key)]
[type (car (hash-keys (cdr kv)))])
(displayln #'getter)
(if (eq? (syntax->datum #'type) 'M)
(dynamo-model-make-accessors #'prefix (hash-ref (cdr kv) 'M))
#`(define (getter O)
(+ 1 1))))))]))
The problem comes in when I try to interpret the nested map. I try to call my syntax rule within itself and I get:
dynamo-model-make-accessors: undefined;
cannot reference an identifier before its definition
How can I recursively call my macro?
EDIT (newest code):
(define-syntax (dynamo-model-make-accessors stx)
(syntax-parse stx
[(_dynamo-model-make-accessors prefix fields)
(define exprs (for/list ([kv (hash->list (eval #'fields))])
(with-syntax* ([key (car kv)]
[getter (format-id #'prefix "~a-~a" (eval #'prefix) #'key)]
[type (car (hash-keys (cdr kv)))]
[value (hash-ref (cdr kv) (syntax->datum #'type))])
(displayln #'getter)
(displayln #'type)
(if (eq? (syntax->datum #'type) 'M)
#`(dynamo-model-make-accessors #'getter value)
#`(define (getter O)
(+ 1 1))))))
(with-syntax ([(expr ...) exprs])
#'(begin expr ...))]))
Upvotes: 2
Views: 491
Reputation: 10663
You cannot write that macro in Racket. In Racket, a program's binding structure (what names are bound and what scopes they have) cannot depend on run-time values.
For an example of why this doesn't work, consider this example:
(define (get-name h obj)
(dynamo-model-make-accessors user h)
(user-name obj))
(get-name (hasheq 'name (hasheq 'S ___)) 'some-object)
(get-name (hasheq) 'another-object)
The use of dynamo-model-make-accessors
in get-name
must be compiled once. But how can it decide what names to bind when the actual value isn't available until the function gets called? And when it might get multiple, different values at run time?
Instead, you can either go more dynamic or more static.
Just write a function that takes a list of symbols and does the references (and checks) at run time. So instead of (user-bio-firstName obj)
you would write something like (get obj '(bio firstName))
.
Then you can add a little syntactic sugar around it by making a macro (let's call it Get
here) that adds quotes. For example:
(Get obj bio firstName) ==> (get obj '(bio firstName))
(define-syntax-rule (Get obj sym ...) (get obj (quote (sym ...))))
The other way is to make the binding structure your macro generates depend on compile-time information rather than run-time information. Here's most of one way to do it:
;; A RecordSpec is (Hasheq Symbol => FieldSpec)
;; A FieldSpec is one of
;; - (hasheq 'S String)
;; - (hasheq 'M RecordSpec)
;; Here we bind `user-fields` to a *compile-time* RecordSpec value
;; using `define-syntax`.
(define-syntax user-fields
(hasheq 'name (hasheq 'S "user")
'isOnline (hasheq 'B #t)
'bio (hasheq 'M (hasheq 'firstName (hasheq 'S "Sally")
'lastName (hasheq 'S "Wallace")))))
The macro can fetch the compile-time RecordSpec value associated with an identifier by calling syntax-local-value
.
(define-syntax (dynamo-model-make-accessors stx)
(syntax-parse stx
[(_ prefix spec:id)
;; generate-recordspec-bindings : Identifier RecordSpec
;; -> (Listof Syntax[Definition])
(define (generate-recordspec-bindings prefix-id recordspec)
(append*
(for/list ([(field fieldspec) (in-hash recordspec)])
(define field-id (format-id prefix-id "~a-~a" prefix-id field))
(generate-fieldspec-bindings field-id fieldspec))))
;; generate-fieldspec-bindings : Identifier RecordSpec
;; -> (Listof Syntax[Definition])
(define (generate-fieldspec-bindings field-id fieldspec)
(cond [(hash-ref fieldspec 'S)
=> (lambda (string-value)
;; left as exercise to reader
___)]
[(hash-ref fieldspec 'M)
=> (lambda (inner-recordspec)
;; left as exercise to reader
___)]))
(define recordspec (syntax-local-value #'spec)) ;; better be a RecordSpec
#`(begin
#,@(generate-recordspec-bindings #'prefix recordspec))]))
Then you can use the macro like this:
(define (get-name obj)
(dynamo-model-make-accessors user user-fields) ;; defines `user-name`, etc
(user-name obj))
Upvotes: 3
Reputation: 31145
Originally a comment, but ran out of space:
I won't rule out the possibility that one can cheat the system, but ... .
The macro system uses phases. At compile time (could be tuesday) the system has one set of bindings. When the program is compiled the bindings/values used during compilation are gone. Then at runtime (could be wednesday) the resulting program is run.
But it is a bit more complicated. If at compile time you use a macro, then your compile time program needs to be compiled first (at compile-compile-time). A macro foo
can therefore not use foo
- because in order to use foo
, foo
needs to be compiled - and to do that foo
needs to be compiled - and ...
What you can do, is to expand into a use of foo
.
That is you can expand into something that uses foo
.
When foo
returns a syntax object as a result, the expander takes
the result and continues to expand it. When the expander sees the new use of foo
, it calls the syntax transformer associated with foo
again.
The use of eval
shows that you are attempting to circumvent the phases. It is an uphill battle, and it might be worth rethinking the current approach.
Upvotes: 2