Is it possible to capture a the value of a variable inside a macro?

59 views
Skip to first unread message

Dimaugh Silvestris

unread,
Sep 16, 2021, 4:21:07 PM9/16/21
to racket...@googlegroups.com
(sorry if I'm asking too many questions about macros lately, I'm learning about them but I keep running into scenarios I can't find documentation for)

I'm trying to capture the value of a variable whose identifier I can only get with format-id, inside a with-syntax.
Something like this pseudocode (imagine name-foo contains a list of symbols):
(define-syntax (my-macro stx)
  (syntax-case stx ()
    ((_ name other-args ...)
     (with-syntax* ((varname (format-id #'name "~a-foo" #'name))
                    (varval (cons (datum->syntax #'varname) (datum->syntax #'(other-args ...)))))
       #'(define name (λ varval (print varval)))))))


Which of course doesn't work. I understand this might have to do with how macros work at an earlier phase than runtime, so is it impossible?

Sorawee Porncharoenwase

unread,
Sep 16, 2021, 4:35:23 PM9/16/21
to Dimaugh Silvestris, Racket list

In general, it would be helpful to provide an example of the macro use, so that we know what you want to do. If it doesn't work, it would be helpful to provide the buggy program and an error message so that we can help with the issue that you are encountering.

From my guess, you have a variable named abc-foo somewhere, and with this macro, you wish to define a function named abc that can access the value of abc-foo? If so, here’s an example of a working program:

#lang racket

(require (for-syntax racket/syntax))

(define-syntax (my-macro stx)
  (syntax-case stx ()
    [(_ name other-args ...)
     (with-syntax ([varname (format-id #'name "~a-foo" #'name)])
       #'(define name
           (λ (other-args ...)
             (println (list varname other-args ...)))))]))

(define abc-foo 123)
(my-macro abc x y)
(abc 5 6) ;=> '(123 5 6)




--
You received this message because you are subscribed to the Google Groups "Racket Users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to racket-users...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/racket-users/CAN4YmRF%3Do3NsXOvK2fvUDeYL_jfA9r946%3D%3DguoGb_%3DKyS%3Dm%2Bxw%40mail.gmail.com.

David Storrs

unread,
Sep 16, 2021, 6:10:39 PM9/16/21
to Dimaugh Silvestris, Racket Users
Sorawee answered your immediate question, but I figured I'd offer a pointer to Fear of Macros in case you haven't seen it:  https://www.greghendershott.com/fear-of-macros/  It helped me a lot when I was trying to get my head around macros.  Also, I got a lot of value from reading through the code of https://pkgs.racket-lang.org/package/struct-update

--

Sorawee Porncharoenwase

unread,
Sep 17, 2021, 11:52:48 AM9/17/21
to Dimaugh Silvestris, Racket list

Instead of creating hola-fields which exists at run-time, you can (define-syntax hola ...) to a struct containing the field information at compile-time (it probably needs to contain other information too). The struct could have prop:procedure which in this case will be the syntax transformer that generates struct creation. Looking up the field compile-time information then can be done by using syntax-local-value or static syntax class. See section 2.1 in https://www.cs.utah.edu/plt/publications/jfp12-draft-fcdf.pdf for explanations.

What’s unclear to me are:

1) How would you deal with the following? Should it be an error?

(card (foo . xs))
(card foo (bar . ys))

2) How would you determine the number of fields (and field names) in the following?

> (card (line . xs))
> (line 1 2 3 4 5 6 7 8 9)

Wouldn’t it make more sense to have:

> (card (line . xs))
> (line '(1 2 3 4 5 6 7 8 9))

where xs is the only field.

3) Shouldn’t the following result in (ciao 7 3)?

> (card (ciao a [b 3]))
> (ciao 7)


On Thu, Sep 16, 2021 at 2:12 PM Dimaugh Silvestris <dimaughs...@gmail.com> wrote:
Sorry, I haven't posted the full macro because it's long and makes use of several other functions, but I'll try to summarize what it does:

Short summary: I'm trying to have a macro (mymacro oldname newname (fields ...)) that accesses oldname-foo, which contains a list of symbols, and then define a function that takes (cons oldname-foo (fields ...)) formated as identifiers as arguments. Or at least to get the length of oldname-foo and name them whatever.

Full explanation: using make-struct-type I'm building a different struct system I call cards, where structs can be defined as a function call, which will be their constructor, and they are printed as the constructor function call that would generate them. So, for instance, we can do:
> (card (hola a b #:c c))
> (hola 1 2 #:c 3)
(hola 1 2 #:c 3)
or
> (card (ciao a [b 3]))
> (ciao 7)
(ciao 7)
> (ciao 7 4)
(ciao 7 4)

or even

> (card (line . xs))
> (line 1 2 3 4 5 6 7 8 9)
(line 1 2 3 4 5 6 7 8 9)

Also the names of the fields are stored in *<name-of-the-card>-fields (this is the abc-foo of the above example), so *hola-fields contains '(a b #:c c).
So far this is working perfectly, but I don't have inheritance. So when I create a card that inherits from a previous card, I need to access its *<parent-card>-fields to define a new function containing both the parent and the son fields. That is, I'm trying to get this behavior:
> (card (hola a #:b b))
> (card hola (ciao c))  ;;; should expand to (define (ciao a #:b b c) ...), among other things
> (ciao 1 #:b 2 3)
(ciao 1 #:b 2 3)

David Storrs

unread,
Sep 17, 2021, 3:08:19 PM9/17/21
to Sorawee Porncharoenwase, Dimaugh Silvestris, Racket list

NB:  You did a 'reply' to Sorawee instead of to the list as a whole, and also the same for the email I sent.  Probably good to 'reply all' so that the list gets the full context.

Sorawee offered some good advice on how to do the things you're asking about and asked relevant questions.  I'm wondering about the macro level:

A)  What exactly are you trying to do, because I think I've got it but I'm still fuzzy.
B)  Why are you trying to do it?
C) Is there a simpler / more Racket-ish way to do it?

On Thu, Sep 16, 2021 at 2:12 PM Dimaugh Silvestris <dimaughs...@gmail.com> wrote:
Sorry, I haven't posted the full macro because it's long and makes use of several other functions, but I'll try to summarize what it does:

Short summary: I'm trying to have a macro (mymacro oldname newname (fields ...)) that accesses oldname-foo, which contains a list of symbols, and then define a function that takes (cons oldname-foo (fields ...)) formated as identifiers as arguments. Or at least to get the length of oldname-foo and name them whatever.

Full explanation: using make-struct-type I'm building a different struct system I call cards, where structs can be defined as a function call, which will be their constructor, and they are printed as the constructor function call that would generate them. So, for instance, we can do:

I notice that you're using make-struct-type instead of struct -- is that intentional or is there some specific feature you want?  I suspect I'm about to get a more experienced person telling me that I've missed something, but to the best of my knowledge struct is the more modern version and can do everything that make-struct-type can do but cleaner.

As to the printing as a constructor call:  putting the #:prefab option on a struct will allow you to print it in a reversible form that can be called in order to generate the struct again, but it makes explicit all the values that go in instead of hiding them away as defaults or etc.  For example:

#lang racket

(struct person (name age) #:prefab)

(person 'bob 17)

(with-output-to-file
  "/tmp/struct-demo"
  #:exists 'replace
  (thunk
   (display (person 'fred 18))))

(with-input-from-file
  "/tmp/struct-demo"
  (thunk (read)))

(with-input-from-file
  "/tmp/struct-demo"
  (thunk (person-name (read))))



Output:

'#s(person bob 17)
'#s(person fred 18)
'fred

Obviously, reading code directly from a file is a bad plan, but the same would work from any appropriate port and I'm using a file because it's easy.  The point is that I was able to write it into a port (in this case a file port) such that it produced a format that represented the constructor call and then read it back into an actual struct that I could use accessors on etc.  One disadvantage is that you can't attach properties to a prefab struct but that might or might not be relevant to you.


> (card (hola a b #:c c))
> (hola 1 2 #:c 3)
(hola 1 2 #:c 3)
or
> (card (ciao a [b 3]))
> (ciao 7)
(ciao 7)
> (ciao 7 4)
(ciao 7 4)

or even

> (card (line . xs))
> (line 1 2 3 4 5 6 7 8 9)
(line 1 2 3 4 5 6 7 8 9)

Also the names of the fields are stored in *<name-of-the-card>-fields (this is the abc-foo of the above example), so *hola-fields contains '(a b #:c c).
So far this is working perfectly, but I don't have inheritance. So when I create a card that inherits from a previous card, I need to access its *<parent-card>-fields to define a new function containing both the parent and the son fields. That is, I'm trying to get this behavior:
> (card (hola a #:b b))
> (card hola (ciao c))  ;;; should expand to (define (ciao a #:b b c) ...), among other things
> (ciao 1 #:b 2 3)
(ciao 1 #:b 2 3)

How are you going to handle the situation where a parent and child struct have a field with the same name?  This is entirely legit:

#lang racket

(struct person (name age) #:prefab)     ; age is years since birth                            
(struct employee person (age) #:prefab) ; age is years since hiring                            

(define bob (employee 'bob 17 3))
bob
(employee-age bob)
(person-age bob)

Output:

'#s((employee person 2) bob 17 3)
3
17
 

Going back to the earlier question:  What is it you are ultimately trying to accomplish at a high level?  i.e. Not "generate a lot of struct types and field data" but something like "store information about a hierarchical structure of <thing> in a persistent way so that it is recoverable across server restarts."


David Storrs

unread,
Sep 17, 2021, 3:14:00 PM9/17/21
to Sorawee Porncharoenwase, Dimaugh Silvestris, Racket list
Additional thought and self-plug:  My struct-plus-plus module creates structures with full reflection information available, including field names, contracts, and wrappers.  It also supports type checking, data normalization, default values, and automatically provides both keyword constructors and dotted accessors for clarifying field names.  It doesn't support inheritance but it would make it easy to do many of the things I think you might be interested in.  https://docs.racket-lang.org/struct-plus-plus/index.html

Dimaugh Silvestris

unread,
Sep 17, 2021, 4:58:03 PM9/17/21
to David Storrs, Sorawee Porncharoenwase, Racket list
Oops, noted, I have to use 'reply all'. In the benefit of context I'll post again my reply to Sorawee:


Short summary: I'm trying to have a macro (mymacro oldname newname (fields ...)) that accesses oldname-foo, which contains a list of symbols, and then define a function that takes (cons oldname-foo (fields ...)) formated as identifiers as arguments. Or at least to get the length of oldname-foo and name them whatever.

Full explanation: using make-struct-type I'm building a different struct system I call cards, where structs can be defined as a function call, which will be their constructor, and they are printed as the constructor function call that would generate them. So, for instance, we can do:
> (card (hola a b #:c c))
> (hola 1 2 #:c 3)
(hola 1 2 #:c 3)
or
> (card (ciao a [b 3]))
> (ciao 7)
(ciao 7)
> (ciao 7 4)
(ciao 7 4)

or even

> (card (line . xs))
> (line 1 2 3 4 5 6 7 8 9)
(line 1 2 3 4 5 6 7 8 9)

Also the names of the fields are stored in *<name-of-the-card>-fields (this is the oldname-foo of the above example), so *hola-fields contains '(a b #:c c).
So far this is working perfectly, but I don't have inheritance. So when I create a card that inherits from a previous card, I need to access its *<parent-card>-fields to define a new function containing both the parent and the son fields. That is, I'm trying to get this behavior:
> (card (hola a #:b b))
> (card hola (ciao c))  ;;; should expand to (define (ciao a #:b b c) ...), among other things
> (ciao 1 #:b 2 3)
(ciao 1 #:b 2 3)

Dimaugh Silvestris

unread,
Sep 17, 2021, 5:34:26 PM9/17/21
to David Storrs, Sorawee Porncharoenwase, Racket list
Thanks, I'll take a look into the code struct++! I'll probably get lost but I'm sure I'll learn something.
Answering your questions:

A)  What exactly are you trying to do, because I think I've got it but I'm still fuzzy.

Structs that allow constructors like any other function, and which are printed back as the function which creates them. So, a bit like prefab structs but with default values, keywords and (f . xs) args.

B)  Why are you trying to do it?

This one is long, but I think they're cool. Also convenient in several scenarios, like representing a DSP graph where things like keyword arguments often come in handy. I've been replicating this behavior by hand, it was time to write a macro.
 
C) Is there a simpler / more Racket-ish way to do it?

Probably, but I'm very picky and I thought Racket was good for this kind of thing.

I notice that you're using make-struct-type instead of struct -- is that intentional or is there some specific feature you want?  I suspect I'm about to get a more experienced person telling me that I've missed something, but to the best of my knowledge struct is the more modern version and can do everything that make-struct-type can do but cleaner.


Struct is a macro built on top of make-struct-type, with higher level features. However, I'm trying to get different features so I have to descend into the lower level.
 
As to the printing as a constructor call:  putting the #:prefab option on a struct will allow you to print it in a reversible form that can be called in order to generate the struct again, but it makes explicit all the values that go in instead of hiding them away as defaults or etc.  For example:

Yes,  prefabs come painfully close to what I'm trying to do, but they still don't allow you to define the constructor however you like (for instance, using keyword arguments). So instead of doing (card (hola a b #:c [c 3])), I end up writing things like:

(struct CARD (a b c)
            .....)           ;;; give it a custom-write-printer so that it prints as 'card' and c is preceeded by #:c, which is long
(define (card a b #:c [c 3]) ...)
(define (card-a x) (CARD-a x))
...

And then I get fed up and I think, well this is what macros were made for, right?

How are you going to handle the situation where a parent and child struct have a field with the same name?  This is entirely legit:

#lang racket

(struct person (name age) #:prefab)     ; age is years since birth                            
(struct employee person (age) #:prefab) ; age is years since hiring                            

(define bob (employee 'bob 17 3))
bob
(employee-age bob)
(person-age bob)

Output:

'#s((employee person 2) bob 17 3)
3
17
 
It wouldn't be allowed, and I consider it a bad naming choice. But at the lower level make-struct-type, fields have no names, there's just a number of them. Make-struct-type returns, among other things, a struct accessor that works like vector ref: (mystruct-accessor mystruct n) to get the nth field. Struct is just a macro on top of make-struct-type that creates a custom accessor for each field.


Going back to the earlier question:  What is it you are ultimately trying to accomplish at a high level?  i.e. Not "generate a lot of struct types and field data" but something like "store information about a hierarchical structure of <thing> in a persistent way so that it is recoverable across server restarts."

Too often I find myself replicating this behavior, in a few things I'm working on: one is a SuperCollider client, another is a drawing library based on racket/draw, but representing things very differently, and also some stuff with html where I didn't want to work with quasiquote and unquote everywhere.

Sorawee Porncharoenwase

unread,
Sep 17, 2021, 9:06:36 PM9/17/21
to Dimaugh Silvestris, Racket list

2) (card (line . xs)) has only one field, xs. Of course, you could also define it as a normal field which contains a list, but there's some other scenarios where I found it more elegant to represent it as a dotted argument (like representing s-expressions as a struct).

Oh sorry, that was a typo. I meant currently you expect

> (card (line . xs))
> (line 1 2 3 4 5 6 7 8 9)
(line 1 2 3 4 5 6 7 8 9)

to be the output, but I was asking if:

> (card (line . xs))
> (line 1 2 3 4 5 6 7 8 9)
(line '(1 2 3 4 5 6 7 8 9))

makes more sense. In any case, your response clears things up that there is indeed only one field. You simply want it to be printed like that.

This is actually a pretty fun problem. Here’s a quick prototype. Dropping it here in case anyone is interested:

#lang racket

(require syntax/parse/define
         (for-syntax syntax/parse/lib/function-header
                     racket/syntax
                     racket/list
                     racket/struct-info))

(begin-for-syntax
  (struct my-struct-info (fields args ctor)
    #:property prop:procedure
    (λ (inst stx)
      (syntax-parse stx
        [(_ args ...) #`(#,(my-struct-info-ctor inst) args ...)]
        [x:id #'#,(my-struct-info-ctor inst)]))))

(define-syntax-parse-rule (define-accessors+predicate
                            {~var struct-id (static values #f)}
                            name:id)
  #:with (fields ...) (struct-field-info-list (attribute struct-id.value))
  #:do [(define the-struct-info (extract-struct-info (attribute struct-id.value)))]
  #:with predicate (list-ref the-struct-info 2)
  #:with (accessors ...) (list-ref the-struct-info 3)
  #:with new-predicate (format-id #'name "~a?" #'name)
  #:with (new-accessors ...)
  (map (λ (id) (format-id #'name "~a-~a" #'name id)) (attribute fields))

  (begin
    (define new-predicate predicate)
    (define new-accessors accessors) ...))

(define-syntax-parse-rule
  (card
   {~optional (~var super-id (static my-struct-info? "card type"))}
   {~and header:function-header (_:id . args)})

  #:with ((all-fields ...) all-args)
  (let ([info (attribute super-id.value)])
    (cond
      [info
       (unless (list? (syntax-e (my-struct-info-args info)))
         (raise-syntax-error 'card
                             "supertype can't have variadic fields"
                             this-syntax))
       #`(({~@ . #,(my-struct-info-fields info)} . header.params)
          ({~@ . #,(my-struct-info-args info)} . args))]
      [else #'(header.params args)]))

  #:fail-when (check-duplicates (attribute all-fields) #:key syntax-e)
  "duplicate field name"

  (begin
    (struct shadow (all-fields ...)
      #:transparent
      ;; TODO: implement gen:custom-write (probably with make-constructor-style-printer)
      ;; to customize struct value printing
      #:reflection-name 'header.name)
    (define-accessors+predicate shadow header.name)
    (define (shadow-ctor . all-args)
      (shadow all-fields ...))
    (define-syntax header.name
      (my-struct-info #'(all-fields ...)
                      #'all-args
                      #'shadow-ctor))))

(let ()
  (card (hola a b #:c c))
  (println (hola 1 2 #:c 3))

  (card (ciao a [b 3]))
  (println (ciao 7))
  (println (ciao 7 4))

  (card (line . xs))
  (println (line 1 2 3 4 5 6 7 8 9)))

(let ()
  (card (hola a #:b b))
  (card hola (ciao c))
  (define v (ciao 1 #:b 2 3))
  (println v)
  (println (list (ciao-a v) (ciao-b v) (ciao-c v)))
  (println (list (ciao? v) (hola? v))))

(let ()
  (card (foo . xs))
  ;; uncomment should result in a syntax error
  (card #;foo (bar . ys))

  (card (a xs))
  ;; uncomment should result in a syntax error
  (card #;a (b xs))

  (void))

What I did not implement is making the struct value printed in the way you want, but that can be adjusted by using gen:custom-write. Note that I didn’t (re)use struct‘s supertype feature since you want fields in the opposite order.


Dimaugh Silvestris

unread,
Sep 18, 2021, 6:28:46 AM9/18/21
to Racket Users
Ohhh, thank you so much, Sorawee! Now I have wealth of code to study.
By the way, I tried to send my full code but apparently once again I hit the "reply" button instead of "reply all" and I only sent it to David, so here it is again in case anyone wants to play with it. I haven't implemented field accessors yet because those are trivial (and since I keep field names somewhere else, I could implement them differently, like (get 'field mycard), though that would probably be slower).


#lang racket/base
(require (for-syntax racket/base
                     syntax/parse
                     racket/syntax)
         syntax/parse/define)
(require (only-in racket ~a))

(provide card)   ;;;;  create a card-out thingy

(define-for-syntax (parse-args xs [rs '()])
  (define (fn xs)
    (if [null? xs] '[]
        [let [(a (car xs))
              (bs (cdr xs))]
          (cond [(keyword? a) (fn bs)]
                [(list? a) (cons (car a) (fn bs))]
                [else (cons a (fn bs))])]))
  (let [(parsed (fn xs))
        (dotlist rs)]
    (if [null? dotlist]
        parsed [append parsed (list dotlist)])))

(define keyword (string->keyword "~a"))
(define keyword-prefix "#:")
(define keyword-suffix "")

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
   
(require (only-in racket/struct
                  make-constructor-style-printer))

(define-syntax (protocard stx)
  (syntax-case stx ()
   ([_ super (name fields ... . xs)]
    (with-syntax [(descr (format-id #'name "record:~a" #'name))
                  (pred? (format-id #'name "~a?" #'name))
                  (maker (format-id #'name "make-~a" #'name))
                  (ref (format-id #'name "~a-ref" #'name))
                  (set (format-id #'name "~a-set!" #'name))
                  (field-names (format-id #'name "*~a-fields" #'name))
                  (explain (format-id #'name "explain-~a" #'name))
                  (tag-params (tag-args (syntax->datum #'(fields ...)) (syntax->datum #'xs)))
                  (plain-params
                   (cons list (map [λ (x) (format-id #'name "~a" x)]
                                   [parse-args (syntax->datum #'(fields ...))
                                               (syntax->datum #'xs)])))]  ;;; éste debería de salir de tag-params
      
      #`[begin
          (define field-names '(fields ... . xs))
          (define-values (descr maker pred? ref set)
            (make-struct-type
             'name super (length 'tag-params) 0 #f   ;; (- (length super-fields))
             (list (cons prop:custom-write
                         (make-constructor-style-printer  
                          (λ (obj)
                            (apply string-append
                                   (symbol->string 'name)
                                   (for/list ((arg 'tag-params) (i (in-naturals 0)))
                                     (print-params (ref obj i) arg))))
                          (λ (obj) '()) )))))
          (define (explain obj)
            (for/list [(p (cdr 'plain-params)) (i (in-naturals 0))]
              (list p (ref obj i))))
          (define (name fields ... . xs)            
            (apply maker plain-params))
          ;;; accessors!!
          #|(define whatev
            (append 'field-names
                    'super-fields))|#
          ;#,@[if ]
          ]))))

(define-syntax (card stx)
  (syntax-case stx ()
    ([_ (name fields ... . xs)]
     #'[protocard #f (name fields ... . xs)])
    ([_ super (name fields ... . xs)]
     #'[protocard super (name fields ... . xs)])))

;;;;;;;;;;;;;;;;;;;;;;;;;;;

;;

(define-for-syntax (tag-args xs [rs '()])
  (define parsed
    (if [null? xs] '[]
       [let ((a (car xs))
             (bs (cdr xs)))
         (cond ((symbol? a) (cons a (tag-args bs)))
               ((list? a) (cons a (tag-args bs)))
               ((keyword? a)
                (cons (list a (car bs))
                      (tag-args (cdr bs)))))]))
  (if [null? rs] parsed [append parsed (list rs)]))
 
;;;;;;;;;;;;;;;;;;;;;;;;;;

(define  (write-if-not-default a-pair val)
  (if [equal? (cadr a-pair) val] ""
      [string-append " " (~a val)]))

(define (print-params val arg)
  (cond
    ((symbol? arg)
     (string-append " " (~a val)))
    ((keyword? (car arg))
     (let ((x (cadr arg))
           (res (string-append " " keyword-prefix  
                               (keyword->string (car arg))
                               keyword-suffix " " (~a val))))
       (if (list? x)  ;; if it's a default arg, x (cadr arg) appears as a list
           (if (eq? (cadr x) val) ""  ;; don't show if it has the default value (cadr x)
               res) res)))
    ((list? arg)
     (write-if-not-default arg val))))
Reply all
Reply to author
Forward
0 new messages