12 Extending Languages
Goals |
— |
12.1 Functions on Syntax
Racket’s compiler is programmable. The most basic way to change the compiler is to define a compile-time function. This is done with define-syntax, which looks like an ordinary function definition.
regular function |
| compile-time function |
(define (f1 x) 5) |
| (define-syntax (f x) #'5) |
Why #'5 instead of 5? Compile-time functions must generate code, and the hash-quote is one way to generate code. It is just a short-hand for syntax.
So how do we use f? Like any other function, except that it is run at compile time, not at run time.
(f 10)
(f) (f 10 "hello world")
; Syntax -> Syntax ; generate the same code no matter what the argument (define-syntax (f-display stx) (displayln stx) #'5) (f-display) (f-display 10) (f-display 10 "hello world")
Since Racket belongs to the Lisp family, there is a function that extracts the underlying list from the syntax tree. We can, for example, take it is length and translates a syntax tree into code that represents its length.
(define-syntax (g stx) (define n (length (syntax->list stx))) #`#,n) (g) (g a) (g a b)
Why #`#,n? Well, hash-backquote also generates code. But, unlike hash-quote it allows unquoting. And you guessed it, hash-comma is unquote at the syntax level.
(define-syntax (i stx) (define l (syntax-line stx)) (define c (syntax-column stx)) #`(list "line and column info" #,l #,c)) (i 1)
(require (for-syntax racket/list)) (define-syntax (world stx) (define expr (syntax-e stx)) (define iden (second expr)) (define code (list 'define iden "hello world, how are you?")) (datum->syntax stx code)) (world hello) hello
12.2 syntax-parse
How do real functions really take apart their arguments? Pattern matching of course!
And syntax-parse is yet another embedded language for pattern matching in Racket. In contrast to match or Redex’s pattern matcher, it is tuned to help with syntax-processing functions.
(require (for-syntax syntax/parse)) (define-syntax (j stx) (syntax-parse stx ((_ x) #'(define x "j")))) (j x) x (j y) y
(j 1)
(define-syntax (k stx) (syntax-parse stx ((_ x:id) #'(define x "k")))) (k 1)
(require rackunit syntax/macro-testing) (check-exn #rx"k: expected identifier" (lambda () (convert-syntax-error (k 1))))
We will do so one step at a time:
(define-syntax (local stx) (syntax-parse stx ((_ ((x1:id e1:expr)) e) #'(let ([x1 e1]) e)) ((_ ((x1:id e1:expr) (x2:id e2:expr)) e) #'(let ([x1 e1][x2 e2]) e))))
(define-syntax (local stx) (syntax-parse stx #:literals (and) ((_ ((x1:id e1:expr)) e) #'(let ([x1 e1]) e)) ((_ ((x1:id e1:expr) (x2:id e2:expr)) e) #'(let ([x1 e1][x2 e2]) e)) ((_ ((x1:id e1:expr) and (x2:id e2:expr)) e) #'(letrec ([x1 e1][x2 e2]) e))))
(define-syntax (in stx) (raise-syntax-error 'in "used out of context" stx)) (define-syntax (local stx) (syntax-parse stx #:literals (and in) ((_ ((x1:id e1:expr)) e) #'(let ([x1 e1]) e)) ((_ ((x1:id e1:expr) (x2:id e2:expr)) e) #'(let ([x1 e1][x2 e2]) e)) ((_ ((x1:id e1:expr) and (x2:id e2:expr)) e) #'(letrec ([x1 e1][x2 e2]) e)) ((_ ((x1:id e1:expr) in (x2:id e2:expr)) e) #'(let* ([x1 e1][x2 e2]) e))))
(local ((x 2) (y 2)) (* x y)) (local ((x 2) in (y x)) (* x y)) (local ((f (λ (x) (g x))) and (g (λ (y) (if (= y 1) 2 (f (- y 1)))))) (f 3))
(conjunction #true #false)
(conjunction #true #true)
(conjunction #false (smt-solver problem-with-one-gzillion-variables))
(define-syntax (conjunction stx) (syntax-parse stx [(_ lhs:expr rhs:expr) #'(if lhs rhs #false)]))
(define-syntax (conjunction stx) (syntax-parse stx [(_ lhs:expr rhs:expr) #'(and-function lhs (lambda () rhs))])) (define (and-function arg1 suspended-arg2) (if arg1 (suspended-arg2) #false))