Functional challenge III
April 11, 2008
As I said, in this post I will cover a simple Haskell-like generator implementation in Common Lisp. The cases I will cover are:
[x..] ; gives [x, x + 1, x + 2 ...] [x1,x2..] ; gives [x, x + d, x + (2 * d)...], where d = x2 - x1 [x1..xn] ; gives [x, x + 1 ... xn] [x1,x2..xn] ; gives t[x, x + d, x + (2 * d) ... xn], where d = x2 - x1
Now that we have lazy lists (from the last post) cases like the first and the third are no longer a problem. What about the Haskell-ish syntax I promised? Reader macros to the rescue. Reader macros are Lisp’s way to let the programmer mess with the reader (the reader is a Lisp’s system component that transforms textual representations of objects to actual objects; more on reader macros here). It’s not as hard as it sounds:
(set-macro-character #\[ #'(lambda (s c) (generator s c)))
This tells the Lisp reader to do something whenever it sees the '[' character (in Lisp characters are prefixed with #\ -- that's actually a reader macro, too :) ). In this case it calls (well, the anonymous function denoted by the lambda calls our function, but I won't get into details here) the generator function passing it two parameters: a stream and the matched character (here '['). The function can parse the stream in any way it likes, thus being able to define a completely new syntax.
First of all we need some utility functions:
(defun read-while (stream condition string-builder)
"Reads characters from a stream pushing them into the string-builder while the condition evaluates to true."
(setf (fill-pointer string-builder) 0)
(loop (if (funcall condition (peek-char nil stream))
(vector-push-extend (read-char stream) string-builder)
(return (> (fill-pointer string-builder) 0)))))
(defun parse-int (stream string-builder)
"Parses an integer from the stream."
(peek-char t stream)
(when (read-while stream #'(lambda (x) (or (digit-char-p x) (eq x #\-))) string-builder)
(parse-integer string-builder)))
(defmacro when-char (stream char &rest body)
"Evaluates the body when the next char in the stream is char and consumes it."
`(when (eq (peek-char t ,stream) ,char)
(read-char ,stream)
,@body))
(defmacro expect (stream char &optional (non-strict nil))
"If the next char in the stream is char it is consumed, otherwise an error is raised."
`(if (eq (peek-char ,non-strict ,stream) ,char)
(read-char ,stream)
(error "Expecting ~S~%" ,char)))
read-while takes a stream, a predicate and a buffer. It reads characters from the stream while the predicate is fulfilled. It returns true if it has read anything, false otherwise. After a call to read-while the matched string will be stored in the buffer passed (note: this is not necessarily good practice since passing the buffer around is clumsy; the alternative would have been to let read-while allocate the buffer).
parse-int parses an int from the stream. The function uses the built-in parse-integer to do the "heavy lifting". All it does is build a string and pass it to parse-integer.
the when-char macro executes the specified piece of code when the next char in the stream is the specified char.
expect raises an error if the next character in the stream is not the one specified.
It's time for the actual implementation of the generator.
(defun make-stream (start end &optional (step 1))
"Constructs a stream of numbers within the given range."
(if (and (not (null end)) (funcall (if (< step 0) #'< #'>) start end))
'()
(cons-stream start (make-stream (+ start step) end step))))
(defun generator (stream char)
(declare (ignore char))
(let* ((sb (make-array 0 :element-type 'character :fill-pointer 0 :adjustable t))
(start (parse-int stream sb)) (end) (step 1))
(when-char stream #\, (setf step (- (parse-int stream sb) start)))
(when-char stream #\. (expect stream #\.))
(setf end (parse-int stream sb))
(expect stream #\] t)
`(make-stream ,start ,end ,step)))
First we need a function that constructs an integer stream. make-stream does exactly that. Next we define generator, the function that the reader-macro uses. It parses a generator obeying the syntax described in the beginning of the post and returns a stream of integers.
Finally, to test the generators, we define the well known take function from Haskell:
(defun take (n stream) (cond ((endp stream) nil) ((= n 0) nil) (t (cons (car-stream stream) (take (- n 1) (cdr-stream stream))))))
take returns a list made out of the first n elements of the stream.
Some usage examples:
CL-USER> (take 10 [1..]) (1 2 3 4 5 6 7 8 9 10) CL-USER> (take 10 [1, 3 ..]) (1 3 5 7 9 11 13 15 17 19) CL-USER> (take 10 [1 .. 5]) (1 2 3 4 5) CL-USER> (take 10 [1, -1 .. 5]) NIL CL-USER> (take 10 [1, -1 .. -10]) (1 -1 -3 -5 -7 -9) CL-USER>
Hope this was not so painful. Btw, can your language do this?