macrosracket

How to define many variables in a loop, with names given by template?


I want to refactor this sort of pattern, where many top-level variables are assigned with a given naming scheme.

#lang racket

(define ((f-x/y x y) z)
  ;; nominal example
  (list x y z))

(match-define (list f-a/a f-a/b f-a/c
                    f-b/a f-b/b f-b/c
                    f-c/a f-c/b f-c/c)
  (for*/list ([x '(a b c)]
              [y '(a b c)])
    (f-x/y x y)))

(f-b/a 'd) ;; => '(b a d)

The problem seems two-fold: defining anything inside a loop so that it can be seen outside (defying lexical scope?), and defining a variable whose name is created by a template.

Ideally something like this is possible; format-id at least seems like the right direction.

(require racket/syntax)

;; notional
(for*/defs ([x '(a b c)]
            [y '(a b c)])
  (define (format-id #'x "f-~a/~a" x y)
    (f-x/y x y)))

For what it's worth, all the '(a b c) elements are fixed and known at compile-time. The first code block above works and is fine in practice, but I'd really like to learn the macro techniques that presumably exist to solve this sort of problem.

UPDATE

The variation on Shawn's answer I've gone with is this:

(require (for-syntax racket/syntax syntax/parse))

(define-syntax (define-all-options stx)
  (syntax-parse stx
    [(_ f:id (sym:id ...))
     (for*/lists (ids defs #:result
                  #`(begin
                      (define-values (sym ...) (values 'sym ...))
                      (define-values #,ids (values #,@defs))))
                 ([x (in-list (syntax->list #'(sym ...)))]
                  [y (in-list (syntax->list #'(sym ...)))])
       (values
         (format-id stx "f-~a/~a" x y)
         #`(f #,x #,y)))]))

(define ((f-x/y x y) z) (list x y z))
(define-all-options f-x/y (a b c))
(println (f-a/b 'd))

Solution

  • I've done things like this before, and, yeah, format-id is the right approach. The lexical context can just be the top level syntax object passed to the macro that's using the function.

    An example:

    #lang racket/base
    
    (require (for-syntax racket/base racket/list racket/syntax syntax/parse))
    
    (define ((f-x/y x y) z) (list x y z))
    
    (define-syntax (define-all-options stx)
      (syntax-parse stx ; Use syntax-parse instead of syntax-case to get type checks in the macro pattern
        [(_ f:id (sym:id ...))
         (let* ([symbols (syntax->list #'(sym ...))] ; list of syntax objects for the given symbol
                [pairs (cartesian-product symbols symbols)] ; every combination of 2 ids
                [identifiers ; build the names of the functions to define
                 (for/list ([combo (in-list pairs)])
                   (format-id stx "f-~a/~a" (first combo) (second combo)))]
                [constructors ; build the function calls whose return values are bound to the above names
                 (for/list ([combo (in-list pairs)])
                   #`(f #,@combo))])
           ;; and put all the parts together
           #`(define-values #,identifiers (values #,@constructors)))]))
    
    (define a 'a)
    (define b 'b)
    (define c 'c)
    (define-all-options f-x/y (a b c))
    (println (f-a/b 'd))
    

    I find it's better to just take literal identifiers instead of a quoted list, to keep things simple in the macro. This does mean those identifiers need to be defined before calling the macro.