CS 1102: Notes on Racket Syntax


What's so good about Racket's #$&%#!@ syntax?

Here's a new syntax for lists:

(list 1 2 3) = '(1 2 3)

(list 1 (list 'a 'b) 2) = '(1 (a b) 2)  

(list (list (list 1 2) (list 'a 'b) 3))
Notice no ' on symbol anymore. And in reverse ...
'(a (b c) ((d))) = (list 'a (list 'b 'c) (list (list 'd)))       
So, ' isn't just for symbols -- it provides a concise way to write nested lists. As for symbols, ' says "don't evaluate what follows -- just treat it as data".

But how is this useful? For starters, it provides a handy way to read in complex data structures. Assume I wanted to write programs to manage my gradebook. I'll define a simple struct:

(define-struct entry (name hw1 hw2 hw3))
I want to keep my gradebook in a file. I could use the following format (assume this is the content of file gb1):
Bert  75 80 95
Ernie 60 90 82
Oscar 72 79 77
How would I read this in to compute final grades? One option is to open the file and read one piece of data at a time. I can do that with the following code:
(define (read-loop1)
  (let ([next (read)])
    (cond [(eof-object? next) empty]
	  [else (let ([h1 (read)]
		      [h2 (read)]
		      [h3 (read)])
		  (cons (make-entry next h1 h2 h3)
			(read-loop1)))])))

(with-input-from-file "gb1" read-loop1)
The read code is messy though: my program has to keep track of how many items to read in for one person's grades, and I don't want to edit the read loop every time I add another field to the gradebook. I could set up my file better (this is file gb2):
(Bert  75 80 95)
(Ernie 60 90 82)
(Oscar 72 79 77)
Now when I read, I get a whole person's entry at a time, which makes my gradebook reader cleaner. (define (read-loop2) (let ([next-line (read)]) (cond [(eof-object? next-line) empty] [else (cons (apply make-entry next-line) (read-loop2))]))) (with-input-from-file "gb2" read-loop2) This code still uses a loop to read in the list (and if you were in C++, you'd need to turn the result of reading in the line into a list). I can simplify this program even more by making one more change to my file:
((Bert  75 80 95)
 (Ernie 60 90 82)
 (Oscar 72 79 77))
Now when I read on the file, I get the entire data structure as a list. Turning it into a list of entries is easy (define (read-loop3) (map (lambda (data) (apply make-entry data)) (read))) (with-input-from-file "gb2" read-loop3) That's it! From that simple command I can build an entire gradebook data structure from a file (writing it back out after making changes is about as simple). Racket's use of simple parens to build lists lets me create lists in files, read them in, and process them quickly and easily.
Rewrite the following list using quote:
(list 'define (list 'f 'x) (list '+ 'x 1))
= '(define (f x) (+ x 1))
Wait -- that's odd: (define (f x) ...) is a function [control], but here we're writing it as a list [data]. What's going on?

In Racket, expressions (control) and data (lists) have the same syntax. Why is that useful? For starters, it's great for I/O (since we usually read/write data to/from files).

> (with-input-from-file "myinput.ss" (lambda () (read)))
(define (square x) (* x x))

;; add-action : func-as-list -> func-as-list
;; adds action parameter to function and sends body to action
(define (add-action func)
  (let ([func-name (first (second func))]
	[func-params (rest (second func))]
	[func-body (third func)])
    (list 'define 
	  (append (list func-name) func-params (list 'action))
	  (list 'action func-body))))

> (with-input-from-file "myinput.ss" 
    (lambda ()
      (add-action (read))))

> (with-output-to-file "myoutput.ss"
    (lambda ()
      (display
       (with-input-from-file "myinput.ss"
	 (add-action (read))))))

> square
reference to undefined identifier: square

> (with-input-from-file "myinput.ss"
    (lambda () (eval (read))))

> square
#
In Racket, the syntax of programs is the same as the syntax for data!

Note that this isn't true in C++ (or Java/Perl/etc):

  #include 

  main() {
      cout << "hello, world\n";
  }
Is a program, but you can't write this down as data in C++ (native C++ data includes numbers, strings, arrays, etc). At best, you can make a string out of it, but that string has no structure. In Racket, the list-version means that you can easily access the function name, parameters, and body.
Let's look at another difference between list and quote.
(list 1 a 3)
'(1 a 3)
List is an operator, so its arguments get evaluated. When you use quote, the following list/datum is turned into data without being evaluated. Could we rewrite the add-action program using quote instead of list?

For a first try, let's remove all the calls to list and replace with a single quote on the outside.

;; add-action : func-as-list -> func-as-list
;; adds action parameter to function and sends body to action
(define (add-action func)
  (let ([func-name (first (second func))]
	[func-params (rest (second func))]
	[func-body (third func)])
    '(define (append (func-name) func-params (action))
       (action func-body))))
Does this give us back the answer that we want? No. Since quote prevents evaluation, the append shows up literally in the returned function. We want the append to get evaluated to construct the appropriate header list. But we want the action to get treated literally as a symbol. With list, everything gets evaluated while nothing gets evaluated with quote. Isn't there a happy medium?

Yes, and you finally get to program with commas. Racket let's you mix evaluation and non-evaluation mode by using backquote in place of quote and commas before the expressions to get evaluated. Let's try that on the func-body in the call to action (last line of this code):

;; add-action : func-as-list -> func-as-list
;; adds action parameter to function and sends body to action
(define (add-action func)
  (let ([func-name (first (second func))]
	[func-params (rest (second func))]
	[func-body (third func)])
    `(define (append (func-name) func-params (action))
       (action ,func-body))))
The output of this now shows the function body dropped in, rather than the identifier "func-body", as we wanted.

Let's do the same with the call to append:

;; add-action : func-as-list -> func-as-list
;; adds action parameter to function and sends body to action
(define (add-action func)
  (let ([func-name (first (second func))]
	[func-params (rest (second func))]
	[func-body (third func)])
    `(define ,(append (func-name) func-params (action))
       (action ,func-body))))
This generates an error because (func-name) tries to evaluate the identifier f. We need to construct this list using parentheses and quote notation instead.
(define (add-action func)
  (let ([func-name (first (second func))]
	[func-params (rest (second func))]
	[func-body (third func)])
    `(define (,func-name ,func-params action)
       (action ,func-body))))
Almost right, but notice the return value:
> (add-action '(define (f x) (+ x 1)))
(define (f (x) action) (action (+ x 1)))
There's an extra set of parens around the x. We really want to merge the func-params list into the rest of the list that we're constructing. For that, we use a augmented comma syntax (,@) that splices one list into another:
(define (add-action func)
  (let ([func-name (first (second func))]
	[func-params (rest (second func))]
	[func-body (third func)])
    `(define (,func-name ,@func-params action)
       (action ,func-body))))
Now, we get the answer that we wanted:
> (add-action '(define (f x) (+ x 1)))
(define (f x action) (action (+ x 1)))

As one last example, try writing a function that converts a function definition without lambda to one with lambda. For example,
  (convert-to-lambda '(define (f x) (+ x 1))) should return
  '(define f (lambda (x) (+ x 1)))

(define (convert-to-lambda func)
  (let ([func-name (first (second func))]
	[func-params (rest (second func))]
	[func-body (third func)])
    `(define ,func-name 
       (lambda ,func-params ,func-body))))
What was the point of showing you this? By treating programs and data uniformly, Racket provides some powerful capabilities for building one program from another. Once you're skilled with these features I've shown here, and if you combine them with macros, you can create custom languages and software tools quickly and easily. This is a hint of what Graham talked about in his article. Few other languages provide these features, and certainly not in this fashion.