Introduction to Scheme Continued¶
Let¶
Sometimes a Scheme function needs to re-do the same calculation, e.g.:
(define operator1
(lambda (e)
(cond
((equal? (car e) '+) 'addition)
((equal? (car e) '-) 'subtraction)
((equal? (car e) '*) 'multiplication)
((equal? (car e) '/) 'division)
(else 'unknown)
)
)
)
The expression (car e)
is repeated 4 times, making the code a little
longer and slower than it needs to be. Better would be to evaluate (car e)
once, save its value, and then use this saved value in place of repeated calls
to (car e)
.
Here’s a neat trick that does that for us:
(define operator2
(lambda (e)
(
(lambda (op)
(cond
((equal? op '+) 'addition)
((equal? op '-) 'subtraction)
((equal? op '*) 'multiplication)
((equal? op '/) 'division)
(else 'unknown)
) ;; cond
) ;; op
(car e) ;; bound to op
)
) ;; e
)
The idea here is to evaluate (car e)
one time and assign its value to
op
. Then we use op
wherever we wrote (car e)
. Notice how we are
taking advantage of the fact that a lambda function call binds the value of
the argument you pass it to op
.
Clearly, this is longer and harder to read than the code in operator1
, and
the distance between op
and the value it gets assigned is pretty big —
in practice one could easily not realize that op
is being assigned (car
e)
.
So what Scheme does is provide some “syntactic sugar” to simplify this trick.
Here is a version that uses a let
expression:
(define operator3
(lambda (e)
(let ((op (car e))
)
(cond
((equal? op '+) 'addition)
((equal? op '-) 'subtraction)
((equal? op '*) 'multiplication)
((equal? op '/) 'division)
(else 'unknown)
)
)
)
)
This is much clearer! Now op
and its assigned value are right beside each
other. Plus the use of the let
keyword signals our intention much more
clearly than using the basic lambda trick.
In general, a Scheme let
expression allows you to define any number of
variables:
(let ((x1 val1)
(x2 val2)
...
(xn valn)
)
body ;; x1 ... xn can be used here
)
The entire expression returns the value of body
, so you can wrap any
expression in a let
if useful.
For example:
> (let ((a 1) (b 1) (c 2)) (+ a b c))
4
There is a subtlety here that is worth exploring a bit. The let
expression
in the example could be re-written like this:
> ((lambda (a b c) (+ a b c)) 1 1 2)
4
Indentation makes the scope clearer:
(
(lambda (a b c)
(+ a b c)
)
1 1 2 ;; a, b, c are not in scope here
)
The scope of the variables a
, b
, and c
is limited to the lambda
function they’re defined in and, in particular, they cannot be used in the
calculation of the parameters passed to it.
So, for example, this expression causes an error:
(
(lambda (a b c)
(+ a b c)
)
1 a 2 ;; error: a is not in scope
)
If you re-write this as an equivalent let
expression, you get this:
(let ((a 1)
(b a) ;; error: can't use a in let value expression!
(c 2)
)
(+ a b c)
)
This is an also an error, because let
does not let you use any of the
variables you are defining within the value expression for any other
variables.
While this limitation makes sense given what let
compiles to, it is
inconvenient in practice. And so Scheme provides let*
to remove this
restriction. This works as expected:
(let* ((a 1)
(b a) ;; okay: this is a let* environment
(c 2)
)
(+ a b c)
)
This let*
could be re-written as embedded let
environments:
(let ((a 1))
(let ((b a)) ;; ok: a is in scope
(let ((c 2))
(+ a b c)
)
)
)
Or even as plain lambdas:
(
(lambda (a)
(
(lambda (b)
(
(lambda (c)
(+ a b c)
)
2 ;; bound to c
)
)
a ;; bound to b
)
)
1 ;; bound to a
)
Here’s an example of a basic let
expression that evaluates numeric
expressions of the form (left op right)
:
(define eval-basic
(lambda (e)
(let ((left (car e))
(op (car (cdr e)))
(right (car (cdr (cdr e))))
)
(cond
((equal? op '+) (+ left right))
((equal? op '-) (- left right))
((equal? op '*) (* left right))
((equal? op '/) (/ left right))
(else 'err)
)
)
)
)
> (eval-basic '(1 + 2))
3
> (eval-basic '(1 - 2))
-1
Another example where a let-environment is useful is in the following
implementation of the deep-map
function. deep-map
is a variation of
map
that works like map
, except the passed-in mapping function is
applied to every non-list value in the list, even those nested inside lists.
For example:
> (deep-map1 1+ '(1 (2 (3) 4 5 (6))))
(2 (3 (4) 5 6 (7)))
Here’s the definition for deep-map1
:
(define deep-map1
(lambda (f lst)
(cond
((null? lst)
())
(else
(let* ((head (car lst))
(body (cdr lst))
(new_body (deep-map1 f body))
)
(cond
((list? head)
(cons (deep-map1 f head) new_body))
(else
(cons (f head) new_body))
)
)
)
)
)
)
Notice that this uses let*
instead of let
to make the definition of
new_body
easier.
An alternative implementation is to embed the conditional into cons
,
e.g.:
(define deep-map2
(lambda (f lst)
(cond
((null? lst)
())
(else
(let* ((head (car lst))
(body (cdr lst))
(new_body (deep-map2 f body))
)
(cons (if (list? head)
(deep-map2 f head)
(f head)
)
new_body
)
)
)
)
)
)
Yet another way to write it would be to use my-fold
from the previous notes. In this
version, there are no repeated computations in the body, so let
/let*
isn’t needed:
(define deep-map3
(lambda (f lst)
(my-fold (lambda (a rest)
(cons (if (list? a)
(deep-map3 f a)
(f a)
)
rest
)
)
'()
lst
)
)
)
In fact, it can be done even more simply just using my-map
from above,
e.g.:
(define deep-map4
(lambda (f lst)
(my-map (lambda (a)
(if (list? a)
(deep-map4 f a)
(f a)
)
)
lst
)
)
)
Again, no let
/let*
is needed here since there are no repeated
expressions.
A Propositional Expression Evaluator¶
One of the applications that Scheme is good at is writing interpreters, compilers, and other programs that process programming languages. As long as we’re willing to stick languages with a Scheme-like syntax, then Scheme is often a great choice for at least prototyping such programs.
Lets write some functions that deal with literal boolean propositional
expressions like (t and (not f))
. The symbols t
and f
stand for
true and false respectively, and there are no variables allowed in the
expressions. We are not using the built-in Scheme boolean values (#t
and
#f
) since this is out own little language that we’re defining.
First, we define a series of functions to test if a value has a particular top-level form:
(define is-true? (lambda (e) (equal? e 't)))
(define is-false? (lambda (e) (equal? e 'f)))
(define is-literal?
(lambda (e)
(or (is-false? e)
(is-true? e))
)
)
;; returns true iff lst is a list of length n
(define nlist?
(lambda (n lst)
(and (list? lst)
(= n (length lst))
)
)
)
;; (not expr)
(define is-not?
(lambda (e)
(and (nlist? 2 e)
(equal? 'not (car e))
)
)
)
;; (p op q)
(define is-bin-op?
(lambda (e op)
(and (nlist? 3 e)
(equal? op (second e))
)
)
)
(define is-and? (lambda (e) (is-bin-op? e 'and))) ;; (p and q)
(define is-or? (lambda (e) (is-bin-op? e 'or))) ;; (p or q)
These are all non-recursive functions that check that the top-level form of an expression. They don’t check if sub-expressions are valid. Also notice how we use lots of little functions to build more complex functions.
Next, lets write a recursive function that tests if an expression is a valid proposition:
;; Returns true iff e is a valid propositional expression.
(define is-expr?
(lambda (e)
(cond
((is-literal? e)
#t)
((is-not? e)
(is-expr? (second e)))
((is-and? e)
(and (is-expr? (first e))
(is-expr? (third e))))
((is-or? e)
(and (is-expr? (first e))
(is-expr? (third e))))
(else
#f)
)
)
)
The idea of this function is to first determine the top-level form of e
,
and then to recursively check that its sub-expressions are also valid.
Next, lets write an evaluator that calculates the value of a valid proposition:
(define eval-prop-bool
(lambda (e)
(cond
((is-literal? e)
(is-true? e))
((is-not? e)
(not (eval-prop-bool (second e))))
((is-and? e)
(and (eval-prop-bool (first e))
(eval-prop-bool (third e))))
((is-or? e)
(or (eval-prop-bool (first e))
(eval-prop-bool (third e))))
(else
#f)
)
)
)
(define eval-prop
(lambda (e)
(if (eval-prop-bool e) 't 'f)
)
)
For example:
> (eval-prop '((not t) and (t or (not f))))
f
We write the -eval-prop
function to be consistent, i.e. in our boolean
literal language t
means true and f
means false, and so we want those
values returned instead of #t
and #f
.
EBNF: Extended Backus-Naur Notation¶
The is-expr?
function is a precise definition of what is, and isn’t, a
valid boolean literal expression. Another way to precisely specify a language
is to use Extended Backus-Naur Formalism, or EBNF
for short. EBNF is a a little language designed for precisely defining the
syntax (not semantics!) of programming languages.
An EBNF “program” consists of a series of named productions, i.e. rules of this general form:
production-name = body .
The idea is that whenever you see production-name
in an EBNF rule, you
can replace that name with the body
of the corresponding rule. A set of
productions is called a grammar, and grammars are very useful for creating
things like lexers and parsers.
We’ll use the Go EBNF notation,
which, among other details given below, ends production rules with a .
.
For example, here is the production that Go uses to define an assignment
operator:
assign_op = [ add_op | mul_op ] "=" .
Anything in “”-marks is indicates a token/symbol that could appear as-is in a
Go program. So in this production, the "="
at the end means a =
character that appears in a Go program, while the =
that appears near the
start is part of the EBNF language that separates a production’s name from its
body.
Two other EBNF operators appear in this production. The |
is the
alternation operator, and indicates a choices between two, or more,
alternatives. So add_op | mul_op
means you can chose add_op
or
mul_up
(but not both). Here’s another example:
add_op = "+" | "-" | "|" | "^" .
This production, named add_op
, says that an add_op
is a "+"
,
"-"
, "|"
, or "^"
. This defines add_op
by simply listing all
the possible alternatives.
The []
operator indicates an optional expression that can occur 0, or
1, times. So the expression [ add_op | mul_op ]
means that the entire
expression can appear, or not appear. If it does occur, then, because of the
|
, either add_op
or mul_op
is chosen.
Another EBNF operator is {}
, which indicates repetition of 0, or
more, times. For example:
identifier = letter { letter | unicode_digit } .
This describes legal identifiers in Go, i.e. the legal names for things like
variables, functions, structs, types, and packages. It says that an identifier
must start with a letter
, and then be followed by 0 or more letters or
digits:
letter = unicode_letter | "_" .
decimal_digit = "0" ... "9" .
In the Go EBNF notation, ...
is
an informal short-hand that is used to indicate an obvious pattern. So the
decimal_digit
production is understood to be equivalent to this:
decimal_digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" .
The identifier
rule lets us check if a sequence of characters is a legal
identifier name. For example, up2
is a legal Go identifier. We can check
this by tracing the identifier
production. The first character, u
, is
a valid letter
, and the next character is a valid letter
. The 2 is a
valid digit, and so the entire identifier is valid.
Now consider 4xyz
, which is not a valid Go identifier. It starts with
4
, but according to the letter
production 4
is not a valid
letter. So we know 4xyz
cannot be an identifier
, because a valid
identifier
must start with a letter.
The unicode_letter
production allows all characters marked as “Letter” by
Unicode. There are thousands of such characters, so they are not listed
explicitly.
Example: EBNF Rules for Literal Propositional Expressions¶
Now lets write an EBNF grammar, using the Go EBNF notation, to describe the valid boolean
literal expressions that (is-expr? e)
returns true for.
Here are the productions:
expr = bool-literal | not-expr | and-expr | or-expr .
bool-literal = "t" | "f" .
not-expr = "(" "not" expr ")" .
and-expr = "(" expr "and" expr ")" .
or-expr = "(" expr "or" expr ")" .
Note that EBNF does not require the productions to appear in any particular order. But, for humans, we usually group related productions, and often put them in the order either from most general to specific, or more specific to general.
We should point out that the syntax of this little language is specifically designed to be Scheme-like, and so every expression is fully-bracketed. This means we don’t need to define the precedence of operators, since the brackets always make that explicit. This makes the parsing much simpler, in fact almost trivial.
Consider the not-expr
production. It is recursive because it expands into
an expression involving expr
, which could expand into not-expr
. This
allows us to show that an expression like “(not (not t))” is valid as
follows:
expr = not-expr
= "(" "not" expr ")"
= "(" "not" not-expr ")"
= "(" "not" "(" "not" expr ")" ")"
= "(" "not" "(" "not" bool-literal ")" ")"
= "(" "not" "(" "not" "t" ")" ")"
Each step expands one part of the expression, and, by making the right choice of rules, eventually we get the exact expression we want. If you want to automate this process, you could write a parser to do this.
Comparing this EBNF grammar to the implementation of is-expr?
, you can
see that it shares some direct similarities. Each of the productions
corresponds to a Scheme function, and those functions essentially mimic
the recursive structure of the productions.
Simplifying a Propositional Expression¶
Some propositional expressions can be simplified into logically equivalent
expressions. For example, an expression of the form (not (not p))
simplifies to the logically equivalent expression p
.
Lets write an expression simplifier that applies this one particular rule to expressions:
;; double negation elimination
;; (not (not expr)) <==> expr
(define simplify
(lambda (e)
(cond
((is-literal? e)
e)
((and (is-not? e) (is-not? (second e))) ;; (not (not e))
(simplify (second (second e))))
((is-not? e)
(list 'not (simplify (second e))))
((is-and? e)
(list (simplify (first e)) 'and (simplify (third e))))
((is-or? e)
(list (simplify (first e)) 'or (simplify (third e))))
(else
(error "invalid expression"))
)
)
)
simplify
first checks if e
is of the form (not (not X))
. If so,
then it extracts the X
(using (second (second e))
) and returns the
simplification of it. Otherwise, it tries to simplify the parts of the sub-
expressions (if any) of e
.
For example:
> (simplify '((not (not t)) and (not (not t))))
(t and t)
Rewriting an Expression with Only nand¶
An interesting and useful fact about propositional logic is that any
expression can be re-written as a logically equivalent expression that uses
onle the nand
operator. The expression (p nand q)
is true just when
either p
, or q
, or both, are false. In other words, (p nand q)
is
equivalent to (not (p and q))
.
Here are the rules for writing the other propositional operators in terms of
nand
:
(not p)
is logically equivalent to(p nand p)
(p and q)
is logically equivalent to((p nand q) nand (p nand q))
(p or q)
is logically equivalent to((p nand p) nand (q nand q))
With these rules we can transform any propositional expression into a
logically equivalent one using only nand
.
Note here that we are allow variables, like p
or q
, in the expressions
we want to simplify. However, we don’t make any assumptions about value these
variables represent.
Here’s some code that does the transformation:
(define is-prop-literal?
(lambda (x)
(symbol? x)
)
)
(define make-nand
(lambda (p q)
(list p 'nand q)
)
)
;; Returns an expression logically equivalent to e that uses
;; "nand" as the only operator.
(define nand-rewrite
(lambda (e)
(cond
((is-prop-literal? e)
e)
((is-not? e)
(let ((np (nand-rewrite (second e)))
)
(make-nand np np)))
((is-and? e)
(let* ((p (nand-rewrite (first e)))
(q (nand-rewrite (third e)))
(pnq (make-nand p q))
)
(make-nand pnq pnq)))
((is-or? e)
(let* ((p (nand-rewrite (first e)))
(q (nand-rewrite (third e)))
(pnp (make-nand p p))
(qnq (make-nand q q))
)
(make-nand pnp qnq)))
)
)
)
> (pp (nand-rewrite '((p and q) or ((not p) or q))))
((((p nand q) nand (p nand q)) nand ((p nand q) nand (p nand q)))
nand
((((p nand p) nand (p nand p)) nand (q nand q))
nand
(((p nand p) nand (p nand p)) nand (q nand q))))
Note the use of the pp
function: it “pretty prints” a Scheme expression,
making it is easier for humans to read.
Example: Rewriting Scheme Code¶
After you’ve written some Scheme code, you may have noticed that these two expressions evaluate to the same thing:
(cond (test val1)
(else val2))
;; same as
(if test val1 val2)
The if-expression is a little simpler and easier to read, so it’s often the
preferred form. But when writing code you don’t always know for sure if you
have only one test, and so it’s wise to use cond
everywhere. After your
program is done you could go back and re-write any single-condition cond
expressions as an equivalent if-expression, but that is tedious and
error-prone.
What we could do instead is write a Scheme function to automatically replace
occurrences of a simple cond
with an equivalent if
.
To do this, we first, we define a few helper functions:
;; returns true iff lst is a list of length n
(define nlist?
(lambda (n lst)
(and (list? lst)
(= n (length lst))
)
)
)
;; returns true iff the first element of lst equals x
(define starts-with
(lambda (x lst)
(equal? x (car lst))
)
)
Next, we’ll write a function that recognizes simple cond
expressions:
;; Returns true for cond expressions of this form:
;;
;; (cond (test val1)
;; (else val2) ;; else could be #f instead
;; )
;;
(define is-simple-cond?
(lambda (lst)
(and (nlist? 3 lst)
(starts-with 'cond lst)
(nlist? 2 (second lst))
(nlist? 2 (third lst))
(member (car (third lst)) '(else #t))
)
)
)
Notice that the last expression of the and
conveniently lets use use
either else
or #t
.
Now, we write a function that re-writes a simple cond
as an equivalent
if
:
;; (cond (test val1)
;; (else val2))
;;
;; same as
;;
;; (if test val1 val2)
;;
;; Pre-condition:
;; (is-simple-cond? lst)
(define rewrite-simple-cond-top-level
(lambda (lst)
(list 'if (car (second lst))
(second (second lst))
(second (third lst)))
)
)
It’s called top-level
because it does not recursively rewrite any sub-
expressions.
Next, we define a general-purpose rewriting function that replaces one expression with another:
(define rewrite
(lambda (is-expr? rewrite-top-level lst)
(cond ((is-expr? lst)
(rewrite is-expr? rewrite-top-level (rewrite-top-level lst)))
((list? lst)
(map (lambda (x) (rewrite is-expr? rewrite-top-level x))
lst))
(else
lst)
)
)
)
Here, is-expr?
is function (such as is-simple-cond?
) that tests if a
value matches an expression, and rewrite-top-level
is a function that,
given an expression that satisfies is-expr?
, returns a new expression.
rewrite
is a deep function, i.e. it recursively rewrites all
sub-expression of the passed-in list.
Finally, we use rewrite
to create a simple-cond-to-if function like this:
(define rewrite-simple-cond
(lambda (x)
(rewrite is-simple-cond? rewrite-simple-cond-top-level x)
)
)
You can use it as follows:
(define test
'(define my-length
(lambda (lst)
(cond
((null? lst)
0)
(else
(+ 1 (my-length (cdr lst))))
)
)
)
)
;;;
> (pp (rewrite-simple-cond test))
(define my-length
(lambda (lst)
(if (null? lst)
0
(+ 1 (my-length (cdr lst))))))
Many kinds of useful code transformations can be implemented in the same way, i.e. by providing a recognizer function, and a top-level rewriting function.
If you write a number of these transformations, you will most likely find that
writing the specific recognizer and re-write functions are usually simple but
tedious. While we won’t go into the details here, it is possible to use
unify
(see below) to implement a function called make-rewriter
that
lets us implement rewrite-simple-cond
like this:
(define rewrite-simple-cond
(make-rewriter '(cond (?test ?val1) ;; input pattern
(else ?val2))
'(if ?test ?val1 ?val2) ;; output patten
)
)
The implementation of make-rewriter
takes a bit of work, but once you have
it then many transformations can be described simply by writing the input
pattern and the output pattern.
Here’s the source code for these two different implementations of these re-writers:
Pattern Matching with Unification¶
If you do a lot of Scheme programming, you soon discover that you need a lot
of code that splits lists up into pieces using functions like car
,
cdr
, cadr
, cddr
, and so on. While this code isn’t hard to write,
it does get pretty tedious and cluttered.
An alternative to writing this sort of code by hand is to use pattern
matching. Here, we will use the simple pattern matching code in the file
unify.scm
. It implements unification
through the use of the function (unify expr1 expr2)
. It is easiest to
understand through some examples.
unify.scm
provides one main function called
unify
that can be called like this:
=> (unify 'a 'b)
#f
This returns #f
(false) because 'a
and 'b
are not the same, i.e.
they don’t unify. However:
=> (unify 'a 'a)
()
'a
and 'a
do unify, because they are the same. (unify 'a 'a)
returns the empty list, '()
, which might seem a bit odd. It will make more
sense after seeing some more examples.
Symbols that begin with a ?
are variables from the point of view of
unify
, and they are can be bound to other expressions. For example:
=> (unify '?x 'a)
((?x . a))
The return value is ((?x . a))
, which is list of pairs (i.e. an
association list), where each pair is a variable and its value. unify
promises that these if you replace the variables in the input expressions with
their corresponding values, then the two expressions will be the same.
In other words, (unify expr1 expr2)
returns a list of variable/value pairs
that, when the variables in expr1
and expr2
are replaced by their
values, the resulting expressions will be the same.
More examples should make this clear:
=> (unify '(?x b c) '(a b c))
((?x . a))
=> (unify '(?x b c) '(a b ?y))
((?y . c) (?x . a))
=> (unify '(?x ?x ?x) '(?y 2 ?y))
((?y . 2) (?x . ?y))
=> (unify '(?left ?op ?right) '(5 + x))
((?right . x) (?op . +) (?left . 5))
=> (unify '(?left ?op ?right) '((2 * a) - (16 / 4)))
((?right 16 / 4) (?op . -) (?left 2 * a))
If there are no assignments of values to the variables in an expression that
will make them equal, then #f
is returned:
=> (unify '(?s 1) '(2 ?s))
#f
=> (unify '(?s ?t) '(?s ?t ?t))
#f
If you try to unify two expressions that are the same and have no variables,
or the variables can be any value, then unify
returns an empty list:
=> (unify '(2 (a b)) '(2 (a b)))
()
=> (unify '(?x ?x) '(?x ?x))
()
The practical value of unification is that it’s often a short and easy way to chop up lists into smaller parts.
For example, suppose we want to check that the top-level of a list is a propositional expression:
(define make-recognizer
(lambda (e1)
(lambda (e2)
(if (unify e1 e2)
#t
#f
)
)
)
)
(define is-and? (make-recognizer '(?x and ?y)))
(define is-or? (make-recognizer '(?x or ?y)))
(define is-not? (make-recognizer '(not ?x)))
The is-and?
, is-or?
, and is-not?
functions are quite are short and
readable.
Example: Binary Search Trees¶
The following is a sample Scheme implementation of a basic binary search tree. It’s provided as an example of straightforward Scheme implementation of a standard program.
;;; bst.scm
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; An implementation of a binary search tree (BST). Nodes are lists of the
;; form (value left-tree right-tree).
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; '() is the empty tree
(define is-empty? (lambda (t) (null? t)))
;; getters
(define value (lambda (node) (first node)))
(define left (lambda (node) (second node)))
(define right (lambda (node) (third node)))
;; insert value x into tree t
(define insert
(lambda (x t)
(if (is-empty? t)
(list x '() '())
(let ((V (value t))
(L (left t))
(R (right t))
)
(cond
((is-empty? t)
(list x '() '()))
((= x V) ;; x already in the tree, so do nothing
t)
((< x V)
(list V (insert x L) R))
(else
(list V L (insert x R)))
)
)
)
)
)
;; convert a list of things into a tree
(define make-tree
(lambda (lst)
(if (null? lst)
'()
(insert (car lst) (make-tree (cdr lst))))))
;; return a list of the values of tree in "inorder" order, i.e. for
;; a BST the list of values will be in ascending sorted order
(define list-inorder
(lambda (t)
(cond
((is-empty? t) '())
(else (append (list-inorder (left t))
(list (value t))
(list-inorder (right t))))
)
)
)
(define tree-sort
(lambda (lst)
(list-inorder (make-tree lst))
)
)
;; Returns the depth of a tree
(define depth
(lambda (t)
(cond
((is-empty? t)
0)
(else
(+ 1 (max (depth (left t)) (depth (right t))))
)
)
)
)
;; return the max value in a tree
(define tree-max
(lambda (t)
(cond
((is-empty? t)
'error)
((is-empty? (right t))
(value t))
(else
(tree-max (right t)))
)
)
)
;; test if x is a value in tree t
(define contains
(lambda (x t)
(cond
((is-empty? t)
#f)
((= x (value t))
#t)
((< x (value t))
(contains x (left t)))
(else
(contains x (right t)))
)
)
)