Comp 210 Challenge Lab 1: Macros and Simple I/O


s-expressions, quote, and quasiquote patterns

In a previous homework, we saw x-expressions, which were a particular way of writing down structured data. An x-expression is either a string or a symbol-tagged list of x-expressions. There is also a generalization of this idea called an s-expression.  In this lab, we will see some of the traditional uses of s-expressions.

An s-expression is one of
- a symbol
- a string
- a number
- a list-of-s-expressions
A list-of-sexpressions is one of
- empty
- (cons <s-expression> <list-of-se-xpressions>)

We saw in a previous lab that the function list can be used as a shortcut for writing down lists. There is another shortcut which is more useful when we want to express large s-expressions.  The special form quote interprets an s-expression as data, rather than trying to execute it.

(quote (1 2 happy sad)) => (list 1 2 'happy 'sad)
(quote angry) => 'angry

On the other hand, what if we want to make an s-expression which is mostly constant, except for some pices somewhere in the middle? Nothing inside of quote will be evaluated, so we can't use a placeholder to stick another value inside of a quoted list.  There is another form called quasiquote, which can be used to insert any expressions into an otherwise constant s-expression.

(define my-var 3)
(quote (1 2 my-var)) => (list 1 2 'my-var)
(quasiquote (1 2 (unquote my-var))) => (list 1 2 3)

quote can also be written as ' (the single quote mark), quasiquote as ` (the back-quote), and unquote as , (the comma).  In that syntax, the above examples become:

(define my-var 3)
'(1 2 my-var) => (list 1 2 'my-var)
`(1 2 ,my-var) => (list 1 2 3)

Like list, quote will never be of any use writing functions which recursively construct lists.  Be sure that you don't use list or the quote syntax when you mean cons.



define-macro

The Law of Scheme states that function applications are evaluated by first evaluating all of the arguments to the function, then substituting the values of the arguments into the body of the function. There are times when one wants to interpret source code as something other than code to be executed directly by DrScheme.  A macro is a "function" that takes in the source representation of its arguments and returns a representation of code for DrScheme to execute.  How is this source code represented?  By s-expressions.

(define-macro apply-to-sexpr
    (lambda (function sexpr)
        `(,function (quote ,sexpr)))) (apply-to-sexpr first ((1 2) (3 4 5))) => (first '((1 2) (3 4))) => (first (list (list 1 2) (list 3 4))) => (list 1 2)
To do:
  1. Write assert, which takes an expression and prints out a message if it evaluates to false:
    (assert (and (< 1 2) (> 3 4))) should print out: "(and (< 1 2) (> 3 4)) FAILED", but (assert (not (= 1 2))) should return true.  (Read about the function display in the Help Desk.)
  2. Write and-list, which takes a list of expressions and returns true if all of them are true, or false if any of them are false.  It should behave like and, only evaluating an expression if all the ones before it are true.  Don't use the built-in and.
  3. Challenge: design and write a macro define-with-defaults which defines a function which allows default arguments. You should read about case-lambda in the Help Desk (under the section mzscheme).

ports and I/O

In some programs you write you will want to read information from a file. This section introduces the idea of ports (also called streams in some languages). A port can be in one of two states.  It can have data left, or it can be at the end of the file.  Unlike lists, where you can use empty? to find out what type of list you have, you find out what state a port is in by calling e.  If read returns an end-of-file object (which can be tested with the predicate eof-object?), then the file is over. Otherwise, read will return the next data in the file.  Once you call read on a port, the port remembers that you've read from it, and the next call to read will return the next data - this is often called a side-effect.

So what does read actually return?  It returns the next entire s-expression in the file.  This may be a simple symbol or number, or it may be a big list with sublists and symbols and numbers inside of it

#|
The "template" for using an input-port.  
Not really a template in the sense we've been using.
(define (reading-func iport)
  (local [(define next-sexpr (read iport))]
      (cond [(eof-object? next-sexp) ...]
[else (... next-sexpr ... (reading-func iport))])))
|#
Functions to know:
open-input-file : string -> input-port
open-input-string : string -> input-port
read : input-port -> s-expression or eof-object

To do:
  1. Write a function contains-word? which takes in an input-port and a symbol and returns true if the symbol is ever read from the file.  Then try (contains-word? (open-input-file "/usr/dict/words" 'zealous)).
  2. Write list-functions, which returns a list of all the functions defined in a Scheme file (you can use your old homework as test cases).  How do you know when you've defined a function?  You'll read in something like (list 'define (list 'my-function-name 'parameter1 'parameter2) ...)).
  3. Go all out: write scheme-doc, which takes a Scheme file and calculates the names of all functions, all structures, and all constants defined in that file.  Of course, you'll want to keep these lists separate.
  4. Challenge: using the xexpr teachpack from Assignment 5, write out an html page which gives the name of the Scheme file, and then has separate sections for functions, structures, and constants.