;; --------------------------------------------------------------------- ;; This file implements a range-case macro motivated by the code we ;; wrote for the opening exercise in Session 4 ;; http://www.cs.uni.edu/~wallingf/teaching/cs3540 ;; /sessions/session04.html#cond-exercise ;; (range-case student-grade ;; ((>= 0.90) 'A) ;; ((>= 0.80) 'B) ;; ((>= 0.70) 'C) ;; ((>= 0.60) 'D) ;; (else 'F)) ;; ;; expands into ;; ;; (if (>= student-grade 0.90) ;; 'A ;; (if (>= student-grade 0.80) ;; 'B ;; (if (>= student-grade 0.70) ;; 'C ;; (if (>= student-grade 0.60) ;; 'D ;; 'F)))) ;; --------------------------------------------------------------------- #lang racket (require rackunit) (provide range-case) (define-syntax range-case (syntax-rules (else) [(range-case id (else default)) default] [(range-case id ((compare bound) value) rest ...) (if (compare id bound) value (range-case id rest ...))] )) ;; --------------------------------------------------------------------- ;; a few rackunit tests ;; --------------------------------------------------------------------- (define student-grade (/ 248.0 285)) (check-equal? (range-case student-grade ; the example from Session 4 ((>= 0.90) 'A) ((>= 0.80) 'B) ((>= 0.70) 'C) ((>= 0.60) 'D) (else 'F)) 'B) (check-equal? (range-case student-grade (else 'F)) 'F) (check-equal? (range-case student-grade ((>= 0.90) 'A) (else 'F)) 'F) (check-equal? (range-case 0.95 ((>= 0.90) 'A) (else 'F)) 'A) ;; --------------------------------------------------------------------- ;; We can expand the macro as sytntax to see the translation ;; --------------------------------------------------------------------- ;; (expand-once #'(range-case taxable-income ;; ((<= 12000) '( 0 0.044 0.00)) ;; ((<= 60000) '( 12000 0.0482 528.00)) ;; ((<= 150000) '( 60000 0.057 2841.60)) ;; (else '(150000 0.06 7971.60)))) ;; --------------------------------------------------------------------- ;; What if we want to pass more complex expression as the first arg: ;; (range-case (compute-grade ...) ;; ((>= 0.90) 'A) ;; ((>= 0.80) 'B) ;; ((>= 0.70) 'C) ;; ((>= 0.60) 'D) ;; (else 'F)) ;; This would plug (compute-grade ...) in place of id everywhere -- and ;; thus recompute the value over and over. If the expression is ;; time-consuming, or touches external resources such as a database, or ;; has side effects, this is unacceptable. ;; --------------------------------------------------------------------- ;; Instead of expanding the range-case to an if exp, we want to expand ;; it to a let exp that computes the value once, binds the value to a ;; local variable, and then uses that variable to create the if exp. ;; --------------------------------------------------------------------- (define-syntax range-case-v2 (syntax-rules () [(range-case-v2 exp form1 ...) (let ((id-for-exp exp)) (range-case id-for-exp form1 ...))] )) (check-equal? (range-case-v2 (/ 248.0 285) ((>= 0.90) 'A) ((>= 0.80) 'B) ((>= 0.70) 'C) ((>= 0.60) 'D) (else 'F)) 'B) ;; --------------------------------------------------------------------- ;; Notice that this macro calls our previous macro! ;; ;; The Racket preprocessor keeps expanding syntactic abstractions until ;; they reach core forms. ;; --------------------------------------------------------------------- ;; --------------------------------------------------------------------- ;; But is there a potential problem here? This macro expands to a let ;; expression that creates a local variable named 'id-for-exp. What if ;; that variable already exists in the calling context? Even worse, ;; what if that variable is part of one of the arguments passed to ;; range-case-v2? Won't there be a name clash? ;; --------------------------------------------------------------------- ;; To find out, uncomment this code and Run. ;; --------------------------------------------------------------------- ;; (define id-for-exp 285) ;; (range-case-v2 (/ 248.0 id-for-exp) ;; ((>= 0.90) 'A) ;; ((>= 0.80) 'B) ;; ((>= 0.70) 'C) ;; ((>= 0.60) 'D) ;; (else 'F)) ;; id-for-exp ;; --------------------------------------------------------------------- ;; In most languages' macro systems, this would be a problem. But ;; Racket macros are "hygienic". They ensure that names created inside ;; of a macro are unique, so that there will never be a clash between a ;; variable created by the macro and a variable in the calling context. ;; Beautiful! ;; ---------------------------------------------------------------------