From posting-system@google.com Tue Jul 16 03:26:02 2002
Date: Mon, 15 Jul 2002 20:25:53 -0700
From: oleg@pobox.com (oleg@pobox.com)
Newsgroups: comp.lang.scheme
Subject: Scheme->syntax-rules [Was: hygienic macro primality tester]
Message-ID: <7eb8ac3e.0207151925.37dfbd59@posting.google.com>
Status: OR
A more systematic way of writing syntax-rules macros is to
mechanically convert the corresponding Scheme procedures. Scheme
functions are far easier to develop and debug. After we are sure our
Scheme code works, we submit our procedures to a
Scheme--to--syntax-rules compiler -- and use but don't look at the
result. As an example, this article develops a more general primality
tester syntax-rule macro based on the Eratosthenes sieve. This
approach also helps Joe Marshall, who wrote "I'm in need of an
INTERCAL compiler written in SYNTAX-RULES" Well, he can just compile
the existing INTERCAL compiler to syntax-rules.
* Eratosthenes sieve
* Pure-functional, generic implementation of the Eratosthenes sieve
* Compilation of the Scheme implementation to syntax-rules macros
* Eratosthenes sieve
We should first remark that most of the examples of the Eratosthenes
sieve -- in particular, the examples in Haskell distributions -- are
incorrect. They mis-represent the sieve algorithm. The beauty of the
Eratosthenes sieve is the complete absence of division (or related
'mod') operations. A division-free algorithm was quite an advantage in
ancient times. This fact lets us encode the primality testing with the
minimum of arithmetics.
The Eratosthenes sieve algorithm trades space for speed. We write out
numbers on a piece of papyrus, starting from 2 till the desired upper
limit. We take the first number -- two in our case -- and hole out
every second number. We scan the piece for the next non-holed-out
number (which will be three) and hole out every third number, starting
from three. At the end, only the prime numbers will be left. The
Eratosthenes sieve, according to the above description, has an
imperative flavor. That is how it is often implemented [Note 1].
Nothing however prevents us from re-writing the algorithm in a pure
functional style, as the next section shows.
The Eratosthenes sieve algorithm can be used for testing if a number n
is prime. We build a list of numbers 2 through n and run the
algorithm. If the last number on the list is "holed out", n is
composite. Otherwise, it is prime.
* Pure-functional, generic implementation of the Eratosthenes sieve
First, we need to build a list of numbers 2 through n.
; Given a number 'n', build a list (2..n)
(define (iota n)
(letrec
((loop
(lambda (curr counter)
(if (less-than-two? counter) '()
(cons curr (loop (incr curr) (decr counter)))))))
(loop (number-two) n)))
Note how generic this function is: it deals with integers as an
abstract data type. We do not care how integers are actually
implemented.
; The sieve algorithm itself
; Given the list (2..n), return a sieved list
; all composite numbers are replaced by 0
(define (sieve lst)
(letrec
((choose-pivot ; choose the first non-zero element in lst
(lambda (lst)
(cond
((null? lst) lst)
((number-zero? (car lst)) ; skip over zeros
(cons (car lst) (choose-pivot (cdr lst))))
(else
(cons (car lst) ; sieve with the pivot, and recurse
(choose-pivot
(do-sieve (car lst) (decr (car lst)) (cdr lst))))))))
; Run the sieve
; (do-sieve step current lst)
; Return the new lst with each step-th element sieved out:
; replaced by 0.
(do-sieve
(lambda (step current lst)
(cond
((null? lst) lst)
((number-zero? current) ; replace the current element with zero
(cons (number-zero) (do-sieve step (decr step) (cdr lst))))
(else
(cons (car lst) (do-sieve step (decr current) (cdr lst)))))))
)
(choose-pivot lst)))
; Check if a given number is a prime: return 'prime or 'composite
; We run the sieve and then check if the last element has been weeded out
(define (is-prime n)
(if (number-zero? (car (reverse (sieve (iota n)))))
'composite
'prime))
To test these functions, we need to supply an implementation of
integers. First we use the most convenient representation:
(define (number-zero) 0)
(define (number-two) 2)
(define (incr n) (+ 1 n))
(define (decr n) (- n 1))
(define (less-than-two? n) (< n 2))
(define number-zero? zero?)
BTW, these are the only integer "methods" that we need to run
the Eratosthenes sieve. We don't even need general addition, subtraction,
or comparisons -- let alone multiplication or division.
Here are sample test cases and their output:
(display (iota 5))
;==> (2 3 4 5)
(display (sieve (iota 5)))
;==> (2 3 0 5)
(display (sieve (iota 25)))
;==> (2 3 0 5 0 7 0 0 0 11 0 13 0 0 0 17 0 19 0 0 0 23 0 0)
(display (is-prime 19)) ;==> prime
(display (is-prime 25)) ;==> composite
Next, we choose a Peano-Church representation of integers:
(define (number-zero) '() )
(define (number-two) '(( () )) )
(define (incr n) (list n))
(define (decr n) (car n))
(define (less-than-two? n) (or (null? n) (null? (car n))))
(define number-zero? null?)
The tests become
(display (iota '((((( () ))))) ))
;==> (((())) (((()))) ((((())))) (((((()))))))
(display (sieve (iota '((((( () ))))) )))
;==> (((())) (((()))) () (((((()))))))
(display (is-prime '((((((((((((((((((( () ))))))))))))))))))) ))
;==> prime
(display (is-prime '((((((((((((((((((((((((( () ))))))))))))))))))))))))) ))
;==> composite
Everything seems to work as expected.
* Compilation to syntax-rules.
Now, we can mechanically compile iota, sieve and is-prime
procedures to syntax-rule macros. First, however, we need to implement
the primitives number-zero, number-two, etc. as syntax-rule macros,
and add them to the initial environment of the compiler:
,(lambda (env)
(env-extend env 'incr '?incr
applies-directly: #t
cps-code:
'(define-syntax ?incr
(syntax-rules ()
((_ n k) (??!apply k (n) ))))))
,(lambda (env)
(env-extend env 'decr '?decr
applies-directly: #t
cps-code:
'(define-syntax ?decr
(syntax-rules ()
((_ (n) k) (??!apply k n ))))))
,(lambda (env)
(env-extend env 'less-than-two? '?less-than-two?
applies-directly: #t
if-fork: 'ifless-than-two?
cps-code:
'(define-syntax ?less-than-two?
(syntax-rules ()
((_ ((n)) k) (??!apply k #f))
((_ x k) (??!apply k #t))))))
etc.
Now, we can just run the compiler:
(define test-Eratosthenes
'(
(define (iota n)
; exactly as above
)
(define (sieve lst)
; exactly as above
)
(define (is-prime n) ; exactly as above
(if (number-zero? (car (reverse (sieve (iota n)))))
'composite
'prime))
(define (reverse lst)
(letrec ((loop
(lambda (lst accum)
(if (null? lst) accum
(loop (cdr lst) (cons (car lst) accum))))))
(loop lst '())))
))
(write-out init-env test-Eratosthenes
; a test case
'(?iota ((((((( () )))))))
(??!lambda (x) (?sieve (??! x)
(??!lambda (x) (display '(??! x))))))
)
See the full code at
http://pobox.com/~oleg/ftp/Scheme/cps-macro-conv.scm
The compilation produces a file "/tmp/a.scm". We can interpret the
compiled code:
~> bigloo -i -hygien /tmp/a.scm
which prints the result:
(((())) (((()))) () (((((()))))) () (((((((()))))))))
If we macroexpaned the code, e.g.,
~> bigloo -hygien -syntax /tmp/a.scm
we would see that this result is indeed computed at macro-expand time.
I do not dare to show the compilation result. Ok, I will, but only a
little:
(define-syntax
?is-prime
(syntax-rules
()
((_ _?n _?kg1074)
(?iota _?n
(??!lambda
(g1081)
(?sieve
(??! g1081)
(??!lambda
(g1080)
(?reverse
(??! g1080)
(??!lambda
(g1079)
(?car (??! g1079)
(??!lambda
(g1078)
(?number-zero?
(??! g1078)
(??!lambda
(g1075)
(?iftrue?
(??! g1075)
(??!lambda
(g1076)
(??!apply _?kg1074 composite))
(??!lambda
(g1077)
(??!apply
_?kg1074
prime))))))))))))))))
(define-syntax
?iota
(syntax-rules
()
((_ _?n _?kg1029)
(letrec-syntax
((?loop (syntax-rules
()
((_ _?currg1031 _?counterg1032 _?kg1030)
(?ifless-than-two?
_?counterg1032
(??!lambda (g1033) (??!apply _?kg1030 ()))
(??!lambda
(g1034)
(?incr _?currg1031
(??!lambda
(g1036)
(?decr _?counterg1032
(??!lambda
(g1037)
(?loop (??! g1036)
(??! g1037)
(??!lambda
(g1035)
(?cons _?currg1031
(??! g1035)
_?kg1030)))))))))))))
(?number-two
(??!lambda
(g1038)
(?loop (??! g1038) _?n _?kg1029)))))))
The assembler (er, CPS) code is never pretty.
To test the primality tester, we compile the following code
(write-out init-env test-Eratosthenes
'(?is-prime (((((( () ))))))
(??!lambda (x) (display '(??! x))))
)
which macro-expands to (display 'composite)
and
(write-out init-env test-Eratosthenes
'(?is-prime ((((( () )))))
(??!lambda (x) (display '(??! x))))
)
which macro-expands to (display 'prime). Indeed, 6 is a composite
number and 5 is a prime.
The compilation to syntax-rules is hardly optimized. The
result is very slow. In general, the implementation of
syntax-rules on Bigloo (and many other systems) is slow and takes lots
of memory -- that's why it is off by default. We need to specify a
special flag -hygien to activate syntax-rules macros on Bigloo. I
guess no one bothered to optimize the macro expander, because no one
asked for that.
We must emphasize however the speed of developing the macros. We do
all the development and testing with regular Scheme procedures. Once
we have done all that, the translated syntax-rules macros _just work_. There
is no need for macrology any more.
Note 1. An optimized imperative-style Eratosthenes sieve is
implemented in
http://pobox.com/~oleg/ftp/Scheme/Eratosthenes-sieve.txt
That implementation took advantage of the fact that even numbers
greater than two are composite -- therefore, there is no point of
storing them. This improves the space and time complexities of the
implementation two-fold. The article computed all prime numbers under
one million, and verified a previously proven equality
Prod{ (1 - 1/p[i]^2), i=1..K} -> 1/Z[2] = 6/Pi^2
where p[i] is the i-th prime number (p[1]=2)