2
votes

I'm trying to make a switch statement macro in Racket. I'm having some trouble figuring out excatly how to do it. I would like to be able to use the function as shown below.

(define x 99)

(switch x
    [3 (displayln "x is 3")]
    [4 (displayln "x is 4")]
    [5 (displayln "x is 5")]
    ['default (displayln "none of the above")])

I've tried using pattern matching with syntax-case but i'm not sure that is the correct approach. Any Racket experts here that could give me a push in the right direction?

3

3 Answers

2
votes

I believe in not reinventing perfectly good wheels, so here's a macro for transforming your switch into the equivalent case, with a slight change to use default instead of 'default. (I apologise to soegaard, for not knowing how to use syntax-parse yet, so I'll just stick with traditional syntax-case instead. ;-))

(define-syntax (switch stx)
  (define (transform-clause cl)
    (syntax-case cl (default)
      ((default expr) #'(else expr))
      ((val ... expr) #'((val ...) expr))))

  (define (transform-clauses cls)
    (syntax-case cls ()
      ((cl)
       (with-syntax ((case-clause (transform-clause #'cl)))
         #'(case-clause)))
      ((cl rest ...)
       (with-syntax ((case-clause (transform-clause #'cl))
                     ((case-rest ...) (transform-clauses #'(rest ...))))
         #'(case-clause case-rest ...)))))

  (syntax-case stx ()
    ((_ x clause ...)
     (with-syntax (((case-clause ...) (transform-clauses #'(clause ...))))
       #'(case x case-clause ...)))))

The downside of using this switch macro, as opposed to using case, is that you lose the ability to distinguish between using default to match the symbol default, vs as the catch-all. So using case is still better. :-)

(case x
  ((3) (displayln "x is 3"))
  ((4) (displayln "x is 4"))
  ((5) (displayln "x is 5"))
  (else (displayln "none of the above")))
2
votes

I like both of the other answers, but I feel like we should also mention the existing match form, which (afaict) already does exactly what you're looking for:

#lang racket

(define x 99)

(match x
  [3 (displayln "x is 3")]
  [4 (displayln "x is 4")]
  [5 (displayln "x is 5")]
  [default (displayln "none of the above")])

The only changes: I write match instead of switch, and I use the pattern default rather than the quoted 'default. In fact, any identifier would work here; an identifier just gives a name to a value.

Indeed, match can do a whole lot more than this, but this is one great use for it.

If you're just looking for practice in writing macros, then you can disregard this answer :).

1
votes

For switch I recommend making a helper macro that switches on a value - this avoids the problem of evaluating the expr in (switch expr clause ...) more than once.

Below I used the identifier else to indicate the default clause.

Note that syntax-parse try the patterns one-at-a-time. This is used to catch raise syntax errors given faulty input such as (switch)

The initial identifier in the patterns are prefixed with a _ to avoid any problems in recursive macros.

Note that the basic rewrite rule used is:

(switch-value v
   [expr result]
   clause ...)

==>

(if (equal? v expr)
    result
    (switch-value v clause ...))

Making the macro recursive is easier than writing one big macro that handles all clauses at once.

#lang racket

(require (for-syntax syntax/parse))

(define-syntax (switch stx)
  (syntax-parse stx
    [(_switch)
     #'(raise-syntax-error 'switch "value expression and at least one clause expected" stx)]
    [(_switch expr clause ...)
     #'(let ([v expr])
         (switch-value v clause ...))]))

(define-syntax (switch-value stx)
  (syntax-parse stx
    #:literals (else)
    [(_switch-value v)
     #'(raise-syntax-error 'switch "at least one clause is expected" _switch-value)]
    [(_switch-value v [else expr])
     #'expr]
    [(_switch-value v [expr1 expr2] clause ...)
     #'(if (equal? v expr1)
           expr2
           (switch-value v clause ...))]))


(define x 4)

(switch x
  [3 "x is 3"]
  [4 "x is 4"]
  [5 "x is 5"]
  [else (displayln "none of the above")])