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
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.
- 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 ...] ...))
- 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]))])
-
#: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:
- Using with-syntax, which is a kind of let for patterns, conveniently takes ellipsis. This allows taking apart a sub-pattern, called pick below.
- 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.
- 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.
- 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.
- 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.
- 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