Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Racket Macro Ellipsis Syntax

I have a macro that's working when one argument is passed, and I'd like to expand it to accept n number of arguments using ..., but I'm having trouble figuring out the syntax.

The macro accepts either custom syntax, ie, key:val key:val, or it accepts a procedure.

For example: (3 different usages)

(schema-properties [(name:first-name type:string)])
(schema-properties [(name:age type:number required:#t)])
(schema-properties [(my-custom-fn arg1 arg2 arg3)])

Definition:

(define-syntax (schema-properties stx)
  (syntax-parse stx
    [(_ [(prop:expr ...)])
     (with-syntax ([prop0 (make-prop-hash #'(prop ...))])
       #'(list prop0))]))

(define-for-syntax (make-prop-hash stx)
  (with-syntax ([(props ...) stx])
    (if (regexp-match #px":"
                      (symbol->string (car (syntax->datum #'(props ...)))))
        #'(pairs->hash 'props ...)
        #'(props ...))))

This works, in that it checks the prop:expr syntax for the presense of ":", and if it exists, passes it to the function (pairs->hash 'props ...), otherwise, it just invokes it (props ...).

Now, I'd like to be able to pass in:

(schema-properties [(name:first-name type:string)
                    (name:last-name type:string)
                    (my-fn arg1 arg2 arg3)])

and have it work the same way. But I'm currently in ellipsis hell and my brain is no longer working correctly.

Any insights are appreciated.

like image 639
Scott Klarenbach Avatar asked Feb 16 '23 18:02

Scott Klarenbach


2 Answers

Recommendation: use helper functions to help deal with nesting. Your schema-properties macro knows how to deal with one level of nesting, and you want to apply that to multiple clauses. It's the same principle as when we deal with lists of things: have a helper to deal with the thing, and then apply that across your list. It helps cut down complexity.

For your code, we can do it like this:

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

(define-syntax (schema-properties stx)
  (syntax-parse stx
    [(_ [clause ...])
     (with-syntax ([(transformed-clauses ...)
                    (map handle-clause (syntax->list #'(clause ...)))])
       #'(list transformed-clauses ...))]))


;; handle-clause: clause-stx -> stx
(define-for-syntax (handle-clause a-clause)
  (syntax-parse a-clause
    [(prop:expr ...)
     (make-prop-hash #'(prop ...))]))


(define-for-syntax (make-prop-hash stx)
  (with-syntax ([(props ...) stx])
    (if (regexp-match #px":"
                      (symbol->string (car (syntax->datum #'(props ...)))))
        #'(pairs->hash 'props ...)
        #'(props ...))))


;;; Let's try it out.  I don't know what your definition of pairs->hash is,
;;; but it probably looks something like this:
(define (pairs->hash . pairs)
  (define ht (make-hash))
  (for ([p pairs])
    (match (symbol->string p)
      [(regexp #px"([-\\w]+):([-\\w]+)" 
               (list _ key value))
       (hash-set! ht key value)]))
  ht)

(schema-properties [(name:first-name type:string)
                    (name:last-name type:string)
                    (list 1 2 3)])
like image 109
dyoo Avatar answered Apr 30 '23 10:04

dyoo


Another recommendation: use syntax classes to help deal with nesting:

First, define a syntax class that recognizes key:value identifiers (and makes their component strings available as key and value attributes):

(begin-for-syntax
  (define-syntax-class key-value-id
    #:attributes (key value)
    (pattern x:id
             #:do [(define m (regexp-match "^([^:]*):([^:]*)$" 
                                           (symbol->string (syntax-e #'x))))]
             #:fail-unless m #f
             #:with (_ key value) m)))

Now define a clause as either a sequence of those (to be handled one way) or anything else (to be treated as an expression, which must produce a procedure). The code attribute contains the interpretation of each kind of clause.

(begin-for-syntax
  (define-syntax-class clause
    #:attributes (code)
    (pattern (x:key-value-id ...)
             #:with code #'(make-immutable-hash '((x.key . x.value) ...)))
    (pattern proc
             #:declare proc (expr/c #'(-> any))
             #:with code #'(proc.c))))

Now the macro just puts the pieces together:

(define-syntax (schema-properties stx)
  (syntax-parse stx
    [(_ [c:clause ...])
     #'(list c.code ...)]))
like image 40
Ryan Culpepper Avatar answered Apr 30 '23 09:04

Ryan Culpepper