Macro Template Syntax Quasi-Quote with Ellipses

I'm using the below as an example to ask the question. Please recall that case works like this:

(case (+ 7 5)
   [( 1 2 3)    'small]
   [( 10 11 12) 'big])

Let's say I never need more than one match for the same body. So, I write a macro to allow dropping the extra () around all the datum clauses (labeled num below):

(define-syntax-rule (my-case check [ num body] ...)
  (case check [( num) body] ...))

This way I can just say:

(my-case check      ->    (case check
  [ 1 (action1)]    ->      [( 1) (action1)]
  [ 5 (action5)])   ->      [( 5) (action5)])

And the macro will put-in the missing () for the correct use of case. But what if I want the template to be conditionally built on the type of num either being: 1) a number? to behave as above, or being: 2) a list? to behave the normal way case works, like this:

(my-case check                  ->  (case check
  [ 1       (action1)]          ->    [( 1)     (action1)]
  [ 5       (action5)]          ->    [( 5)     (action5)]
  [( 2 4 6) (action-special))   ->    [( 2 4 5) (action-special)])

What can I do to the code below to make it work for more clauses? Where should the ellipsis ... go?

(define-syntax (my-case stx)
  (syntax-case stx ()
    [(_ check [num body] ) ;like this?: body] ... )  this gives this error:
     #` (case check        ;missing ellipsis with pattern variable in template
         [#,(if (number? (syntax-e #'num))
                (list #'num)
                #'num)
          body])]))

I feel I'm missing something basic. How can I conditionally build a template while unravelling the ellipsis into the parent template accordingly? Maybe I'm approaching it wrong? Any advise would be appreciated.

1 Like

If I am not mistaken:

(define-syntax (my-case stx)
  (syntax-case stx ()
    [(_ check [num body ...] ...)
     (with-syntax (((the-num ...)
                    (for/list ((num1 (syntax->list #'(num ...))))
                      (if (not (list? (syntax->datum num1)))
                          (list num1)
                          num1))))
       #'(case check
           [the-num
            body ...] ...))]))

Sorry for the weird identifier names, of course it evolved from my original "just use double ellipsis" a bit :wink:

1 Like

There are several possible approaches. Below I will use the syntax/parse library, but you should be able to translate them to vanilla Racket constructs — syntax-case / define-syntax-rule / syntax-rules — except when indicated otherwise.

  1. syntax class: this is what I would personally do. It can’t be easily translated to vanilla Racket because syntax class is a concept introduced in syntax/parse.
#lang racket

(require syntax/parse/define)

(begin-for-syntax
  (define-syntax-class lhs
    (pattern x:number #:with out #'(x))
    (pattern (y:number ...) #:with out #'(y ...))))

(define-syntax-parse-rule (my-case v [l:lhs r ...+] ...)
  (case v [l.out r ...] ...))

  1. recursive expansion: this is a pure term rewriting, with no escape back to Racket
#lang racket

(require syntax/parse/define)

(define-syntax-parse-rule (my-case v [lhs body ...+] ...)
  (my-case* v
            ([lhs body ...] ...)
            ())) ;; this is an "accumulator"

(define-syntax-parser my-case*
  ;; no more clause to process
  [(_ v () ([lhs . body] ...))
   #'(case v [lhs . body] ...)]

  ;; process each clause one-by-one
  [(_ v ([x:number . body] rst ...) (out ...))
   #'(my-case* v (rst ...) (out ... [(x) . body]))]
  [(_ v ([(x:number ...) . body] rst ...) (out ...))
   ;; this puts stuff at the end of the list, which is inefficient if there are a lot of clauses
   #'(my-case* v (rst ...) (out ... [(x ...) . body]))])

  1. #:with / with-syntax: this is essentially @dominik.pantucek’s solution
#lang racket

(require syntax/parse/define)

(define-syntax-parse-rule (my-case v [num body ...+] ...)
  #:with (new-num ...)
  (map (λ (n)
         (if (list? (syntax-e n))
             n
             (list n)))
       (attribute num))

  (case v [new-num body ...] ...))

4 Likes

Thank you @dominik.pantucek. I know what you mean. Sometimes our intuition just works its magic. Yours was very clear. To recap, for my understanding:

  1. Using with-syntax, which is a kind of let for patterns, conveniently takes ellipsis. This allows taking apart a sub-pattern, called pick below.
  2. This allows us to go through a list in the main pattern to examine how (and most importantly if) it should go into our picked sub-pattern.
  3. Then very easily, in a normal #'block, we can just put back our sub-pattern that we just built.
(define-syntax (my-case stx)
  (syntax-case stx ()
    [(_ check [ num body ...] ...)
     (with-syntax ([( pick ...)
                    (for/list ([ head (in-list (syntax->list #'(num ...)))])
                      (if (not (list? (syntax->datum head)))
                          (list head)
                          head))])
       #'(case check
           [pick body ...] ...))]))

Thank you @sorawee.

  1. Here a syntax-class was used to recognize and name some sub-patterns using a notation that loads semantics inside token names as the spec for the syntax-class.
  2. Here a common recursive accumulation technique was used, but with a syntax-parser that takes sub-patterns then regularize them into/using an internal helper.
  3. Here apparently a syntax-parse-rule can have a directive for making a pattern-let that takes a function to fill it from on a specific sub-part in the main pattern.

Since you agree with @dominik.pantucek on the last one, I'll pick his version since it's in pure Racket. This tour was wonderful and gave me a sense for how robust the syntax-parse library approaches the subject. Thanks for taking the time!

Another option would be to use something other than case, with match being the obvious choice. For example:

(match check
  [ 1 (action1)]
  [ 5 (action5)])

case is intended to be used when you have multiple items that might trigger the action. If there's only one then you're likely better off with match.

Thanks, I was just using that as an example to ask a specialized macro question. My understanding of syntax-unquote with ellipsis needed some guidance towards the natural common usage. I take your point though. Sometimes the answer is not to bend a tool for the usage (like we can with macros), but to use the right tool in the first place. Note that case has a magical dispatch research paper behind it with multiple strategies, while match is a specialized tool that checks its patterns in sequence per the documentation.

2 Likes