100% found this document useful (2 votes)
193 views

Programming in Scheme

Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
100% found this document useful (2 votes)
193 views

Programming in Scheme

Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 241

PROGRAMMING IN SCHEME

Springer
New York
Berlin
Heidelberg
Barcelona
Budapest
Hong Kong
London
Milan
Paris
Santa Clara
Singapore
Tokyo
Mark Watson
PROGRAMMING IN SCHEME
LEARN SCHEME THROUGH ARTIFICIAL
INTELLIGENCE PROGRAMS

With 22 Illustrations

Springer
Mark Watson
535 Mar Vista Drive
Solana Beach, CA 92075, USA

Cover illustration: Network of geometric shapes.


Steven HuntfThe Image Bank

Library of Congress Cataloging in Publication Data


Watson, Mark, 1951-
Programming in Scheme: learn Scheme through artificial intelligence
programs / Mark Watson.
p. cm.
Includes bibliographical references and index.
ISBN-13:978-0-387-94681-8 e-ISBN-13:978-1-4612-2394-8
DOl: 10.1007/978-1-4612-2394-8
1. Scheme (Computer program language) 2. Artificial intelligence.
I. Title.
QA76.73.S34W37 1996
005.13'3-dc20 96-10599

Printed on acid-free paper.

© 1996 Springer-Verlag New York, Inc.


All rights reserved. This work may not be translated or copied in whole or in part without
the written permission of the publisher (Springer-Verlag New York, Inc., 175 Fifth Av-
enue, New York, NY 10010, USA), except for brief excerpts in connection with reviews or
scholarly analysis. Use in connection with any form of information storage and retrieval,
electronic adaptation, computer software, or by similar or dissimilar methodology now
known or hereafter developed is forbidden.
The use of general descriptive names, trade names, trademarks, etc., in this publication,
even if the former are not especially identified, is not to be taken as a sign that such
names, as understood by the Trade Marks and Merchandise Marks Act, may accordingly
be used freely by anyone.

Production managed by Frank Ganz; manufacturing supervised by Joe Quatela.


Typeset in TEX from the author's files.

987654321

ISBN-13:978-0-387-94681-8 Springer-Verlag New York Berlin Heidelberg SPIN 10524195


CONTENTS

Preface ix
Acknowledgments Xl

CHAPTER 1 Introduction 1

CHAPTER 2 Tutorial Introduction to Scheme 3


2.1 Lists 3
2.1.1 Evaluating expressions 4
2.1.2 List data structures 4
2.1.3 Built-in functions for lists 7
2.2 Vectors 9
2.3 Logical tests 10
2.3.1 Testing for equality 10
2.3.2 Arithmetic tests 11
2.3.3 Conditional tests 11
2.4 Defining local variables 12
2.5 Defining functions 14
2.5.1 Recursion 17
2.6 Miscellaneous Scheme utility functions 22

CHAPTER 3 Designing for Reuse 25


3.1 Modularity 26
3.2 Cohesion in function libraries 27
3.3 Loose coupling between library modules 27
3.4 Example: a library for genetic algorithms 28
3.4.1 Requirements 29
3.4.2 The design process 29
3.4.3 An implementation 33
3.5 Example application: allocating resources
for sales and marketing 41
3.5.1 Problem description 41
3.5.2 Method for allocating resources 41
3.5.3 Program for allocating resources 41
vi CONTENTS

CHAPTER 4 Writing Portable Scheme Code 45


4.1 Designing for portability 46
4.2 Isolating operating system and graphics code 46
4.3 A portable graphics library 47
4.4 Example library for displaying graph structures 52
4.5 Example: plotting Mandelbrot sets 62
4.6 Example: plotting chaotic population growth 65

CHAPTER 5 An Iterative Approach to Analysis, Design,


and Implementation 71
5.1 Preliminary analysis, design, and implementation
of a network search program 71
5.2 Evaluation of the first implementation
of the network search program 80
5.3 Improving our analysis, design, and implementation 80

CHAPTER 6 Neural Network Library 89


6.1 Requirements for a neural network library 89
6.2 Design of a neural network library 90
6.2.1 Designing a separate library for two-dimensional arrays 92
6.2.2 Algorithm for supervised learning in neural networks 92
6.3 Implementation of a neural network library 94
6.3.1 Two-dimensional array library 95
6.3.2 Neural network library 99
6.4 Example application: character recognition 128

CHAPTER 7 Complex Data Structures 141


7.1 Using Scheme effectively to prototype
and test complex data structures 141
7.2 Example application: natural language processing 142
7.2.1 Analysis 142
7.2.2 Design 143
7.2.3 Implementation 143

CHAPTER 8 Chess Playing Program 151


8.1 Analysis 151
8.2 Design 152
8.3 Implementation 152

CHAPTER 9 Go Playing Program 183


9.1 Requirements and analysis 186
9.1.1 The rules of Go 186
9.1.2 Data structures for maintaining information required
to play Go 188
9.2 Module architecture 190
CONTENTS vII

9.3 Interactively prototyping data structures 192


9.4 Low level Scheme functions
to manipulate Go data structures 198
9.5 Go program design 214
9.6 Go program implementation 216
9.7 Ideas for improving the Go playing program 229

APPENDIX A Installing and Running the MIT Scheme System 231

APPENDIX B More Information Available on the Internet 233

Bibliography 235

Index 237
PREFACE

There is a popular debate over using Scheme or c++ to teach pro-


gramming. At a recent conference a computer science professor
asked me which language was better for learning programming,
Scheme or c++ (Scheme is a modem dialect of the LISP program-
ming language). At the time, I had written only one LISP book
and three c++ books, so he expected that I would favor c++. I had
to admit that I W;1S not sure which was the better language for
teaching programming. For my own use, I like to use Scheme for
experimenting with data structures and exploring new ideas. For
large projects and commercial software development I use C++.
I believe that dynamic languages like Scheme are the most.ef-
ficient languages to use for many programming tasks where we
want to minimize software development time rather than execu-
tion speed. In writing this book I wanted to both provide a concise
guide to learning the Scheme language and to demonstrate the
use of the language to solve interesting, non-trivial problems pre-
sented as complete programs consisting of reusable libraries and
application-specific code. By working through this book, you will
learn the syntax of the Scheme language, control and iteration
techniques how to write portable and reusable Scheme code, how
to use Scheme's dynamic memory allocation to simplify rapid pro-
totyping the application of technologies like genetic programming
and neural networks artificial intelligence techniques like natural
language processing efficient search strategies, how to program a
computer to play chess, how to program a computer to play Go,
and effective use of graphics in Scheme programs.
This is a lot of material to cover in a rather short book, but
most of what you will learn from this book will be the result of
your own experimentation and modification of the example pro-
grams. The second chapter of this book is a tutorial introduction
to the Scheme language. The reader is urged to read the tutorial to
Scheme with the MIT Scheme (or any other Scheme implemen-
tation) running on his or her computer. This tutorial is brief since
Scheme is a simple programming language, but it is important to
read the tutorial carefully, trying the examples as you read the text.
The tutor;al does not attempt to cover every aspect of the Scheme
programming language; rather, it covers only those elements of
the language used in the example programs in this book.
x PREFACE

This book is not an exhaustive reference for the Scheme lan-


guage. Rather, the purpose of this book is to teach the reader to
solve interesting problems using the Scheme language. I place
more emphasis on teaching the reader how to analyze a problem,
design an algorithm for solving the problem, and quickly write a
working prototype program to solve the problem. For most prob-
lems, it is important both to get a prototype program running
quickly and to design the prototype so that it can be modified
when future requirements change and also lend itself to a mod-
ular implementation so that we develop reusable libraries while
solving whatever is today's programming problem.
This book is based on the freely distributable MIT Scheme
system, which requires Microsoft Windows 3.1, Windows 95, or
Windows NT to run. An OS/2 version of MIT Scheme is avail-
able on the Internet (see Appendix B). A copy of a minimal MIT
Scheme development system is provided on the disks included
with this book. You can also obtain new versions of the MIT
Scheme system, as well as an Emacs-like editor and a native mode
compiler, from various Internet FTP sites listed in Appendix B
(e.g., ftp.cs.cmu.edu and ftp.cs.indiana.edu).
I also use and recommend three other public domain Scheme
implementations:
MacGambit Scheme on the Macintosh PC Scheme from Texas
Instruments, now in the public domain SCM Scheme, with
versions for DOS, OS/2, and UNIX
Some of the examples in this book rely on MIT Scheme-specific
functions that are not part of the Scheme language standard: the
chess playing program uses the MIT Scheme sort function, and the
genetic algorithm and neural network examples require a random
number generator.
The source code and executables of MIT Scheme are freely
available on the Internet.

Mark Watson
Solana Beach, California
mwa@netcom.com
MarkWatson@aol.com
ACKNOWLEDGMENTS

I would like to thank my wife, Carol, for her encouragement while


I was writing this book. I would like to thank both Guy Lewis Steel
Jr. and Gerald J. Sussman, who designed and first implemented
the Scheme language. I would like to thank the team at MIT for
writing the MIT Scheme development system, which is included
with this book. I would like to thank my editor, Martin Gilchrist,
both for recommending this project and for his encouragement. I
would like to thank my production editor Frank Ganz and my copy
editor David Kramer. I would like to thank my technical reviewer,
Jonathan Rossie, for many suggested improvements.
CHAPTER 1
INTRODUCTION

I have always believed that computer programmers who only know one
programming language are at a competitive disadvantage. Depending
on the application and audience for a computer program, some lan-
guages are more suitable than others. I have enjoyed programming
in LISP, and have often been disappointed when colleagues whom I
respect seem to have a closed mind with respect to LISP languages.
The Scheme language, a dialect of LISP, is a modern, efficient, dy-
namic language. I hope that this book, in addition to being a good
first programming book, will also convince C, C++, Ada, COBOL, and
FORTRAN programmers that dynamic, list-oriented languages like
Scheme are appropriate for solving a wide range of programming
problems.
In choosing applications for the example programs in this book
I tried to select material that would be reusable in your own pro-
grams and would also show off some of the advantages of the Scheme
language.
I recommend that you read this book while sitting near your com-
puter with a Scheme programming environment running. This is
especially important when working through the Scheme tutorial mate-
rial in Chapter 2. Even though most readers will be using MIT Scheme,
since it is included with this book, the examples that do not use graph-
ics will work with any Scheme system. I use the following notation for
examples in the text:

• I use the prompt character ''>'' to indicate that Scheme is ready to


read an expression that you type
• I show the Scheme system's response to an expression that you type
with ";Value:"

For example (don't worry about the syntax yet):

> (+ 1 2)
;Value: 3
2 1. INTRODUCTION

Here, Scheme prompts you with a ''>'' character, letting you know that
it is ready to read any expression that you type in. The string ";Value:"
indicates the value, or result, of evaluating the last expression that you
typed.
Not all Scheme systems use the same type of prompts. For example,
in MIT Scheme the last example would look like this:
1 J=> (+ 1 2)
;Value: 3
Here the number 1 in "1 1=>" indicates the number of expressions that
the Scheme system has evaluated since starting.
The last example for Texas Instruments PC Scheme would look like
this:
> (+ 1 2) 3
Appendix A contains instructions for installing MIT Scheme. Ap-
pendix B contains references to both World Wide Web and FTP sites
on the Internet where the interested reader can find freely available
Scheme language implementations for most types of computer systems.
CHAPTER 2
TUTORIAL INTRODUCTION
TO SCHEME

Scheme is a fairly simple, extensible language. Scheme is extensible


because functions that you define are essentially indistinguishable from
built-in functions. As we will see in later chapters, with a good design
a large part of writing an application program is developing a library
of reusable components that in effect extend the Scheme language for
your programming projects.
In this chapter we will discuss Scheme data structures and the
built-in functions that manipulate these data structures. The primary
primitive Scheme data structures are lists and vectors. The storage for
all Scheme data structures is created automatically and disposed of au-
tomatically when it is no longer used; this is called dynamic memory
allocation and garbage collection.

2.1 LISTS
A list is an ordered collection of data elements. The fact that the ele-
ments in a list are ordered is important. What makes lists in Scheme
(and other LISP languages) so powerful is that the elements of a list
can be any data structure, including a procedure. List elements can be
numbers, character strings, other lists, and vectors.
Organizing data in your programs using lists is easy and convenient.
Many programmers do not like Scheme's notation (or syntax) for spec-
ifying lists, but I think that you will find it to be a natural notation to
use when you get used to it. Lists are defined by surrounding the list
elements with a pair of parentheses. For example, the following list has
four elements:
'(1 "the dog ran" 3.14159 cat)
4 2. TUTORIAL INTRODUCTION TO SCHEME

The first argument is an integer, the second argument is a character


string, the third argument is a floating point number, and the fourth
argument is an "atomic" symbol, or name. The quote mark in front
of the list prevents the expression from being evaluated. This list is
statically allocated; later, we will use the function list to dynamically
create new lists at runtime.
If you attempt to evaluate the unquoted expression
C1 lithe dog ran")

the Scheme system assumes that the first element in the list is a
procedure, and attempts to execute the non-procedure 1.
The following list is empty:
'0
The following list has only two elements, but the first element is itself
a list containing four elements:
'( C1 2 3 4) lithe dog ran")

We will see that we use lists for just about everything, including
defining variables and functions.

2.1.1. EVALUATING EXPRESSIONS

We will now see how a Scheme language processor evaluates expres-


sions. This would be a very good time to start up the MIT Scheme
environment on your PC. Figure 2.1 shows the Scheme Listener Win-
dow. Instructions for installing and executing the MIT Scheme system
under Microsoft Windows are located in Appendix A.

2.1.2. LIST DATA STRUCTURES

Figure 2.1 shows two lists typed into the Scheme listener window. The
first list uses the define keyword to declare a new variable function
definition. (The define keyword is also used to declare global variables.
We will discuss global and local variables and function definitions in
Sections 2.4 and 2.5.) As we will see in Section 2.5, we will usually use
the special form lambda to define procedures, rather than overloading
define both for the declaration of variables and as a short cut for defining
new procedures. The second list executes the procedure (or function)
factorial, which is defined in the window in Figure 2.1; the second list
element,S, is passed as an argument to function factorial. The syntax
for defining new functions will be covered in Section 2.5; now we will
concentrate on how the Scheme system evaluates lists. When you type
the expression
(factorial 5)
2.1. LISTS 5

~I .'

Scheme saved on Tuesday November 3D, 1993 at 8:47:30 PM


Release 7.3.0 (beta)
Microcode 11.146
Runtime 14.166
Win32 1.0
1 ]-) (+ 1 2)

;Ualue: 3
1 ]-) (define (factorial n)
(if « n 2)
1
(* n (factorial (- n 1»»)

;Ualue: factorial
1 ]-) (factorial 5)
;Ualue: 120
1 ]-) I

FIGURE 2.1. Defining a new function, factorial, by typing a list in the Scheme listener window.

in the Scheme listener window (which I will refer to as either the "lis-
tener window" or the "listener"), the following steps are executed to
evaluate the list:
1. The last argument,S, is evaluated (value is 5)
2. The first argument, factorial, is evaluated (value is a globally defined
function)
3. The function factorial is called with the single argument 5
4. The function factorial calculates a return value of 120. 120 is the
value of the list

Most Scheme implementations evaluate arguments in reverse order


(i.e., last to first). However, the Scheme language standard is careful not
to specify the order of argument evaluations. In the text it is convenient
to discuss a specific order of argument evaluation; however, do not
assume any particular argument evaluation order when you write your
own procedures. In Section 2.5.1 we will discuss recursion as a method
for repetitively executing a sequence of instructions in the functions we
write. For now, we will examine how the evaluation of lists is a recursive
process. We have seen that list elements are evaluated in reverse order,
6 2. TUTORIAL INTRODUCTION TO SCHEME

from the last list element to the first. What happens when a list element
is itself a list? The evaluation process immediately evaluates that list
element and calculates a value for it, then proceeds with the back-to-
front evaluation of the outer list. The following example illustrates the
evaluation of nested lists:
> (+ (* 2 4) (- 40 20»
;Value: 28
This example is easier to follow if we "pretty print" the list structure
(any list or function can be "pretty printed" using the built-in function
pp) by indenting sub-lists:
> (+
(* 2 4)
(- 40 20»
;Value: 28
The Scheme system performs the following steps to evaluate this list:
Evaluate the list (-40 20)
Evaluate the constant 20.
The value is 20
Evaluate the constant 40.
The value is 40
Call the built-in function - passing the two arguments 40 and 20.
The value is 20
Evaluate the list (* 2 4)
Evaluate the constant 4. The value is 4
Evaluate the constant 2. The value is 2
Call the built-in function * passing the two arguments 2 and 4. The
value is 8
Evaluate the list (+ 8 20)
Evaluate the constant 8. The value is 8
Evaluate the constant 20. The value is 20
Call the built-in function + passing the two arguments 8 and 20.
The value is 28

Assuming that you have your Scheme system running, try typing the
following lists into your listener window:
(+ 1 2 3 4)
"this is a string"
(pp '«1 2) «(a b) c) d) e ) f)
'(the dog chased the cat)

Notice that the value of the last list is simply the list itself:
'(the dog chased the cat)
;Value: (the dog chased the cat)
2.1. LISTS 7

The procedure pp is not a standard Scheme function, but it is


included with most Scheme implementations. Procedure pp "pretty
prints" an expression. The character' is called the quote character, and
it prevents the Scheme system from evaluating the expression imme-
diately following it. In our previous examples, the list elements that we
used were

• numbers
• built-in functions like +, -, and·
• a function that we defined (in Figure 2.1): factorial
• a string (list of characters enclosed in double quote marks: "like this")
• lists
All five of these list element types have something in common: they
all have a legitimate value when evaluated. Symbols like dog will, in
general, not have a value and must be quoted by preceding them with
the' character.
If you get an error in evaluating a list, you can restart MIT Scheme by
typing (evaluating) the following expression (which is evaluated, since
it is not quoted):
(restart 1)

2.1.3. BUILT-IN FUNCTIONS FOR LISTS

Scheme provides built-in functions for creating and accessing lists. The
following examples illustrate the built-in functions for creating (or con-
structing) lists. The first thing that we will do is to define three variables
x, y, and z that we will use in our examples:
(define x)
(define y)
(define z)

Note that some Scheme systems (e.g., MacGambit for the Macintosh)
require some initial value in define special forms; for example:
(define x If)
(define y If)
(define z If)

These three variables are not yet assigned any values. As in other
programming languages a variable can be reused by changing its value.
Unlike most other programming languages, Scheme permits a variable
to be assigned a value of any type (e.g., we could set x to the value 3,
then change its value to the string "test string"). The built-in function set!
is used to change the value of a variable. For example:
8 2. TUTORIAL INTROOUCTION TO SCHEME

(set! x 1)
(set! y "test string")
(set! z (list x y)

In this example we are using the built-in function list to create a


new list object dynamically. The function list is passed any number of
arguments (including zero arguments; try this!). A list with no elements
is sometimes called a null list.
For historical reasons the built-in functions to extract the first el-
ement of a list and to extract all but the first element of a list have
peculiar names: car and cdr. These function names are the names of
low-level machine instructions on the first IBM computer used to im-
plement LISP. The built-in function first is identical to car, but we are
stuck with the name cdr. All other built-in function names that we use
in this book have meaningful names. Here is an example of the use of
functions car and cdr:
> z
;Value: C1 "test string")
> (car z)
;Value: 1
> (cdr z)
; Value: "test string"

The built-in procedure (or function) cons (for construct) dynamically


adds a new element to the beginning of a list:
> (cons 'the '(dog ran down the street))
;Value: (the dog ran down the street)
> (cons '(a little) '(dog ran down the street))
;Value: «a little) dog ran down the street)

it is often useful to append the elements of one list to the end of


another. The following example demonstrates the use of the built-in
function append:
> (set! z (append '(1 2 3) '(red orange))
; Value: C1 "test string")
> z
;Value: (1 2 3 red orange)
> (write z)
C1 "test string")
;Value: No value
> (display z)
C1 test string)

Note that the function set! returns as its value the previous value
of the variable that it is changing. In this case, the variable z had the
value (1 "test string") from a previous example. The function write prints
out the values of Scheme variables. The function display is similar to
2.2. VECTORS 9

function write, except that character strings are printed with double
quotation marks.

2.2 VECTORS
As seen in Section 2.1, lists can be used to represent arbitrarily complex
data structures. Vectors are similar to lists, but add an implicit indexing
scheme: vector elements can be accessed by index (starting at zero) in
the vector. Scheme provides many built-in functions for manipulating
vectors. The MIT Scheme Help System documents these built-in func-
tions. The following functions are used in the example programs in this
book.
A vector contains zero or more elements. The built-in function make-
vector is used to make a new vector object, and has one required
argument (the size of the vector) and one optional argument (the initial
value that is assigned to each element of the vector). Try the following
example in your Scheme listener:
> (define v)
;Value: v
(set! v (make-vector 5 'cat»
;No value
> v
;Value: #(cat cat cat cat cat)

The built-in function vector is similar to the built-in function list; call
function vector with zero or more elements, and a new vector object is
created with the argument values copied into the vector. The built-in
function vector-length returns the number of elements in a vector. Try
the following example in your Scheme listener:
> (define v2)
;Value: v2
> (set! v2 (vector 'dog 'cat 3.14159»
;No value
> v2
;Value: #(dog cat 3.14159)
(vector-length v2)
;Value: 3

The built-in function vector-ref is used to extract the value of any


element in a vector. The built-in function vector-set! is used to change
the value of any element of a vector. Try the following example in your
Scheme listener:
10 2. TUTORIAL INTRODUCTION TO SCHEME

> (vector-ref v2 1)
;Value: cat
> (vector-set! v2 1 '(1 2 3»
;No value
> v2
;Value: #(dog (1 2 3) 3.14159)

Like an element of a list, an element of a vector can be an object of


any type (e.g., symbol, number, list, vector, etc.).

2.3 LOGICAL TESTS


Scheme provides many built-in logical predicate functions that evalu-
ate and/or compare one or more arguments and return a true or false
value: #t or #f. Any value that is not #f is a true value. We will look at ex-
amples of the most commonly used predicates in this section, covering
the logical functions used in this book.

2.3.1. TESTING FOR EQUALITY

In Scheme testing for equality is not as easy as you might think. We


have to ask
• Are the values of two expressions the same?
• Are two variables pointing to the exact same data in memory?

In the first case, when we simply want to know whether the values of
two expressions are identical (but perhaps stored in different memory
locations), we use the built-in function equal? If we want to see whether
two expressions refer to the same memory location (i.e., they refer to
the same data), we use the built-in function eq? For example:
> (define x)
;Value: x
> (define y)
;Value:. y
> (set! x '(1 2 3 cat»
;Value: (1 2 3 'cat)
> (set! y '(1 2 3 cat»
;Value: (1 2 3 cat)
> (equal? x y)
;Value: #T
> (eq? x y)
;Value: #F
2.3. LOGICAL TESTS 11

In this example the variables x and y contain references to lists con-


taining the same values, but the lists themselves are different data items.
The built-in procedures and, or, and not operate on true and false values;
for example:
> (define a #t)
;Value: a
> (define b #f)
;Value: b
> (and a b)
;Value: #f
> (or a b)
;Value: #t
> (not b)
;Value: #t

2.3.2. ARITHMETIC TESTS

The following examples show the "greater than" and "less than"
predicates:
> (> 4 1)
;Value: #T
> « 4 1)
;Value: #F
> (>= 3 3)
;Value: #T
> «= 10 2)
;Value: #F

The logical predicate equal? can be used to test numbers for equality:
> (equal? 3 3)
;Value: #T
> (equal? 3 4.45)
;Value: #F

The Boolean value for false, #F, might also print as a null list (i.e., a
list with no elements) on some Scheme systems:
> (equal? 3.14159 3.14)
;Value: 0

2.3.3. CONDITIONAL TESTS

Scheme provides several built-in functions for performing conditional


tests. All examples in this book use the built-in function if. This function
expects three arguments (the third argument can be omitted):
12 2. TUTORIAL INTROOUCTION TO SCHEME

(if
<expression-1>
<expression-2>
<expression-3>

Scheme evaluates <expression-1>. If this value is equal to #F, then


<expression-3> is evaluated, otherwise <expression-2> is evaluated. In
English, this is equivalent to:
If expression 1 is true, then evaluate expression 2, otherwise evaluate
expression 3.
The value of calling function if is the value of expression 2 or that of
expression 3. For example:
> (if (equal? 3 5) 1 2)
;Value: 2

Note that the expression (equal? 3 5) evaluates to #F, so the return


value of function if is 2.
In Section 2.5 we will see how to define our own functions. For now,
assume that we have written functions check-for-user-input, write-output,
and do-idle-work. The following shows how we might use the built-in
function if:
> (if (check-for-user-input)
(write-output) ; write output if user input data
(do-idle-work)); no user input, so do idle work

Note that I typed a single expression in three lines to increase the


readability of the expression; remember that Scheme does not care
about new lines in expressions, rather it counts matching left and right
parenthesis to determine when you have typed in a complete expres-
sion. Any characters following a semicolon are treated as a program
comment and are ignored. It is good programming style to include com-
ments in programs so that other programmers can easily understand
what the program does.

2.4 DEFINING LOCAL VARIABLES


In the examples that we have typed directly into a Scheme listener
window we have used global variables (e.g., x, y, and z). In the example
programs developed in this book (and in the programs that you write!)
we will usually try to avoid using global variables. Global variables are
visible to all functions in the Scheme environment, and it is too easy to
have conflicts if functions use global variables, especially if they have
2.4. DEFINING LOCAL VARIABLES 13

common names. It is very useful to define local variables in functions to


store data temporarily during a computation. Scheme provides several
ways to declare and use local variables; we will use only two methods
in the examples in this book, using the let special form and the built-in
do macro. We will see examples of the let special form in this section,
and the do macro in section 2.6.
The let special form has the following syntax:
(let
((<variable 1> <initial value 1»
«variable 2> <initial value 2»

<expression 1>
<expression 2>

You do not have to define any variables in a let special form. The value
of a let special form is the value of the last expression evaluated in the let
special form. The order of the definition of local variables in a let special
form is undefined. A let special form is a single expression, so it provides
a convenient way to group a list of expressions. The begin special form
can also be used to group a list of expressions in, for example, a call to
the built-in if function:
(if (> count *max-value*)
(begin
(display "Illegal value for count: ")
(display count)
(newline) )
(set! count (+ count 1») ; increment count

The value of a begin special form is the value of the last expression
evaluated in the begin special form.
Let special forms usually define one or more local variables, as seen
in Ptoblem 2.1:

PROBLEM 2.1 What will be the value of the following expression?


(let ((a-list '(the dog chased the cat»
(a-value 3.14»
(display (car (cdr a-list») ; prints "dog"
(newline) ; print a newline, or carriage return character
(set! a-value (* a-value 2»
a-list)
14 2. TUTORIAL INTROOUCTION TO SCHEME

Hint: type this into your Scheme listener window to check your
answer.

Sometimes it is convenient to define variables in a let statement that


calculate their initial values from previously defined variables in the
same let statement. For example, we might want to declare:
(let ((i 1)
(j (+ i 1))) ;; this is illegal!
(display i)
(newline)
(display j)
(newline))

This example is illegal, because the order of variable instantiation


is undefined in let special forms. If you want to logically initialize let
statement variables in order, use the let* statement; for example:
(let* (Ci 1)
(j (+ i 1))) ;; this is legal since 'i' is defined first
(display i)
(newline)
(display j)
(newline) )

2.5 DEFINING FUNCTIONS


So far this tutorial has shown you how to use only the basic tools of the
Scheme language. In this section we will see how to define functions
(or procedures). Usually we will enter functions into a file so that we
can load them into the Scheme environment without retyping them;
for the purposes of this section we will simply enter function defini-
tions in "immediate mode" as expressions for Scheme to evaluate. A
by-product of this evaluation is the definition of the function for use in
the programs that we write.
We have seen the use of the keyword define for declaring global vari-
ables. We can also use this same keyword as a "shortcut" for defining
functions. We introduce the syntax of defining functions by writing
a function called double that takes a single numerical argument and
returns two times this value:
> (define (double x)
(+ x x))
;Value: DOUBLE
> (double 45)
;Value: 90
2.5. DEFINING FUNCTIONS 15

The syntax for defining functions is


(define «function name> <argument 1> <argument 2> ... )
(expression 1)
(expression 2)

A function or a procedure is also data. A longer, but more precise,


method is to use lambda expressions. A lambda expression evaluates to
a procedure. The environment in which a lambda expression is evalu-
ated is called the closing environment. When the procedure returned as
the value of a lambda expression is called, the arguments in the call-
ing expression are bound to the formal parameters of the procedure
and added to the closing environment. The following example shows
function double defined as the returned value of a lambda expression:
> (define double
(lambda (x)
(+ x x)))
;Value: double
You can define functions (i.e., procedures) that have no arguments.
The value returned by a function is the value of the last expression in
the function that is evaluated when the function is executed.
Our example defining function double looks only marginally useful,
since this function is very simple. Most functions that we write use if
tests, looping, or iteration, and calls to other functions. We have al-
ready seen how to use the built-in function if and how to call functions.
Looping is a more interesting problem in Scheme. We will use two
techniques for looping: recursion and the built-in macro do.
You can also define functions inside other function definitions. We
will use this technique in the sample applications in this book. For now,
the following example shows the definition of a local function:
(define (quadruple n) ; global function definition
(define (local-double x) ; definition of a local function
(+ x x)) ; to double a numeric value
(local-double (local-double n))) ; "body" of the global function

This is not a practical example, but it does show the syntax for nesting
function definitions. While this example works, it is very inefficient
because the inner function definition for local-double is executed each
time function quadruple is executed! This inefficiency is easier to see if
we rewrite this example explicitly using lambda expressions:
(define quadruple
(lambda (n)
(define local-double
(lambda (x)
16 2. TUTORIAL INTROOUCTION TO SCHEME

(+ X x)))
(local-double (local-double n))))

The definition of local-double is the returned value of the inner lambda


expression, which is evaluated each time function quadruple is executed.
There is a better way to nest procedure definitions: the letrec special
form. For example:
(define quadruple
(letrec
«local-double (lambda (x) (+ x x)))
(quad (lambda (x) (local-double (local-double x)))))
quad))

It is possible to define Scheme functions that take a variable num-


ber of arguments. Consider the following example that I typed into a
Scheme listener window:
> (define (arg-test.args) a period indicates a function that
(write args) can accept a variable number of arguments
(newline))
;Value: arg-test

> (arg-test)
o
;No value

> (arg-test 1)
(1)
;No value

> (arg-test 'I "see" 1 'cat)


(i "see" 1 cat)
;No value

> (arg-test 1 2 3 4 5 6 7 8 9 10)


(1 2 3 4 5 6 7 8 9 10)
;No value

When a period is used in the argument list definition for a Scheme


function, it indicates that the remaining arguments supplied to a func-
tion are placed in a list, and the argument following the period is
assigned the value of this list.
If you take my advice and explicitly use lambda expressions to define
functions, you can still define functions that take a variable number of
arguments; for example:
> (define arg-test
(lambda args ;; Note: the formal parameter list
is not enclosed in parens
2.5. DEFINING FUNCTIONS 17

(write args)
(newline)))
;Value: arg-test

> (arg-test)
o
;No value

> (arg-test 1)
(1)
;No value

> (arg-test 'I "see" 1 'cat)


(i "see" 1 cat)
;No value

> (arg-test 1 2 3 4 5 6 7 8 9 10)


(1 2 3 4 5 6 7 8 9 10)
;No value

Before, we used lambda expressions in which the formal parameter


list was enclosed in parentheses; here the formal parameter list is a
single variable name. The arguments passed to the defined function
will be bound to the single formal parameter as a list.

2.5.1. RECURSION

In Section 2.5 we defined a simple function, double, which calls the


built-in function + to add a number to itself. What would happen if we
rewrote function double to call itself?

PROBLEM 2.2 Consider the function definition


(define (double x)
(+ x (double x)))

What do you think would happen if you defined this function and
then executed
(double 10)

Hint: you probably do not want to try this in your Scheme


environment.

Clearly something is wrong with the function double defined in Prob-


lem 2.2. Surprisingly, even though this function definition is flawed, it is
frequent;y convenient to use recursion in function definitions. Recursion
is defined as a function calling itself.
We will use recursion for three reasons in the examples in this book:
18 2. TUTORIAL INTROOUCTION TO SCHEME

1. Looping or iterating over a range of numeric values


2. Looping or iterating through the elements in a list
3. Breaking down problems by breaking down complex data struc-
tures and calling a recursive function with components of a data
structure

A recursive function is a function that calls itself at least once.

2.5.1.1. USING RECURSION FOR LOOPING OVER A RANGE OF NUMERIC VALUES


Using recursion for iteration, or looping, seems strange until you write
a few programs yourself that use recursion. We saw a simple example
of nested function definitions in Section 2.5. Here we will use both let
and letrec special forms and recursion of a local function to calculate
the sum of all even numbers in a specified range:
(define sum-even-numbers
(lambda (first-number second-number)
(if « first-number second-number)
(let «work-proc
(letrec
«sum 0)
(check-for-even
(lambda (n)
(if (equal?
(truncate (/ (- n 1) 2))
(truncate (/ n 2)))
o ;; n is an odd number
n))) ;; n is an even number
(local-iterator
(lambda (n)
(set! sum (+ sum (check-for-even n)))
(if « n second-number)
(local-iterator (+ n 1)))))
(calc
(lambda (n)
(local-iterator n)
sum)) )
calc))) ;; return value from letrec
(work-proc first-number))
0)))

The recursion takes place in the local function local-iterator. The vari-
able sum is visible to all three lambda expressions (i.e., procedures or
functions) defined inside the letrec special form. This is a good example
of a Scheme function in that it uses
• Local variables defined in a lef special form that contains a lexically
scoped function definition.
2.5. DEFINING FUNCTIONS 19

• A local iterator function that loops using recursion.


• Defining local functions inside a letrec special form.

For readers with no previous programming experience this complete


example requires detailed explanation. Remember that the built-in
function If usually has three arguments; if the first argument is eval-
uated to be true, then evaluate the second argument and return this
value as the value of the if function call, otherwise evaluate the third
argument and return this value as the value of the if function call. The
purpose of this first "if test" is to insure that the first argument, first-
number, is less than the second argument, second-number. For example
we evaluate
> (sum-even-numbers 1 10)
;Value: 30
Note that 2 + 4 + 6 + 8 + 10 equal 30. We call the function incorrectly:
> (sum-even-numbers 20 10)
;Value: 0
Since the function expects the first argument to be less than the
second, it makes sense for the function to return zero. It is good pro-
gramming practice to check the input values of the functions that
we write to insure that they are appropriate for the definition of the
function.
The letrec special form defines the local variable sum. The function
local-Iterator, which is only visible inside the letrec special form, incre-
ments the value of the local variable sum by the value of its argument
if the argument is an even number. When you use nested let special
forms and function definitions, remember that inner let special forms
and function definitions have access to the variables in the functions
or let special forms that contain them. This is called lexical scoplng. A
variable used in a function is "within scope" if it is defined in a let, let·,
or a letrec special form which contains this function.
The local function check-for-even is a simple utility function that re-
turns the value zero if its argument is not an even number and returns
the value of its argument if the argument is an even number. We use the
built-in function truncate to truncate a floating point value to an integer
value. For example:
> (truncate 3.14159)
;Value: 3
> (truncate 3)
;Value: 3
> (truncate -10.9)
;Value: 10
The letrec special form completely encapsulates four data items:
20 2. TUTORIAL INTRODUCTION TO SCHEME

• Variable sum
• Lambda expression bound to variable check-for-even
• Lambda expression bound to variable local-iterator
• Lambda expression bound to variable calc

The value of the letrec special form is the compiled lambda expression
bound to variable calc.
For readers who have experience with other programming lan-
guages, I hope that you have some critical questions about Scheme
and this example. Specifically, you may think that this program is too
long and cumbersome to do something as simple as loop through a
range of numbers, summing the even ones. You are right! Scheme is
not an optimal language for dealing with numeric data. You will see in
later examples of non-numeric data (e.g., graph structures, search trees,
etc.) that Scheme programs are much shorter than programs written
in languages like CH.
Experienced programmers might also object to having to use recur-
sion for iterating over a range of numbers. Scheme implementations
convert most forms of recursion to iteration in compiled code; the use of
recursion does not cause any performance penalty in compiled Scheme
programs.

2.5.1.2. THE do SPECIAL FORM FOR ITERATION


Almost every Scheme system defines a special form do for looping
and iteration. Usually I prefer to use recursion rather than the do spe-
cial form. The do special form is a macro that expands to code using
recursion.
The do special form is very general; many Scheme implementations
also provide special forms for looping over a range of numbers and for
iterating over the elements in a list. All examples in this book will use
either recursion or the do special form for iteration.
The format of the do special form is
(do «local-variable-l init-value-l step-i)
(local-variable-2 init-value-2 step-2)

)
«test expression for continuing the do special form»
«command expression 1»
«command expression 2»

The do special form is similar to the let special form because it al-
lows you to specify local variables, and the initial values of those local
2.5. DEFINING FUNCTIONS 21

variables. You can optionally define "step" expressions that define how
a local variable will be modified during each iteration.
The iteration proceeds until the test condition is true. A few examples
will illustrate the syntax of the do special form:

> (do ((n 0 (+ n 1») local variable, initial value, step


((> n 5» termination test
(display n) command to execute
(newline) ) command to execute
o
1
2
3
4
5
;Value: #T

Note that the built-in function display prints its argument immedi-
ately, so we see the output

o
1
2
3
4
5

appear before the value of the do special form. The built-in function
newline prints a newline character.
The following example shows how to iterate through the elements
of two lists, printing the values extracted from each list as a new list,
until either list runs out of elements:

> (let ((x '(1 2 3»


(y '(thS cat ran qUickly»)
(do ((x x (cdr x»
(y y (cdr y»)
(or (null? x) (null? y» 'done)
(display (list (car x) (car y»)
(newline) ) )
(1 the)
(2 cat)
(3 ran)
;Value: done
22 2. TUTORIAL INTRODUCTION TO SCHEME

2.6 MISCELLANEOUS SCHEME UTILITY FUNCTIONS


The built-in function reverse requires a list argument. Function reverse
returns as its value a newly constructed list with the order of the original
list elements reversed. The list passed as an argument is not modified.
> (define test-list '(the dog ran down the street))
;Value: test-list

> test-list
;Value 4: (the dog ran down the street)

> (reverse test-list)


;Value 5: (street the down ran dog the)

> test-list
;Value 4: (the dog ran down the street)
The built-in function read is used to read in an expression from
the keyboard. If the expression is a list, the function read inputs all
characters until you type a closing right parenthesis.
> (define x)
;Value: x

> (set! x (read»

(this is a list) I typed this list while (read) waited for input
;No value

> x
;Value: (this is a list)
The function symbol->strlng is useful for converting an internal
Scheme symbol to a character string.
> (symbol->string 'cat)
;Value: "cat"

The function string-length returns the length of a character string. The


function string-ref returns a specific character in a character string. The
function string->number converts a number represented as a character
string into a number.
> (string-length "The cat scratched the dog's nose.")
;Value: 33

> (strinl-ref "The cat scratched the dog's nose." 4)


;Value: # \c
2.6. MISCELLANEOUS SCHEME UTILITY FUNCTIONS 23

> (string->number "123")


;Value: 123
The case statement is used for selecting one statement from many
statements based on a key value. The use of a case statement allows us
to avoid using a series of if statements.
> (define key-value 2)
;Value: key-value

> (case key-value


((0) (display 0) (newline))
((1) (display 1) (newline))
((2) (display 2) (newline))
((3) (display 3) (newline)))
2
;No value
The function char->dlgit converts a character to a number.
> (char->digit # \5)
;Value: 5
The function char-code converts a character to its ASCII code value.
> (char-code # \5)
;Value: 53

> (char-code # \A)


;Value: 65
CHAPTER 3
DESIGNING FOR REUSE

The syntax for writing Scheme programs was discussed in the Scheme
tutorial in chapter 2. We must certainly understand the syntax of a
programming language, but there is more to writing programs than
writing a sequence of instructions to solve a problem. It is expensive
to write software, so it important to write programs so that portions
of the program can be reused in future programs. It is also important
to document properly what the program is supposed to do (analysis
of problem) and the design decisions that were used to implement the
program. These topics will be introduced in chapter 4. We will discuss
modularity and cohesion in this chapter. Modularity refers to separating
functions into separate program modules. These fairly independent
modules can more easily be reused in future programs that we write.
Cohesion refers to placing in a code module (or library) functions that
have similar functionality and that tend to operate on the same type of
data.
We will develop reusable Scheme libraries for genetic algorithms in
this chapter as an example of how to design and implement modular
and cohesive libraries. Genetic algorithms can be used to search effi-
ciently for solutions to difficult problems. We will see that this is a
good starting example: the internal behavior of the genetic algorithm
libraries is complex, but the interface to the library is simple to use. The
interface to a library is a set of one or more functions in the library
that can be called by a program using the library. We will see another
example of designing for modularity and cohesion in chapter 6, when
we design and implement a library for supporting pattern matching
using neural networks.
26 3. DESIGNING FOR REUSE

3. 1 MODULARITY
When I first started programming in high school (in 1967), I learned
FORTRAN, which was an excellent language for numerical calcula-
tions. Unfortunately, the FORTRAN I language had one severe flaw:
programs were written in a single file, often without using separate
functions. In the Scheme tutorial in chapter 2 the functions that we
studied were all very short. Writing short functions is good program-
ming style. A function should have one task to perform. Complex
behavior should be implemented by building a hierarchy of simple
functions: short simple functions that call other short simple functions.
Complex behavior can be hidden inside a code module, with a few
functions visible outside the library. There are thousands of types of
libraries; for example:
Business graphics
Pie charts
Bar graphs
Signal processing
Fast Fourier Transform
Convolution
Communications
Modem control
Ethernet access
The important point is to collect similar functionality in a module
or library for reuse in future programs. As an example, you might need
to write a simple program to read your monthly household expenses
and plot either pie charts or bar charts showing your expenditures by
category. Given a low-level graphics library to draw lines and shapes,
you could place Scheme code in your program to produce the desired
plots. However, later you might need to write a simple program to plot
rainfall for each day in a month. You could "cut and paste" the plotting
code from your first program into your new program, but this would
take time and possibly introduce errors into your new program. It is
much better to plan ahead and place potentially reusable code in a
separate library. All of the functions in a library should be kept in a
single file. Comments at the beginning of this file should indicate the
names of other required libraries.
3.3. LOOSE COUPLING BETWEEN LIBRARY MODULES 27

3.2 COHESION IN FUNCTION LIBRARIES


A library should perform one primary task. Sometimes you may be
tempted to make one library provide several services, but there is a
correct way to do this. For example, you may have a job to write a
business plotting software tool kit with the following functionality:
Business graphics
Pie charts
Bar graphs

While it may be reasonable to write one library that supports both


pie charts and bar graphs, it is usually better to write smaller libraries
that have dependencies on other libraries. In this case we would write
three libraries, the last two being dependent on the first:
1. Basic graphics: draw text, lines, and filled shapes that are scalable
to any drawing area
2. Pie charts: uses the basic graphics library
3. Bar graphs: uses the basic graphics library

Each library or module is placed in a separate source file. It may


seem less convenient to build a large program with many small library
files, but there is a big payback in reducing the development time of
future programming projects.

3.3 LOOSE COUPLING BETWEEN LIBRARY MODULES


In Section 3.2 we saw how a single business-graphics package might
actually be implemented using three libraries. When designing libraries
we want the public interface to be as simple as possible and still provide
access to the functionality hidden in the internal implementation of the
library. Some object-oriented languages, like C++, explicitly allow the
declaration of public and private interfaces. For functional languages
like Scheme there are often many functions in a library that are not
meant to be called from outside the library. There are two good ways
to "hide" these private functions:
1. Use lexical scoping of function definitions
2. Prepend the name of the library to "public" functions, and prepend
"private" to internal functions

We saw an example of lexical scoping of functions in Section 2.5.1.1:


28 3. DESIGNING FOR REUSE

(define sum-even-numbers
(lambda (first-number s~cond-number)
(if « first-number second-number)
(let ((work-proc
(letrec
((sum 0) ..
( check-for~even
(lambda (n) ... )) details omitted
( local-iterator
(lambda (n) ... )) details omitted
( calc
(lambda (n) ... ))) details omitted
caI6))) ;; return value from letrec
(work-proc first-number))
0)))

I have left out the details, but remember that the functions check-
and calc are not defined outside the scope of the
for-even, local-iterator,
outer function definition sum-even-numbers.

3.4 EXAMPLE: A LIBRARY FOR GENETIC ALGORITHMS


The first example library that we wih write in this book supports exper-
iments with genetic algorithms. We will also introduce new Scheme
programming techriiques in this section. Genetic algorithms are in-
spired from the study of genetics. We will use the following terms to
describe genetic algorithms:
• Gene: this is the smallest data item for encoding information. We
will treat genes as single bits of information
• Chromosome: a vector containing genes
• Locus: this is the position, or index, of an individual gene in a
chromosome
• Population: this is a list, or vector, of chromosomes
• Mutation: we will randomly change the value of a single gene at
a randomly chosen locus to change a single chromosome in our
population
• Crossover: we will randomly selett two genes and select a random
locus; the halves of the two chromosomes will be switched at this
locus
• Fitness: a numerical value of an individual chromosome
• Generation: a calculation cycle where we calculate the fitness of
each chromosome in the population, discard those chromosomes
3.4. EXAMPLE: A LIBRARY FOR GENETIC ALGORITHMS 29

with low fitness values, and replace them with new chromo-
somes produced by mutation and crossover of the remaining
chromosomes
• Genetic experiment: a definition of the length of the chromosomes
in the population, the number of chromosomes in the population,
and the data for encoding the chromosomes

In order to perform a genetic algorithm experiment we need to write


a fitness function. A fitness function has one argument, a gene, and
returns a numerical fitness value.

3.4.1. REOUIREMENTS

We want the genetic algorithm library to provide the following services


(Le., supply public functions for these operations):
• Create a new genetic experiment by specifying the number of
genes in each chromosome and the number of chromosomes in
the population
• Update a genetic experiment data structure by specifying the fit-
ness function to use for evaluating individual chromosomes in the
population
• Return the chromosome with the highest fitness value from a
genetic experiment data structure

The interface to this library is relatively simple: three public func-


tions, one for each of the three supported operations. We will see
that complex problems can often be solved with this library simply by
writing a fitness function and calling the three functions in this library.

3.4.2. THE DESIGN PROCESS

From our requirements we know that we will be writing three functions.


For ease of discussion we name them here:
(define create-genetic-experiment
(lambda (chromosome-size population-size a-fitness-function)

(define update-population
(lambda (genetic-experiment-data)
30 3. DESIGNING FOR REUSE

(define get-best-chromosome
(lambda (genetic-experiment-data)

We also know that we will need to design the data structures for
chromosomes and genetic experiments. To hold the bits representing
a chromosome we will use a Scheme vector whose elements will equal
zero or one.
One of the most powerful aspects of interactive languages like
Scheme is that we can define and experiment with new data structures
by using a Scheme listener window. Follow the next example in your
Scheme environment. We start by demonstrating the use of Scheme
functions as data; then we create a vector to hold a chromosome,
manually building a list containing
1. A function pointer
2. The size of each chromosome
3. The number of chromosomes
4. A list of the chromosomes (a list of vectors)

Scheme supports "first class" functions; this means that functions


can be assigned as the value of variables in your program. Try the
following example in your Scheme environment to see how this works:
(define (f1 x) (+ xl))
;Value: f1

(define (f2 x) (+ x 2))


;Value: f2

(define (test a-function a-value) (a-function a-value))


;Value: test

(test f1 10)
;Value: 11

(test f2 10)
;Value: 12

The functions f1 and f2 simply add one and two, respectively to their
arguments. Here, I have used define to directly define a function instead
of using lambda; please remember that the following two definitions are
equivalent:
(define (f1 x)
(+ x 1))
(define f1
3.4. EXAMPLE: A LIBRARY FOR GENETIC ALGORITHMS 31

(lambda (x)
(+ xl)))

The function test has two arguments: a function pointer and a value.
Function test executes its first argument as a function.
We will store a pointer to the fitness function as data in the genetic
experiment data structure. The following listing shows a complete pro-
totype for creating a fitness function and building a genetic experiment
data structure. The fitness function returns as its value the number of
bits in the chromosome set to 1. Note that I enter comments in the
following example as any text on a line after a semicolon. (I like to use
two semicolons to set off comments.) Any text following a semicolon is
a comment that the Scheme system ignores.
Define a variable 'z' and set it to a vector:

> (define z) variable 'z' will refer to a test chromosome


iValue: z

> (set! z (vector 0 1 1 0 1»


iNo value

> z
iValue: #(0 1 1 0 1)

> (vector-length z)
iValue: 5

Define a test fitness function:

> (define (fitness-1 a-chrom)


(let «sum 0)
(vector-len (vector-length a-chrom»)
(do «i 0 (+ i 1»)
«= i vector-len) sum)
(if (equal? (vector-ref a-chrom i) 1)
(set! sum (+ sum 1»»»
iValue: fitness-1

> (fitness-1 z)
iValue: 3

~uild a genetic experiment data structure:

> (define gen-experiment)


iValue: gen-experiment

> (set! gen-experiment


(list j; create a list to hold the data
32 3. DESIGNING FOR REUSE

fitness-1;; function pointer


10 number of genes (bits) per chromosome
3 number of chromosomes in the population
(vector create a vector for population
(make-vector 10 0) ;; Make a vector holding 10 zeros
(make-vector 10 0) ;; Make a vector holding 10 zeros
(make-vector 10 0)))) ;; Make a vector holding 10 zeros
;Value: (#[compound-procedure 10 fitness-1] 10 3
(#(0 0 0 0 0 0 0 0 0 0)
#(0 0 0 0 0 0 0 0 0 0) #(0 0 0 0 0 0 0 0 0 0)))

> (pp gen-experiment)


(#[compound-procedure 10 fitness-1]
10
3
#(#(0 0 0 0 0 0 0 0 0 0) #(0 0 0 0 0 0 0 0 0 0)
#(0 0 0 0 0 0 0 0 0 0)))
;No value

By interactively defining an example of the genetic experiment data


structure, we can experiment with its format until we are satisfied that
the data structure contains the information that we need, and that we
understand how to manipulate the data.
In order to find the best chromosome in a population stored in a
genetic experiment, we need to calculate the fitness value of each chro-
mosome in the population and return the chromosome with the highest
value.
In order to evolve a population of chromosomes, we need to perform
the following calculations:

1. Apply the fitness function to each chromosome


2. Discard the half of the population with the lowest fitness values
3. Replace the discarded chromosomes in the population with new
chromosomes created by mutation and crossover from the chro-
mosomes with high fitness values

In order to discard chromosomes with low fitness values, we sort


the population into descending order, chromosomes with the highest
fitness occurring first in the population vector.
In order to mutate a gene, we will randomly select one gene (a 3ingle
bit) and change its value. Since a single bit can encode only zero or one,
if the gene is equal to zero we will set it to one; if the gene is equal to
one, then we will set it to zero.
We can also create new chromosomes in our population by means
of crossover. Crossover works by selecting two chromosomes and a
3.4. EXAMPLE: A LIBRARY FOR GENETIC ALGORITHMS 33

random allele (gene index). Both chromosomes are split at this allele,
and the chromosomes swap halves at this randomly chosen allele.
The genetic algorithm library is especially simple in that no complex
programming logic is necessary to implement the library; we have only
to create one data structure, iterate through the data structure applying
a function to each chromosome in the population, and to retrieve the
chromosome with the highest numerical fitness value. Other libraries
created in this book will be much more complex algorithmically, so the
design process will include not only understanding the data structures
required for the library, but also a clear description of what needs to be
calculated and how the calculation is performed.

3.4.3. AN IMPLEMENTATION

In Section 3.4.2 we started to design our library for performing genetic


algorithm experiments by interactively defining the data structures that
we would use in the library. In order to get the design data structures
right, it is useful to create sample data in the Scheme listener window
and interactively write short functions to operate on the data. If you
can describe (and understand) the functionality of a software library
and the data structures that will be used in the library and describe
the calculations that need to be performed, then implementing the li-
brary by writing Scheme code is usually relatively simple. The following
Scheme functions can be found in the source file GENETIC.S on the
disks provided with this book.
The first function that we need to define is create-genetic-experiment.
This function creates a genetic experiment data structure like the one
we created interactively in Section 3.4.2.
(define create-genetic-experiment
(lambda (chromosome-size population-size a-fitness-function)
(let ((the-population (make-vector population-size»
(vec #f»
(do ((i 0 (+ i 1»)
((equal? i population-size) the-population)
(set! vec (make-vector chromosome-size 0»
;; We want to set about one third of the genes to
;; the value of one:
(do ((j 0 (+ j 1»)
((equal? j chromosome-size»
(if (> (random 10) 6)
(vector-set! vec j 1»)
;; Store the chromosome vector in the population vector:
(vector-set! the-population i vec»
;; Build the list that is the return value of this function:
(list
a-fitness-function
34 3. DESIGNING fOR REUSE

chromosome-size
population-size
the-population
(make-vector population-size -9999) initial fitness values
)
)
)

In an initial population we set about one third of the genes to the


value one and the rest to zero. The next function that we will write is
update-population:

(define update-population
(let «gene-ref vector-ref)
(gene-set! vector-set!»
(lambda (experiment)
(apply
(lambda (fitness-function
chromosome-size
population-size
population
fitness-values)
(let «chromosome-ref (lambda (c) (vector-ref population c»)
(chromosome-set! (lambda (c value)
(vector-set!
population
c
value»))
(fitness-ref (lambda (f) (vector-ref fitness-values f))
(fitness-set! (lambda (f value)
(vector-set!
fitness-values
f
value»»

(letrec
«set-fitness-values
Iterate through all chromosomes in the population
updating the fitness value in the fitness value
vector for each chromosome.

NOTE: we make this a local function, so we can reset


the fitness values before returning from the
global function 'update-population:

(lambda 0
(do «c 0 (+ c 1»)
«equal? c population-size»
3.4. EXAMPLE: A LIBRARY FOR GENETIC ALGORITHMS 35

(vector-set!
fitness-values
c
(fitness-function (vector-ref population c) c»»)

(bubble-sort
(lambda 0
We need to sort the fitness-values vector in descending
order. As we move fitness values in the fitness-value
vector, we move the corresponding chromosome in the
population vector.

We will want to also sort the population in order


of decreasing fitness value at the end of this function
so we will make the "bubble sort" into a local function
which we will use twice:

(do ((i 0 (+ i 1»)


((equal? i population-size»
(do ((j (- population-size 2) (- j 1»)
((< j i»
(if «
(vector-ref fitness-values j)
(vector-ref fitness-values (+ j 1»)
(let ((x (vector-ref fitness-values j»
Note: we need to make a copy of one
of the chromosomes that we are
going to modify:
(c (vector-ref population j»)
Set the values of the fitness value and
;; chromosome at index equal to 'j' to the
;; values at index equal to 'j + 1':
(vector-set!
fitness-values
j
(vector-ref fitness-values (+ j 1»)
(vector-set!
population
j
(vector-ref population (+ j 1»)

Set the values of the fitness value and


chromosome at index equal to 'j + l' to the
values at index equal to 'j':
(vector-set!
fitness-values
(+ j 1)
x)
(vector-set!
36 3. DESIGNING FOR REUSE

population
(+ j 1)
c))))))) best chromosomes now are

(cross-over
;; Local function to do a "cross over" of genetic material:
(lambda (inqex-1 index-2)
(if DEBUG
(begin
(display "crossover at indicies: ")
(display index-1)
(display ", ")
(display index-2)
(display": cross over allele is ")))

(let «allele (+ 2 (random (- chromosome-size 2))))


(chrom-1 (vector-copy (vector-ref population index-1)))
(chrom-2 (vector-ref population index-2)))
(if DEBUG
(begin
(display allele)
(newline)))
(do «i 0 (+ i 1)))
«equal? i allele))
(let «gene-temp (vector-ref chrom-1 i)))
(vector-set! chrom-1 i (vector-ref chrom-2 i))
(vector-set! chrom-2 i gene-temp)))))))

Use the local function to initialize the fitness values:


(set-fitness-values)

;; at the lowest indices


(bubble-sort)

Perform a few randow cross over gene swaps:


Note: we will leave the best chromosome at
index 0 unchanged.

(do «i 0 (+ i 1)))
«equal? i (truncate (/ population-size 3))))
(let «index-1
(+ 1 (random (- population-size 1))))
(index-2
(+ 1 (random (- population-size 1)))))
(cross-over index-1 index-2)))

;; Perform a few random gene mutations:

(let «num-mutations
(truncate
3.4. EXAMPLE: A LIBRARY FOR GENETIC ALGORITHMS 37

(/ (* population-size chromosome-size) 30»»


(do ((i 0 (+ i 1»)
((equal? i num-mutations»
(let ((chrom-index ;; skip best chrom at index 0
(+ 1 (random (- population-size 1»»
(gene (random chromosome-size»)
(let ((chromosome
(vector-ref population chrom-index»)
(if DEBUG
(begin
(display "mutating chromosome at index")
(display chrom-index)
(display ", value: ")
(display chromosome)
(display " at gene index ")
(display gene)
(newline» )
(vector-set!
chromosome
gene
(- 1 (vector-ref chromosome gene»»»)

We will now copy the best half of the chromosome


population into the worst half:
(let ((num-copy (truncate (/ population-size 2»)
(best-index 0)
(worst-index (- population-size 1»)
(do ((j 0 (+ j 1»)
((equal? j num-copy»
(vector-set!
fitness-values
worst-index
(vector-ref fitness-values best-index»
(vector-set!
population
worst-index
(vector-copy (vector-ref population best-index»)
(set! best-index (+ best-index 1»
(set! worst-index (- worst-index 1»»

;; Update all of the fitness values at once after doing


;; the cross over and mutation operations:
(set-fitness-values)

we bubble sort the chromosomes a second time


so that the application using this library
can inspect the chromosomes in order. It would
however be more efficient not to sort a second time.
(bubble-sort»»
experiment»»
38 3. DESIGNING FOR REUSE

The next function that we define is a utility function to return a list


containing the best chromosome and its fitness value:
Utility to return a list containing both the best
chromosome its fitness value:

NOTE: usually, the best chromosome is at index 0 in


the population. The following function searches
all chromosomes in case an application using this
library creates a genetic experiment, and calls
this function without first calling 'update-population'.

(define best-chromosome-and-fitness
(lambda (experiment)
(let «fitness-function (car experiment))
(chromosome-size (cadr experiment))
(population-size (caddr experiment))
(population (cadddr experiment))
(fitness-values (cadddr (cdr experiment)))
(best-chromosome)
(best-fitness-value)
(best-index))
;; Iterate through all chromosomes in the population
;; finding the chromosome with the best fitness value:
(set! best-index 0)
(set!
best-fitness-value
(vector-ref fitness-values best-index))
;; start at the second chromosome (index 1):
(do «c 1 (+ c 1)))
«equal? c population-size))
(if (>
(vector-ref fitness-values c)
best-fitness-value)
(begin
(set! best-index c)
(set!
best-fitness-value
(vector-ref fitness-values c)))))
(set! best-chromosome (vector-ref population best-index))
(list
best-chromosome
(fitness-function best-chromosome best-index)))))

The following two functions are used to test this library. The first
function is a fitness function that returns as its value the number of
genes in a chromosome that are set to a non-zero value. The second
function simply creates a new genetic experiment data structure and
iterates through several new generations.
3.4. EXAMPLE: A LIBRARY FOR GENETIC ALGORITHMS 39

TEST FUNCTIONS:

Define a test fitness function:

(define fitness-1
(lambda (a-chrom chromosome-index)
(let ((sum 0)
(vector-len (vector-length a-chrom»)
(do ((i 0 (+ i 1»)
((= i vector-len) sum)
(if (equal? (vector-ref a-chrom i) 1)
(set! sum (+ sum 1»»»)

Test function

(define (test)
(let ((ge ;; experiment with 10 chromosomes, each with 6 genes
(create-genetic-experiment 6 10 fitness-1»)
(do ((i 0 (+ i 1»)
((equal? i 20» ;; 20 "generations"
(update-population ge)
(let ((temp (best-chromosome-and-fitness ge»)
(newline)
(display "best fitness: ")
(display (cadr temp»
(display ", and best chromosome: ")
(display (car temp»
(newline)
(display ge)
(newline»»)

Evaluate the following to test the system:

(test)

The following listing shows the output generated from running the
function test in a Scheme listener window:
(test)
crossover at indides: 1, 3: cross over allele is 3
crossover at indides: 2, 3: cross over allele is 3
crossover at indides: 9, 4: cross over allele is 4
mutating chromosome at index5, value: #(0 0 1 0 0 1) at gene index 1
mutating chromosome at index6, value: #0 0 0 1 0 0) at gene index 5

best fitness: 4, and best chromosome: #(0 1 1 1 0 1)


(#[compound-procedure 7 fitness-1] 6 10 #(#(0 1 1 1 0 1) #(0 1 1 1 0 1)
40 3. DESIGNING FOR REUSE

#(0 1 1 1 0 1) #(0 1 1 0 0 1) #(1 0 0 1 0 1) #(1 1 0 0 0 0)


#(0 0 1 0 0 1) #(0 0 1 1 0 0) #(0 0 1 0 0 1) #(1 1 0 0 0 0))
#(4 4 4 3 3 2 2 2 2 2))
crossover at indicies: 1, 1: cross over allele is 2
crossover at indicies: 8, 1: cross over allele is 3
crossover at indicies: 5, 2: cross over allele is 2
mutating chromosome at index1, value: #(0 1 1 1 0 1) at gene index 5
mutating chromosome at index8, value: #(0 1 1 1 0 1) at gene index 3

best fitness: 4, and best chromosome: #(0 1 1 1 0 1)


(#[compound-procedure 7 fitness-1] 6 10 #(#(0 1 1 1 0 1) #(1 0 1 1 0 1)
#(0 1 1 1 0 1) #(0 1 1 1 0 1) #(0 1 1 1 0 0) #(0 1 1 0 0 1)
#(1 0 0 1 0 1) #(1 0 0 1 0 1) #(0 1 1 0 0 1) #(0 1 1 0 0 1))
#(4 4 4 4 3 3 3 3 3 3))
crossover at indicies: 2, 8: cross over allele is 4
crossover at indicies: 1, 6: cross over allele is 3
crossover at indicies: 6, 3: cross over allele is 4
mutating chromosome at index6, value: #(1 0 1 1 0 1) at gene index 4
mutating chromosome at index9, value: #(0 1 1 1 0 1) at gene index 2

best fitness: 5, and best chromosome: #(1 0 1 1 1 1)


(#[compound-procedure 7 fitness-1] 6 10 #(#(1 0 1 1 1 1) #(0 1 1 1 0 1)
#(1 0 1 1 0 1) #(0 1 1 1 0 1) #(1 0 1 1 0 1) #(0 1 1 1 0 1)
#(0 1 1 1 0 1) #(0 1 1 1 0 0) #(0 1 1 1 0 0) #(0 1 0 1 0 1))
#(5 4 4 4 4 4 4 3 3 3))
crossover at indicies: 9, 1: cross over allele is 4
crossover at indicies: 8, 6: cross over allele is 5
crossover at indicies: 3, 6: cross over allele is 4
mutating chromosome at index8, value: #(0 1 1 1 0 1) at gene index 3
mutating chromosome at index9, value: #(1 0 1 1 1 1) at gene index 4

best fitness: 5, and best chromosome: #(1 0 1 1 1 1)


(#[compound-procedure 7 fitness-1] 6 10 #(#(1 0 1 1 1 1) #(1 0 1 1 0 1)
#(1 0 1 1 0 1) #(0 1 1 1 0 1) #(1 0 1 1 0 1) #(1 0 1 1 0 1)
#(0 1 1 1 0 1) #(1 0 1 1 0 1) #(1 0 1 1 0 1) #(0 1 1 0 0 1))
#(5 4 4 4 4 4 4 4 4 3))
crossover at indicies: 9, 9: cross over allele is 3
crossover at indicies: 6, 9: cross over allele is 5
crossover at indicies: 3, 6: cross over allele is 2
mutating chromosome at index8, value: #(1 0 1 1 0 1) at gene index 4
mutating chromosome at index5, value: #(1 0 1 1 0 1) at gene index 0

best fitness: 5, and best chromosome: #(1 0 1 1 1 1)


(#[compound-procedure 7 fitness-1] 6 10 #(#(1 0 1 1 1 1) #(1 0 1 1 1 1)
#(1 0 1 1 0 1) #(1 0 1 1 0 1) #(0 1 1 1 0 1) #(1 0 1 1 0 1)
#(0 1 1 1 0 1) #(1 0 1 1 0 1) #(0 1 1 1 0 1) #(0 0 1 1 0 1))
#(5 5 4 4 4 4 4 4 4 3))
;Value: #t
3.5. EXAMPLE APPLICATION: ALLOCATING RESOURCES FOR SALES ANO MARKETING 41

The fitness function used in this simple example returns a value


that is the count of the number of genes in a chromosome that have
the value 1. This example run only calculated five new generations of
the original population. This program usually needs to run for ten to
twenty generations before it finds an "optimal" population, that is, a
chromosome with all genes set to the value 1.

3.5 EXAMPLE APPLICATION: ALLOCATING RESOURCES FOR SALES


AND MARKETING
The test function for the genetic algorithm library in Section 3.4.3 was
very simple. In this section we will demonstrate the use of the genetic
algorithm library on a non-trivial problem.

3.5.1. PROBLEM OESCRIPTION

Company X has ten sales-and-marketing staff, each independently cov-


ering ten sales regions. We want to write a program that dynamically
chooses, for each salesperson, what fraction of their time should be
spent on the following activities:
1. Direct marketing phone calls
2. Personal visits to customer sites
3. Writing personal sales letters and mailing brochures to potential
clients

3.5.2. METHOO FOR ALLOCATING RESOURCES

Each month we will use a genetic algorithm to adjust the activities of


each salesperson. The fitness function for each salesperson will be the
sales performance for the last month divided by the total sales perfor-
mance of all the sales staff; this will at least partially eliminate seasonal
variation in sales productivity.

3.5.3. PROGRAM FOR ALLOCATING RESOURCES

The following short listing shows the Scheme implementation of a sales


resource scheduler for Company X:

Description: This file contains the example program


described in Section 3.5. We want to
set the sales/marketing strategy for a
42 3. DESIGNING FOR REUSE

company with several sales staff. In


particular, we want to adjust the fraction
of time each sales person spends on the
following activities:

1. Telephone sales calls


2. Personal sales calls to customer sites
3. Preparation and mailing of sales letters

The fitness function for each sales person


will simply be the dollar amount of sales
in the current period, normalized by the
sales of the other sales staff (this will
help reduce seasonal effects on evaluating
sales productivity).

Copyright 1995, Mark Watson.

This software may be used without restriction in compiled


form. All source code'rights reserved.

We will use a global variable to hold the current sales


productions. Usually we avoid global variables, but it
does make it easier for our genetic algorithm fitness
function to access sales figures.

(define Company-X-sales)

Define global genetic algorithm data object. We want


to encode three weighting factors in each chromosome; we
will use three genes per weighting factor (range 0 through 7),
requiring 9 genes total per chromosome:

(define ge)

; Define the average sales productivity:

(define average-sales 0.0)

Define the fitness function:

(define (fitness-1 a-chrom chromosome-index)


(- (vector-ref Company-X-sales chromosome-index) average-sales))

(define (make-sales--data number-of-sales-staff)


(set!
ge
(create-genetic-experiment
9 number-of-sales-staff fitness-1))
3.5. EXAMPLE APPLICATION: ALLOCATING RESOURCES FOR SALES AND MARKETING 43

(set! Company-X-sales
(make-vector number-of-sales-staff 0.0)))

(define (update-sales sales-list)


(if (vector? sales-list)
(set! Company-X-sales (vector-copy sales-list))
(set! Company-X-sales (list->vector sales-list))))

(define (update-sales-strategy new-sales-list)


(update-sales new-sales-list)
(set! average-sales 0.0)
(do ((i 0 (+ i 1)))
((> i (- (list-ref ge 2)))) ;; (list-ref ge 2) == # sales people
(set! average-sales
(+ average-sales (list-ref new-sales-list i))))
(set! average-sales (/ average-sales (list-ref ge 2)))
(update-population ge)
;; display best chromosome:
(display (best-chromosome-and-fitness ge)))

;; Test code:

(make-sales-data 10) ;; 10 sales staff


(update-sales-strategy '(100 101 120 90 120 99 78 102 121 105))
The following listing shows how to run this example program, and
the sample output:
(load "genetic.s")
;Loading "genetic.s" -- done
;Value: test

(load "gen_exam.s")
;Loading "gen_exam.s"
crossover at indicies: 9, 5: cross over allele is 6
crossover at indicies: 1, 1: cross over allele is 2
crossover at indicies: 8, 1: cross over allele is 3
mutating chromosome at index5, value: #(0 0 0 0 1 0 0 0 1) at gene index 1
mutating chromosome at index9, value: #(0 0 0 0 1 0 0 1 0) at gene index 1
mutating chromosome at index5, value: #(0 1 0 0 1 0 0 0 1) at gene index 0
(#(0 0 1 1 0 0 0 0 0) 100.) -- done
;No value

In actual use, the function update-sales-strategy would be called once


per month with the sales productivity date for each salesperson; for
example:
(update-sales-strategy '(100 101 120 90 120 99 78 102 121 105))

You would use the chromosomes with the highest fitness values to
specify the sales strategies used for the following month.
CHAPTER 4
WRITING PORTABLE
SCHEME CODE

We never know when we first write a program what the futures uses
for the program will be. Developing software costs money, so there are
strong economic reasons to make software reusable. Software has a
greater probability of reuse if it is written to be (relatively) portable
on different types of computers using different compilers and run-time
environments. Not all Scheme systems are completely compatible. For
example, the following Scheme statement is accepted by only some
Scheme systems:

(define X)

Here, we did not assign an initial value to the variable X. To be safe,


specify an initial value, even if you know that your program will reset
the value of variable X. For example:

(define X If)

Some Scheme systems allow you not to specify initial values for
variables in let and let* statements; for example:

(let «x 1)
(y 2)
(z))
(set! z 3)
(display (+ x y z))

Some Scheme systems will not accept this statement because the
variable z has not been assigned an initial value in the let statement.
46 4. WRITING PORTABLE SCHEME CODE

4. 1 DESIGNING FOR PORTABILITY


We have already discussed the benefits of properly designing the File
Module Architecture of the programs we write. In Chapter 3 we saw that
it is important to segregate Scheme functions in separate file modules
based on functionality.
It is also important in some computer programming languages to
design data structures for portability. In some low level languages like
C, and to some extent C++, variable types (e.g., integers) may require
different amounts of storage on different types of computers. Fortu-
nately, dynamic languages like Scheme abstract away any concerns for
physical storage requirements of data.

4.2 ISOLATING OPERATING SYSTEM AND GRAPHICS CODE


We can dramatically increase the portability of our programs if we write
separate library file modules for performing operating-system-specific
calculations (like writing to a disk file, or getting the current time of
day) and graphics operations. Since Scheme is a high-level, dynamic
language. we can usually insure the portability of our programs by
simply writing our own versions of operating-system-specific functions.
For example, there may be a non-standard function $system-get-date-
and-time that is available in our Scheme environment and that we want
to use in our program. We will write our own function, which calls
the system function. For example, we can write an operating-system-
specific library as seen in Listing 4.1.

Listing 4.1

File: OS_FUNeS.S

Description: This file contains "wrapper" functions for operating


system specific calls.

(define (get-time)
(cadr ($system-get-date-and-time))

Our programs will always call our function get-time instead of the
non-portable, system-specific call $system-get-date-and-time.
We will design and implement a portable graphics library in Section
4.3.
4.3. A PORTABLE GRAPHICS LIBRARY 47

4.3 A PORTABLE GRAPHICS LIBRARY


We will design a portable graphics library in this section. We will also
implement this library for MIT Scheme and for Macintosh Gambit
Scheme. When we are writing a new program, we usually do not have
time to completely implement libraries to meet the requirements of fu-
ture programs that we will write. A good strategy is to write down the
requirements for the library for the current programming project, and
plan to extend the capabilities of the library in the future when it is
reused for new programming projects.
The purpose of the portable graphics library implemented in this
section is to support the example programs in this book. We can start
by writing down the types of graphics-related operations that we will
need:
• Open a new graphics window
• Close the graphics window
• Erase, or clear, the contents of the graphics window
• Plot a straight line between two points
• Plot a character string at a specified location in the graphics window
• Fill a rectangular area with a specified color
• Fill a rectangular area with a specified gray-scale value
• Change the current pen width for drawing lines
These simple graphics operations are sufficient to support all of
the example programs in this book. If any additional operations are
required, they are simply added to the library as needed.

Listing 4.2
File: graph. s

Description: This file module contains a library for


plotting graph data structures.

Public interface functions:

(open-gr) - initializes a new graphics window

(close-gr) - closes a graphics window

(clear-plot) - clears the graphics area

(plot-line xl yl x2 y2)

(plot-string x y str)
48 4. WRITING PORTABLE SCHEME COOE

(plot-fill-rect X Y xsize ysize color-string)

(plot-fill-rect-gray-scale x y xsize ysize int255)

(plot-ellipse left top right bottom color-string)

(pen-width width)

Copyright 1995, Mark Watson. All source code rights


reserved: you may not redistribute this source file.
No binary rights reserved: you may use this software
in compiled form without restriction.

(define g-c #f)

(define open-gr
(lambda 0
(if (null? g-c)
(begin
(set! g-c (make-graphics-device #f»
(graphics-set-coordinate-limits g-c 0 0 1024 1024»
(begin
(clear-plot»»)

(define close-gr
(lambda 0
(clear-plot»)

(define clear-plot
(lambda 0
(graphics-clear g-c»)

(define plot-line
(lambda (xl yl x2 y2)
(graphics-draw-line g-c xl yl x2 y2»)

(define plot-string
(lambda (x y str)
(graphics-draw-text
g-c
(inexact->exact (floor x»
(inexact->exact (floor y»
str»)

(define plot-ellipse
(lambda (left top right bottom color-string)
(graphics-operation g-c 'set-foreground-color color-string)
4.3. A PORTABLE GRAPHICS LIBRARY 49

(graphics-operation g-c 'draw-ellipse left top right bottom)


(graphics-operation g-c 'set-foreground-color "black")))

(define graph_temp_vec (vector 0 0 1 1 2 2 3 3 4 4))

(define plot-fill-rect
(lambda (x y xsize ysize color-string)
(graphics-operation g-c 'set-foreground-color color-string)
(let ((x2 (+ x xsize))
(y2 (+ Y ysize)))
(vector-set! graph_temp_vec 0 x)
(vector-set! graph_temp_vec 1 y)
(vector-set! graph_temp_vec 2 x)
(vector-set! graph_temp_vec 3 y2)
(vector-set! graph_temp_vec 4 x2)
(vector-set! graph_temp_vec 5 y2)
(vector-set! graph_temp_vec 6 x2)
(vector-set! graph_temp_vec 7 y)
(vector-set! graph~temp_vec 8 x)
(vector-set! graph_temp_vec 9 y)
(graphics-operation g-c 'fill-polygon graph_temp_vec))
(graphics-operation g-c 'set-foreground-color "black")))

(define plot-fill-rect-gray-scale
(lambda (x y xsize ysize int255)
(let ((color (inexact->exact (floor int255))))
(vector-set! graph_temp_vec_3 0 color)
(vector-set! graph_temp_vec_3 1 color)
(vector-set! graph_temp_vec_3 2 color)
(plot-fill-rect
(inexact->exact x)
(inexact->exact y) xsize ysize graph_temp_vec_3))))

(define pen-width
(lambda (w)
(graphics-operation g-c 'set-line-width w)
1)

(define test
(lambda 0
(plot-fill-rect 200 100 300 300 "blue")
(let ((xx '(0 25 50 75 100 125 150 175 200 225 250)))
(do ((x xx (cdr x)))
((null? x))
(plot-fill-rect-gray-scale
(car x) (car x) 25 25 (car x))))
50 4. WRITING PORTABLE SCHEME CODE

(plot-line 500 400 300 600)


(plot-ellipse 50 200 100 120 "red")
(plot-string 50 800
"This is a test of the Scheme Graphics Portability Library")))

(open-gr)
(test)
(close-gr)

Listing 4.3 shows an implementation of the portable graphics library


for the Macintosh MacGambit Scheme system.

Listing 4.3
File: MAC GRAF. S

Description: This file contains an implementation of


the protable Scheme graphics library for
for the Macintosh MacGambit Scheme system.

(define local-window #f)

(define Y-MAX-VALUE 250)

Thla la a teal ollhe Seheme Graphlea or1ablllty LIbrary

FIGURE 4.1. Example output in a portable Scheme graphics window.


4.3. A PORTABLE GRAPHICS LIBRARY 51

(define PLOT-SCALE 4)

(define (open-gr)
(if (equal? local-window #f)
(set! local-window
(mac#newwindow
(mac#rect 40 10 (+ Y-MAX-VALUE 100) 300)
"Portable Scheme Graphics"
#t visible
4 ; nogrowdoc
-1 ; in front of all windows
#t» ; goawaybox
(clear-plot»)

(define (clear-plot)
(mac#eraserect local-window (mac#rect 0 0 512 512»)

(define (set-color color-string)


#f) ;; color support is (apparently) not available

(define (plot-line xl yl x2 y2)


(set! xl (/ xl PLOT-SCALE»
(set! yl (/ yl PLOT-SCALE»
(set! x2 (/ x2 PLOT-SCALE»
(set! y2 (/ y2 PLOT-SCALE»
(mac#moveto local-window xl (- Y-MAX-VALUE yl»
(mac#lineto local-window x2 (- Y-MAX-VALUE y2»)

(define (plot-string x y str)


(set! x (/ x PLOT-SCALE»
(set! y (/ y PLOT-SCALE»
(mac#moveto local-window x (- Y-MAX-VALUE y»
(mac#drawstring local-window str»

(define (plot-fill-rect x y xsize ysize color-string)


(set! x (/ x PLOT-SCALE»
(set! y (/ y PLOT-SCALE»
(set! xsize (/ xsize 2»
(set! ysize (/ ysize 2»
(set-color color-string)
(mac#paintrect
local-window
(mac#rect (- Y-MAX-VALUE y) x
(+ (- Y-MAX-VALUE y) ysize) (+ x xsize»»

(define (plot-ellipse left top right bottom color-string)


(set! top (/ top PLOT-SCALE»
(set! left (/ left PLOT-SCALE»
(set! bottom (/ bottom PLOT-SCALE»
52 4. WRITING PORTABLE SCHEME COOE

(set! right (/ right PLOT-SCALE))


(set-color color-string)
(mac#paintoval
local-window
(mac#rect (- Y-MAX-VALUE top) left (- Y-MAX-VALUE bottom) right)))

(define (pen-width width)


(mac#pensize local-window width width))

;; test function

(define (test)
(open-gr)
(plot-string 50 200 "Test of Portable Scheme graphics for MacGambit")
(plot-line 10 10 100 200)
(plot-fill-rect 50 100 330 220 "red")
(pen-width 10)
(plot-line 200 200 190 190)
(pen-width 1)
(plot-ellipse 150 150 200 100 "blue"))

4.4 EXAMPLE LIBRARY FOR DISPLAYING GRAPH STRUCTURES


We have already seen the Scheme pp function that can "pretty print"
Scheme data structures. It is also convenient to be able to display data
structures graphically. We will develop a library for displaying graph
and tree data structures that uses the portable graphics library from
Section 4.3.
The functions in the file module GRAPHER.S will allow us to plot
tree data structures. A tree has a root node. Each node, including the
root node, can have one or more child nodes. A straight line connects
each child node to its parent node. Figure 4.2 shows a simple tree
display.
The algorithm for calculating where to plot each node in the tree is
fairly simple. We will

1. Set the plot coordinates of each node to zero


2. Call function y-Iayout to recursively calculate the Y plot display
coordinates for each node
3. Call function x-layout to recursively calculate the X plot display
coordinates for each node
4.4. EXAMPLE LIBRARY FOR DISPLAYING GRAPH STRUCTURES 53

The function y-Iayout is called with a node index and the plot level.
The plot level is increased by one every time the function y-Iayout calls
itself recursively. Function y-Iayout creates a list of all child nodes from
the parent node passed in its argument list. Function y-Iayout calls itself
recursively for each child node on this list. Actual Y plot coordinates
are only assigned to a node after the Y coordinates are set for all child
nodes.
The function x-layout is similar to y-Iayout. A list of all child nodes is
created, and function x-layout calls itself recursively for each of these
child nodes.
The root node name is set when the function make-grapher is called
to create a new Grapher object. Child nodes are created by calling the
function add-node with the new node name, the parent node name, and
any arbitrary data that should be stored with the new node. After any
child nodes are added, the function do-layout can be called to reset the
plot coordinates of all nodes in the Grapher object. The function draw
is used to draw the nodes in a Grapher object.

Listing 4.4
File: grapher.s

Description: This file module contains a library for


plotting graph data structures.

Public interface functions:

(make-grapher root-node-name) -> a 'grapher' object


This function opens a graphics window and
returns a 'grapher' object. All other public
functions in this file module require a
'grapher' object as their first argument.

(add-node a-grapher name parent-name)


This function creates a new node as a child
node to an existing node.

(name-to-id a-grapher name) -> integer node ID

(id-to-name a-grapher id) -> string name of node

(get-node-id-from-position a-grapher x y)
-> integer node ID of closest node to the
specified screen coordinates x and y.

(get-node-info a-grapher node)


-> a vector containing elements:
54 4. WRITING PORTABLE SCHEME CODE

0) x coordinate in virtual screen units


1) y coordinate in virtual screen units
2) name
3) parent 10 (integer)
4) node selection flag (integer 0 or 1)
5) arbitrary data '
The node argument can be an integer (in which
case it is interpreted as a node 10), or a
string (in which case it is interpreted as
a name).

(make-node x y name) -> a new node

(get-node-x a-node) -> x virtual screen coordinate

(get-node-y a-node) -> y virtual screen coordinate

(get-node-name a-node) -> node name

(get-parent-id a-node) -> integer parent 10

(set-node-selection-flag a-node flag)


(get-node-selection-flag a-node)
-> integer selection flag value

(set-node-data a-node any-data-at-all)


(get-node-data a-node)
-> any Scheme data structure stored in node

(layout-graph)

Copyright 1995, Mark Watson. All source code rights


reserved: you may not redistribute this source file.
No binary rights reserved: you may use this softeware
in compiled form without restriction.

"constants" for ccessing node data fields:


(define _X-COORD 0)
(define _V-COORD 1)
(define _NAME 2)
(define _PARENT-ID 3)
(define _SELECTION-FLAG 4)
(define _DATA 5)

;; "constants" for accessing grapher data fields:


(define _NUM-NODES 0)
(define _TREE 1)
(define _ROOT-NAME 2)
4.4. EXAMPLE LIBRARY FOR OISPLAYING GRAPH STRUCTURES 55

(define _MAX-NODES 3)
(define _LAST-Y 4)

;; Miscelanious "constants":
(define _V-SPACING 90)
(define _X-SPACING 300)
(define _CHAR-WIDTH 18) width of character of screen in pixels

(define make-grapher
(lambda (root-node-name)
(if (not (string? root-node-name))
(begin
(write "Error: node names must be trings.")
(newline) )
;; Name OK, so create a ne grapher object:
(let ((max-nodes 7)
(num-nodes 1)
(last-y 0) ;; used for layout calculations
(new-node (make-vector 6))
(tree #f))
create the root node:
(set! tree (make-vector max-nodes))
(vector-set! new-node 0 0) ; x coordinate
(vector-set! new-node 1 0) ; y coordinate
(vector-set! new-node 2 root-node-name)
(vector-set! new-node 3 -1)
(vector-set! new-node 4 0) ; selection flag
(vector-set! new-node 5 #f) ;; data for node
;; insert the root node into the tree:
(vector-set! tree 0 new-node)
;; create the list which is a new grapher object:
(vector num-nodes tree root-node-name max-nodes last-y)))))

Private utility functions for accessing fields in


a grapher object:

(define get-num-nodes
(lambda (a-grapher)
(vector-ref a-grapher _NUM-NODES)))

(define get-tree
(lambda (a-grapher) (vector-ref a-grapher _TREE)))

(define get-node
(lambda (a-grapher node)
(let ((tree (get-tree a-grapher))
(num (get-num-nodes a-grapher))
(node-index -1))
node can either be a string name of a node,
56 4. WRITING PORTABLE SCHEME CODE

;; or an integer index:
(if (string? node)
(do ((i 0 (+ i 1)))
«equal? i num))
(if (string=?
node
(vector-ref (vector-ref tree i) _ROOT_NAME))
(set! node-index i)))
(set! node-index node))
(if (and
(> node-index -1)
« node-index (vector-ref a-grapher _MAX-NODES)))
(vector-ref tree node-index) ;; found the node
;; node not in grapher object, so return If:
If))))

(define get-node-index
(lambda (a-grapher node)
(let (tree (get-tree a-grapher))
(num (get-num-nodes a-grapher))
(node-index -1))
;; node can either be a string name of a node,
;; or an integer index:
(if (string? node)
(do ((i 0 (+ i 1)))
«(equal? i num))
(if (string=?
node
(vector-ref (vector-ref tree i) _ROOT-NAME))
(set! node-index i)))
(set! node-index node))
node-index)))

(define add-node
(lambda (a-grapher node-name parent-node data)
(let ((tree (get-tree a-grapher))
(parent-index (get-node-index a-grapher parent-node))
(num (get-num-nodes a-grapher))
(new-node (make-vector 6)))
make sure that there is enough space for a new node:
(if (> (+ num 2) (vector-ref a-grapher _MAX-NODES))
(begin
(display "No space for a new node.")
(newline)
If)
(begin
(vector-set! new-node _X-COORD 0) ; x coordinate
(vector-set! new-node _Y-COORD 0) ; y coordinate
(vector-set! new-node _NAME node-name)
4.4. EXAMPLE LIBRARY FOR DISPLAYING GRAPH STRUCTURES 57

(vector-set! new-node _PARENT-ID parent-index)


(vector-set! new-node _SELECTION-FLAG 0)
(vector-set! new-node _DATA data)
(vector-set! tree num new-node)
(vector-set! a-grapher _NUM-NODES (+ num 1»
new-node»»)

(define do-layout
(lambda (a-grapher)
(let ((tree (get-tree a-grapher»
(parent-index (get-node-index a-grapher _PARENT-ID»
(num (get-num-nodes a-grapher»)
(do ((i 0 (+ i 1»)
((equal? i num»
(let ((node (vector-ref tree i»)
(vector-set! node _X-COORD 0)
(vector-set! node _Y-COORD 0»)
(vector-set! a-grapher _LAST-Y 0)
(y-Iayout a-grapher 0 0)
(x-layout a-grapher 0 0»»

(define y-Iayout
(lambda (a-grapher node-id level)
(let ((tree (get-tree a-grapher»
(parent-index (get-node-index a-grapher _PARENT-ID»
(average-y 0)
(last-y (vector-ref a-grapher _LAST-Y»
(num (get-num-nodes a-grapher»)
(let ((node (vector-ref tree node-id»)
(if (equal? (vector-ref node _Y-COORD) 0) ;; check y coord.
(let ((child-nodes (get-children a-grapher node-id»)
(if (null? child-nodes)
(let ((new-y-value
(+ (vector-ref node _Y-COORD) _Y-SPACING»)
(vector-set! node _Y-COORD
(+ last-y _Y-SPACING»
(display "last-y=") (display last-y) (newline)
(vector-set! a-grapher _LAST-Y new-y-value»
(let ((copy-child-nodes (list-copy child-nodes»
(len (length child-nodes»)
(do ((child-nodes child-nodes (cdr child-nodes»)
((null? child-nodes»
(y-Iayout a-grapher (car child-nodes) (+ level 1»)
(set! average-y 0)
(do ((copy-child-nodes copy-child-nodes
(cdr copy-child-nodes»)
((null? copy-child-nodes»
(set! average-y
(+ average-y
58 4. WRITING PORTABLE SCHEME CODE

(vector-ref
(vector-ref tree (car copy-child-nodes))
3-COORD ))))
(set! average-y (/ average-y len))
(vector-set! node _V-COORD average-y)))
(vector-set! node _X-COORD 0)))))))

(define x-layout
(lambda (a-grapher node-id level)
(let «tree (get-tree a-grapher))
(num (get-num-nodes a-grapher)))
(if (equal? level 0)
;; top level root node:
(let «node (vector-ref tree node-id))
(child-nodes (get-children a-grapher node-id)))
(vector-set! node _X-COORD _X-SPACING)
(do «child-nodes child-nodes (cdr child-nodes)))
«null? child-nodes))
(x-layout a-grapher (car child-nodes) (+ level 1))))
not the root node, so calculate the spacing
based on the number of characters in the
parent node's name:
(let «node (vector-ref tree node-id))
(child-nodes (get-children a-grapher node-id)))
(let «parent-id (vector-ref node _PARENT-ID)))
(let «parent (vector-ref tree parent-id)))
(let «len (string-length (vector-ref parent _NAME))))
(vector-set!
node _X-COORD
(+
(vector-ref parent _X-COORD)
(* len _CHAR-WIDTH)))
layout any unprocessed child nodes:
(do «child-nodes child-nodes (cdr child-nodes)))
«null? child-nodes))
(x-layout
a-grapher
(car child-nodes)
(+ level 1)))))))))))

(define get-children
(lambda (a-grapher node-id)
(let «tree (get-tree a-grapher))
(num (get-num-nodes a-grapher))
(count 0)
(return-list 'C)))
(do «i 0 (+ i 1)))
«equal? i num))
(if (equal? node-id (vector-ref (vector-ref tree i) _PARENT-ID))
4.4. EXAMPLE LIBRARY FOR DISPLAYING GRAPH STRUCTURES 59

(set! return-list (cons i return-list))))


return-list)))

The following plotting code requires the file "graph.s"


to be loaded in order to run:

(define draw
(lambda (a-grapher)
(let «tree (get-tree a-grapher))
(num (get-num-nodes a-grapher)))
(do «i 0 (+ i i)))
«equal? i num))
(let «node (vector-ref tree i)))
(let «parent-id (vector-ref node _PARENT-ID)))
(if (> parent-id -i)
(let «parent (vector-ref tree parent-id))
(y_half (truncate (/ _Y-SPACING 8))))
(plot-line
(+
(vector-ref parent _X-COORD)
(* _CHAR-WIDTH
(string-length
(vector-ref parent _NAME)))
-20)
(+
(vector-ref parent _Y-COORD)
y_half)
(vector-ref node _X-COORD)
(+
(vector-ref node _Y-COORD)
y_half)))))))
(do «i 0 (+ i i)))
«equal? i num))
(let «node (vector-ref tree i)))
(plot-string
(vector-ref node _X-COORD)
(vector-ref node _Y-COORD)
(vector-ref node _NAME)))))))

test code:

; (define g)
;(set! g (make-grapher "root"))
; (display g)
; (pp g)
; (add-node g "node-i" "root" '(this is data 1))
; (add-node g "node-2" "root" '(this is data 2))
; (add-node g "node-i-i" "node-i" '(this is data ii1))
; (get-children g 0)
60 4. WRITING PORTABLE SCHEME CODE

; (get-children g 1)
; (get-children g 3)
; (do-layout g)
;(pp g)
; (open-gr)
; (clear-plot)
; (draw g)
; (c1ose-gr)

Listing 4.5 shows an example of creating a Grapher object in a


Scheme listener window.

Listing 4.5
(load "graph.s")
;Loading "graph.s" -- done
;Value: test

(load "grapher. s")


;Loading "grapher.s" -- done
;Value: draw

(define g (make-grapher "root"))

,flode-l-flode-l-l
root(
\ode-2

FIGURE 4.2. Example Grapher output in a Scheme graphics window.


4.4. EXAMPLE LIBRARY FOR DISPLAYING GRAPH STRUCTURES 61

;Value: g

(pp g)
#(1 #(#(0 0 "root" -1 0 0) 0 0 0 0 0 0) "root" 7 0)
;No value

(add-node g "node-1" "root" '(this is data 1))


;Value 5: #(0 0 "node-1" 0 0 (this is data 1))

(add-node g "node-2" "root" '(this is data 2))


;Value 6: #(0 0 "node-2" 0 0 (this is data 2))

(add-node g "node-1-1" "node-1" '(this is data 1 1 1))


;Value 7: #(0 0 "node-1-1" 1 0 (this is data 1 1 1))

(get-children g 0)
;Value 8: (2 1)

(get-children g 1)
; Value 9: (3)

(get-children g 3)
;Value: 0

(do-layout g)
last-y=O
last-y=90
;Value: #t

(pp g)
#(4
#(#(300 135 "root" -1 0 ())
#(372 180 "node-1" 0 0 (this is data 1))
#(372 90 "node-2" 0 0 (this is data 2))
#(480 180 "node-1-1" 1 0 (this is data 1 1 1))
o
o
0)
"root"
7
90)
;No value

(open-gr)
;No value

(draw g)
;Value: #t
62 4. WRITING PORTABLE SCHEME CODE

4.5 EXAMPLE: PLOTTING MANOELBROT SETS


We are accustomed to thinking of the dimensionality of space as being
an integer number. Distance along a line is measured in one dimen-
sion, position on a piece of paper is measured in two dimensions, and
a point in space is measured in three dimensions. For IBM mathe-
matician Benoit Mandelbrot, space can have a fractional dimension in
the study of fractals. For example, blood vessels form a fractal space.
Mapped out, they fit into a three-dimensional space, but they take up
relatively little volume in our bodies.
We have all seen fractal plots. In this section we develop a very short
program to plot Mandelbrot sets. We calculate Mandelbrot sets using
the arithmetic of complex numbers. A complex number has two real
numbers associated with it: a real and an imaginary part. A complex
number Z is usually represented as
Z = X +iY
where i is defined to be the square root of minus one. X and Yare
real (scalar) numbers. In programs we need to store two real numbers
for each complex number. We also need to define the standard arith-
metic operations for complex numbers. File COMPLEX.s in Listing 4.6
shows the implementation of a Scheme library for complex-number
arithmetic:

Listing 4.6

File: complex.s

Description: This file contains a complex number


package that simulates the following
Common LISP functions:

(complex <real-part> <imaginary-part»


==> a list
(realpart <a list of 2 numbers»
==> the first number in the list
(imagpart <a list of 2 numbers»
==> the second number in the list
(complex_+ <2 number list> <2 number list»
==>
(complex_* <2 number list> <2 number list»
==>

(define complex
(lambda (x y)
4.5. EXAMPLE: PLOTTING MANDELBROT SETS 63

(list x y)))

(define realpart
(lambda (z)
(car z)))

(define imagpart
(lambda (z)
(cadr z)))

(define complex_+
(lambda (zl z2)
(list (+ (realpart zl) (realpart z2))
(+ (imagpart zl) (imagpart z2)))))

(define complex_*
(lambda (zl z2)
(list
(-
(* (realpart zl) (realpart z2))
(* (imagpart zl) (imagpait z2)))
(+
(* (realpart zl) (imagpart z2))
(* (realpart z2) (imagpart zl))))))
The equation for calculating Mandelbrot sets is
Z-at-time-n+ 1 = (Z-at-time-n) * (Z-at-time-n) + Constant
We can set the constant term to zero, and we can use the following al-
gorithm to determine whether a complex number Z is in the Mandelbrot
set:
Loop N times:
set Z = Z * Z
if Z > small constant, then exit loop
A complex point Z is a member of the Mandelbrot set if the ex-
pression Z=Z*Z stays small. If the complex number Z is not in the
Mandelbrot set, then this expression quickly becomes large. For points
not in the Mandelbrot set we count the number of times we execute the
expression Z = Z * Z before the value of Z becomes larger than some
pre-defined, small threshold value; a color is selected for plotting based
on this count.
Listing 4.7 shows the file MANDELBR.S.

Listing 4.7

; File: Mandelbr.s
64 4. WRITING PORTABLE SCHEME CODE

Calculate and plot the Mandelbrot set


Copyright 1990, 1994 by Mark Watson

Required file modules: graph.s and complex.s

Define the maximum number of gray-scale or color values


that function plot-fill-rect uses as a pattern (or color) range:
(Plot-fill-rect is defined in the graphics library in Chapter 2.)

(define MAX_COLORS 250)

Define the number of complex points to evaluate for Mandelbrot


set membership: a rectangle of points num-x-cells by num-y-cells:

(define num-x-cells 80)


(define num-y-cells 80)

Set the cell width and height for plotting a single point
on the complex plane:

(define cell-width 10)


(define delta-x-cell 0)
(define delta-y-cell 0)

(set! delta-x-cell (/ 3.2 num-x-cells))


(set! delta-y-cell (/ 3.2 num-y-cells))

Function M is called to calculate the Mandelbrot set


for complex points around the complex number zero.

(define debug #f)

(define M
(lambda 0
(open-gr) ;; open a graphics window
(plot-string 100 900 "Mandelbrot Plot")
(let ((x 0.5) (y 0.5) (z (complex 0 0)))
(do ((ix 0 (+ ix 1)))
((= ix num-x-cells))
4.6. EXAMPLE: PLOTTING CHAOTIC POPULATION GROWTH 65

(set! x (- (* ix delta-x-cell) 2.0))


(display "x =") (display x) (newline)
(do ((iy 0 (+ iy 1)))
((= iy num-y-cells))
(set! y (- (* iy delta-y-cell) 1.6))
(display "y =") (display y) (newline)
(set! z (complex x y))
(let ((index 0))
(do ((i 0 (+ i 1)))
((not « i MAX_COLORS)))
(set! z (complex_+
(complex x y)
(complex_* z z)))
(set! index i)
(if (> (+ (* (realpart z) (realpart z))
(* (imagpart z) (imagpart z))) 4)
(set! i 1000)))
(if debug
(begin
(display "ix = II) (display ix) (newline)
(display "iy = II) (display iy) (newline) (newline)))
(plot-fill-rect-gray-scale
(+ 10 (* ix cell-width))
(+ 10 (* iy cell-width))
cell-width cell-width
(- MAX_COLORS index))))))))

4.6 EXAMPLE: PLOTTING CHAOTIC POPULATION GROWTH


As a simple example of a chaotic (nonlinear) system, biologist Robert
May modeled fish population growth (Gleick, 1987) with the simple
equation
Population [N+1] = delta_value * Population[N] * (1 - Population[n])

The parameter delta_value controls the rate of population growth.


The truly amazing thing about this simple equation is its chaotic
behavior as delta_value is slowly increased in value.
The first sample program, shown in Listing 4.8 (file MAYl.S), pro-
duced the diagram seen in Figure 4.4. (The graphics library must be
loaded before running this program.) The main loop in this first ex-
ample program iterates the value of the y-coordinate in the graphics
window (y-axis).
The population growth rate delta_value increases in value with the
y-coordinate y-axis. The x-coordinate in Figure 4.4 is calculated for
66 4. WRITING PORTABLE SCHEME CODE

FIGURE 4.3. Plot produced by the program in Listing 4.7. Note that the graphics library for MIT
Scheme frames filled rectangles with a black border; these borders were manually removed
by setting the color map entry of black to white in the screen dump for this figure.

each iteration of the above population growth equation by scaling the


population: multiplying it by 500.02.

Listing 4.8

File: Mayl. s

Population grovth model of Robert May: generates a bifurcation


diagram shoving large scale effect of the population grovth rate.
Note: the area to the upper right of the initial curve indicates
extinction due to the rate of population grovth being too small
(the variable lambda).

Copyright 1990, 1995 by Mark Watson

(define bifur
(lambda 0
(open-gr)
(clear-plot)
4.6. EXAMPLE: PLOTTING CHAOTIC POPULATION GROWTH 67

(plot-string 500 920 "Extinction")


(plot-string 32 890 "Steady state")
(plot-string 660 700 "Period doubled")
(plot-string 400 500 "Chaos")
(let ((x-axis 0)
(delta-value 0)
(x 0.1)
(population 0))
(do ((y-axis 0 (+ y-axis 1)))
((> y-axis 219))
(set! delta-value (* 4 (+ 0.20 (/ y-axis 250.0))))
(do ((iter 0 (+ iter 1)))
((> iter 197))
(set! population (* delta-value x (- 1 x)))
(set! x-axis (truncate (* population 500.02)))
(if (and (> x-axis 0) « x-axis 501))
(plot-line
(* 2 x-axis)
(- 1000 (* 2 y-axis))
(* 2 x-axis)
(- 1000 (* 2 y-axis))))
(set! x population))))))

The example population growth shows the extreme sensitivity of


nonlinear systems to initial conditions. In Figure 4.4 the top of the
diagram illustrates the area of small population growth rate where
the population increases gradually. As the growth rate delta_value
increases, frequency doubling occurs: the population tends to oscil-
late between two values. As the growth rate continues to increase
(lower in Figure 4.4), frequency doubling occurs again: the popula-
tion oscillates among four values. As delta_value continues to increase
(decreasing y-coordinate in the plot), the population becomes chaotic
and unpredictable.
The program in Listing 4.9 (file MAY2.S) generated the plot in Figure
4.5, which magnifies the region around the area where the period of
population growth doubles and the lower area, where the population
growth becomes chaotic. The value for delta_value is different from that
in the program in Listing 4.9.

Listing 4.9

File: May2. s

Generates a magnified bifurcation diagram magnifying the area


where period doubling turns into chaos.

Copyright 1990, 1995 by Mark Watson


68 4. WRITING PORTABLE SCHEME CODE

_ -------- ExtInction
Study atate -------~ ..
....
~..........

Perlod doubled

Chaos

FIGURE 4.4. Effect of population growth rate on population. The vertical axis shows the popula-
tion growth rate parameter lambda increasing from 0 (top of plot) to thd value 1 (at the bottom
of the plot). The horizontal axis represents the simulated population (larger values to the right
side of the plot).

(define bifur
(lambda 0
(open-gr)
(clear-plot)
(let ((x-axis 0)
(delta-value 0)
(x 0.1)
(population 0))
(do ((y-axis 0 (+ y-axis 1)))
((> y-axis 219))
(set! delta-value (* 4 (+ 0.85 (/ y-axis 1500.0)))) change the
growth rate
(do ((iter 0 (+ iter 1)))
((> iter 197))
(set! population (* delta-value x (- 1 x)))
(set! x-axis (truncate (* population 500.02)))
(if (and (> x-axis 0) « x-axis 501))
(plot-line
4.6. EXAMPLE: PLOTTING CHAOTIC POPULATION GROWTH 69

FIGURE 4.5. Effect of population growth rate on population. This plot is an enlargement of the
area showing frequency doubling in Figure 4.4.

(* 2 x-axis)
(- 1000 (* 2 y-axis»
(* 2 x-axis)
(- 1000 (* 2 y-axis»»
(set! x population»»»
In Figure 4.5 notice the clear, non-chaotic areas near the bottom of
the diagram. These are regimes in which the system becomes stable and
oscillates among three possible values. If the sample program in Listing
4.9 is modified to magnify this stable regime, the resulting plot will
look surprisingly like the plot in Figure 4.4. Indeed, this repeatability is
infinitely deep as we continue to magnify areas of the plot near stable
regimes. This self-similarity under magnification is also seen in the
Mandlebrot set plots generated in Section 4.5.
CHAPTER 5
AN ITERATIVE ApPROACH
TO ANALYSIS, DESIGN,
AND IMPLEMENTATION

The example programs that we have developed so far in this book have
been fairly simple. More complex programs usually require an iterative
approach: we perform initial requirements analysis, design, and a first-
stage implementation. After using the first-stage implementation we
revisit the analysis and design, then re-implement parts of the program,
building a second-stage implementation.

5. 1 PRELIMINARY ANALYSIS, DESIGN, AND IMPLEMENTATION


OF A NETWORK SEARCH PROGRAM
Figure 5.1 shows a simple network with eleven sparsely connected
nodes. This figure was generated by the program in file DEPTH.S on
the disk included with this book. This file is listed at the end of this
section.
We will use the network shown in Figure 5.1 as an example in our
analysis of the problem of finding a path between two nodes in a net-
work. If we want to start at node n1 and find an efficient path to node
n11 in Figure 5.1, there are two good search strategies to use: depth-first
search and breadth-first search.
For depth-first search we start by listing all nodes connected to node
n1 (in this example). One of these connected nodes is chosen to start
with, and we then list all nodes connected to this second node. This
process is repeated until either we find the goal node (node n11 in this
example) or we exhaust our search possibilities. Whenever a depth-first
72 5. AN ITERATIVE ApPROACH TO ANALYSIS, DESIGN, AND IMPLEMENTATION

nll

FIGURE 5.1. A sparsely connected network with eleven nodes.

search ends without finding the goal node, the search backs up to the
last node with unexplored connected nodes. A depth first search can be
very inefficient because if we are unlucky enough to start searching the
wrong path, we search that path exhaustively before trying alternative
paths. Depth-first search has the advantage of not requiring very much
storage.
A breadth-first search uniformly "fans out" from the starting node
until the goal node is reached. The breadth-first search has the ad-
vantage of a fairly constant execution time for a given network. The
disadvantage of a breadth-first search is the storage required to keep
track of all the nodes that we visit while "fanning out" from the starting
node.
We will implement a depth-first search in this section. In Section 5.3,
we will improve the depth-first search program by by modifying it to
perform a breadth-first search.
For the example network shown in Figure 5.1, successive iterations
of our depth-first search from node n1 to node n11 will visit the following
nodes:
5.1. PRELIMINARY ANALYSIS, DESIGN, AND IMPLEMENTATION Of A NETWORK SEARCH PROGRAM 73

1. nl
2. nl n2
3. nl n2 n3
4. nl n2 n3 n5
5. nl n2 n3 n5 n8
6. nl n2 n3 n5 n8 n4
7. nl n2 n3 n5
8. nl n2 n3 n6
9. nl n2 n3 n6 nlO
10. nl n2 n3 n6 nlO n9
11. nl n2 n3 n6 nlO n9 n7
12. nl n2 n3 n6 nlO n9 n7 nIl

Notice how the search proceeds in a given direction until it finds the
goal node, runs into a "dead end," or finds only nodes that are on the
current path list.
Listing 5.1 shows the contents of file DEPTH.S on the disk in-
cluded with this book. The variable nodes defines the coordinates of
each node in the network. Referring to Figure 5.1, the reader can
add additional nodes to the example program in Listing 5.1. The
variable paths is used to specify which nodes in the network are con-
nected. The function init-path-Iengths is used to redefine the path list
to include the distance between nodes. The function slow-path-Iength
calculates the shortest possible path between two nodes. The function
dist-between-points calculates the Cartesian distance between two nodes.
The function find-connected-nodes returns a list of all nodes directly con-
nected to a specified node. The function depth performs a depth-first
search for a path between two specified nodes. The two functions test1
and test2 are used to test the search; these functions generated the plots
in Figures 5.1 and 5.2.

Listing 5.1
File: depth. s

Description: This file contains a simple depth-first search


program.

Copyright 1995, Mark Watson. All source code rights reserved.


This program may be used in compiled form without restrictions.

(define (Y-coord x) (truncate (cadr x)))


(define (X-coord x) (truncate (car x)))
74 5. AN ITERATIVE ApPROACH TO ANALYSIS, DESIGN, AND IMPLEMENTATION

(define nodes
,(
(nl (120 804))
(n2 (100 620))
(n3 (220 120))
(n4 (440 750))
(n5 (385 440))
(n6 (520 88))
(n7 (610 600))
(n8 (695 808))
(n9 (702 515))
(nl0 (800 220))
(nll (830 808))))

(define paths
,(
(nl n2) (n2 n3) (n3 n5) (n3 n6) (n6 nl0)
(n9 nl0) (n7 n9) (nl n4) (n4 n2) (n5 n8)
(n8 n4) (n7 nll)))

(define init-lengths
(lambda (pathlist)
(let ((new-path-list 'C))
(pathlength 0)
(path-with-length 'C)))
(do ((path pathlist (cdr path)))
( (null? path))
(set! pathlength (slow-path-length (car path)))
(set! path-with-length (append (car path) (list pathlength)))
(set! new-path-list (cons path-with-length new-path-list)))
new-path-list)))

"As the crow flies" distance between


the starting and ending nodes on a path:

(define slow-path-length
(lambda (path)
(let ((nodel (car path))
(node2 (cadr path)))
(let ((nl (assoc nodel nodes))
(n2 (assoc node2 nodes)))
(dist-between-points (cadr nl) (cadr n2))))))

Calculate the Cartesian distance between points:


5.1. PRELIMINARY ANALYSIS, DESIGN, AND IMPLEMENTATION OF A NETWORK SEARCH PROGRAM 75

(define dist-between-points
(lambda (pointl point2)
(let «x-dif (- (X-coord point2) (X-coord pointl»)
(y-dif (- (V-coord point2) (V-coord pointl»»
(sqrt (+ (* x-dif x-dif) (* y-dif y-dif»»»

Change the global path list to include distance between


adjacent nodes:

(set! paths (init-lengths paths»

; (pp paths)

(define find-connected-nodes
(lambda (a-node)
(let «ret-list 'C»~)
(do «1 paths (cdr 1»)
«null? 1»
(let «path (car 1») ; (nodel node2 distance)=path
(if (equal? a-node (car path»
(set! ret-list (cons (cadr path) ret-list»)
(if (equal? a-node (cadr path»
(set! ret-list (cons (car path) ret-list»»)
ret-list»)

(find-connected-nodes 'n2)

(define depth
(lambda (path goal)
(if (equal? (car (last-pair path» goal)
path ;; done with search
(let «new-nodes (find-connected-nodes (car (last-pair path»»
(keep-searching #t)
(ret-val #f)
(do «nn new-nodes (cdr nn»)
«or
(null? nn)
(equal? keep-searching #f»)
(if (not (member (car nn) path»
(let «temp (depth (append path (list (car nn»)
goal»)
(if (not (null? temp»
(begin
(set! ret-val temp)
(set! keep-searching #f»»»
ret-val»»

;;;;;;;;;;;;;;;; Test code with graphics support:


76 5. AN ITERATIVE ApPROACH TO ANALYSIS, DESIGN, AND IMPLEMENTATION

(define testl
(lambda 0
(open-gr)
(pen-width 1)
(do ((p paths (cdr p)))
((null? p))
(display "(car p)=") (display (car p)) (newline)
(let ((from (cadr (assoc (caar p) nodes)))
(to (cadr (assoc (cadar p) nodes))))
(plot-line
(x-coord from)
(y-coord from)
(x-coord to)
(y-coord to))))
(do ((n nodes (cdr n)))
((null? n))
(let ((n-val (cadar n)))
(plot-string
(+ 2 (x-coord n-val))
(y-coord n-val)
(symbol->string (caar n)))))))

(define test2
(lambda 0
(define (draw-path pI)
(pen-width 3)
(let ((nodel (cadr (assoc (car pI) nodes))))
(set! pI (cdr pI))
(do ((p pI (cdr p)))
((null? p))
(plot-line (x-coord nodel)
(y-coord node!)
(x-coord (cadr (assoc (car p) nodes)))
(y-coord (cadr (assoc (car p) nodes))))
(set! nodel (cadr (assoc (car p) nodes))))))
(draw-path (depth '(nl) 'nll))))

(test!)
(test2)

Listing 5.2 shows the values of the variables nodes and paths "pretty
printed" with the built-in Scheme function pp after loading the file
DEPTH.S.

Listing 5.2
(pp nodes)
((nl (120 804)) (n2 (100 620)) (n3 (220 120)) (n4 (440 750))
(n5 (385 440)) (n6 (520 88)) (n7 (610 600)) (n8 (695 808))
5.1. PRELIMINARY ANALYSIS, DESIGN, AND IMPLEMENTATION OF A NETWORK SEARCH PROGRAM 77

(n9 (702 515» (nl0 (800 220» (nll (830 808»)


;No value

(pp paths)
«n7 nll 302.76063152266016) (n8 n4 261.51290599127225)
(n5 n8 481.1694088364305) (n4 n2 364.0054944640259)
(nl n4 324.5242671973854) (n7 n9 125.25573839150046)
(n9 nl0 310.85205484281425) (n6 nl0 309.5545186231337)
(n3 n6 301.7018395701292) (n3 n5 360.0347205478938)
(n2 n3 514.1984052872976) (nl n2 185.0837648201484»
;No value
Calculating and storing the path lengths between nodes are not
required for the example depth-first search in file DEPTH.S, but I an-
ticipated that this information would be useful for applications using
the search functions.
Listing 5.3 shows the output from executing the function depth. The
function is called twice; the second call has recursive calls to function
depth traced using the built-in Scheme trace function.

Listing 5.3
» (load "depth.s")
;Loading "depth.s" done
;Value: test2

» (depth '(nl) 'nll)


;Value 4: (nl n2 n3 n6 nl0 n9 n7 nll)

» (trace depth)
;No value

» (depth '(nl) 'nll)


[Entering #[compound-procedure 6 depth]
Args: (nO
nl1]
[Entering #[compound-procedure 6 depth]
Args: (nl n2)
nl1]
[Entering #[compound-procedure 6 depth]
Args: (nl n2 n3)
nl1]
[Entering #[compound-procedure 6 depth]
Args: (nl n2 n3 n5)
nl1]
[Entering #[compound-procedure 6 depth]
Args: (nl n2 n3 n5 n8)
nl1]
[Entering #[compound-procedure 6 depth]
78 5. AN ITERATIVE ApPROACH TO ANALYSIS, DESIGN, AND IMPLEMENTATION

Args: (nl n2 n3 n5 n8 n4)


n11]
(0
<== #[compound-procedure 6 depth]
Args: (nl n2 n3 n5 n8 n4)
n11]
[0
<== #[compound-procedure 6 depth]
Args: (nl n2 n3 n5 n8)
n11]
[0
<== #[compound-procedure 6 depth]
Args: (nl n2 n3 n5)
n11]
[Entering #[compound-procedure 6 depth]
Args: (nl n2 n3 n6)
n11]
[Entering #[compound-procedure 6 depth]
Args: (nl n2 n3 n6 nl0)
n11]
[Entering #[compound-procedure 6 depth]
Args: (nl n2 n3 n6 nl0 n9)
n11]
[Entering #[compound-procedure 6 depth]
Args: (nl n2 n3 n6 nl0 n9 n7)
n11]
[Entering #[compound-procedure 6 depth]
Args: (nl n2 n3 n6 nl0 n9 n7 nll)
n11]
[(nl n2 n3 n6 nl0 n9 n7 nll)
<== #[compound-procedure 6 depth]
Args: (nl n2 n3 n6 nl0 n9 n7 nll)
n11]
[(nl n2 n3 n6 nl0 n9 n7 nll)
<== #[compound-procedure 6 depth]
Args: (nl n2 n3 n6 nl0 n9 n7)
n11]
[(nl n2 n3 n6 nl0 n9 n7 nll)
<== #[compound-procedure 6 depth]
Args: (nl n2 n3 n6 nl0 n9)
n11]
[(n1 n2 n3 n6 nl0 n9 n7 nll)
<== #[compound-procedure 6 depth]
Args: (nl n2 n3 n6 nl0)
n11]
[(nl n2 n3 n6 nl0 n9 n7 nll)
<== #[compound-procedure 6 depth]
Args: (nl n2 n3 n6)
n11]
5.1. PRELIMINARY ANALYSIS, DESIGN, AND IMPLEMENTATION OF A NETWORK SEARCH PROGRAM 79

FIGURE 5.2. A solution to the problem of finding an efficient path from node n1 to node n11.

[(n1 n2 n3 n6 n10 n9 n7 n11)


<== #[compound-procedure 6 depth]
Args: (n1 n2 n3)
n11]
[(n1 n2 n3 n6 n10 n9 n7 n11)
<== #[compound-procedure 6 depth]
Args: (n1 n2)
n11]
[(n1 n2 n3 n6 n10 n9 n7 n11)
<== #[compound-procedure 6 depth]
Args: (n1)
n11]
;Value 7: (n1 n2 n3 n6 n10 n9 n7 n11)

Notice in Listing 5.3 that the depth-first search first looks at nodes in
the following order: n1 n2 n3 n5 n8 n4. This search is discarded since
node n4 is connected to nodes n1, n2, and n8, which have already been
searched.
Figure 5.1 was produced from executing the function test1 in file
DEPTH.S. Figure 5.2 was produced from executing function test2.
80 5. AN ITERATIVE ApPROACH TO ANALYSIS, DESIGN, ANO IMPLEMENTATION

5.2 EVALUATION OF THE FIRST IMPLEMENTATION


OF THE NETWORK SEARCH PROGRAM
The depth-first search function depth shown in Listing 5.1 is simple, and
fairly efficient for small search problems. However, a depth-first search
can be extremely inefficient for large search problems when the first
path searched is the wrong path. The search will continue recursively
"going in the wrong direction" until it can go no further, and it then
backtracks its steps.

5.3 IMPROVING OUR ANALYSIS, DESIGN, AND IMPLEMENTATION


Figure 5.1 showed a simple network with eleven sparsely connected
nodes. We implemented a depth first search in Section 5.1.
For a breadth-first search we start with a node and list all connected
nodes. We then list all nodes that are connected to these nodes. The
search uniformly "fans out" from the starting node until the goal node
is reached. The breadth-first search has the advantage of a fairly con-
stant execution time for a given network. Usually the search time is
considerably less than for a depth-first search. The disadvantage of
a breadth-first search is the storage required to keep track of all the
nodes that we visit while "fanning out" from the starting node. We will
implement a breadth-first search in this section.
For the example network shown in Figure 5.1, successive iterations
of our breadth first search from node n1 to node n11 will have visited
the following nodes:
1. nl
2. nl n2 n4
3. nl n2 n4 n3 n8
4. nl n2 n4 n3 n8 n5 n6
5. nl n2 n4 n3 n8 n5 n6 nlO
6. nl n2 n4 n3 n8 n5 n6 nlO n9
7. nl n2 n4 n3 n8 n5 n6 nlO n9 n7
8. nl n2 n4 n3 n8 n5 n6 nlO n9 n7 nIl

Notice how the search uniformly "fans out" from the starting node
n1 towards the goal node n11.
Listing 5.4 shows the contents of file BREADTH.S on the disk in-
cluded with this book. The file BREADTH.s was created by copying
the file DEPTH.s, replacing the depth-first search function depth with
5.3. IMPROVING OUR ANALYSIS, DESIGN, AND IMPLEMENTATION 81

the breadth first search function search. The last line of the test function
test2was also modified to call function search instead of function depth.

Listing 5.4
;; File: breadth.s

Description: This program was developed from the simple depth


first search program in the file DEPTH.S. Here,
we store all possible nodes to search, and "fan
out" the search in parallel from each step in
the search.

Copyright 1995, Mark Watson. All source code rights reserved.


This program may be used in compiled form without restrictions.

(define V-coord (lambda (x) (truncate (cadr x))))


(define X-coord (lambda (x) (truncate (car x))))

(define nodes
,(
(nl (120 804))
(n2 (100 620))
(n3 (220 120))
(n4 (440 750))
(n5 (385 440))
(n6 (520 88))
(n7 (610 600))
(n8 (695 808))
(n9 (702 515))
(nl0 (800 220))
(nl1 (830 808))))

(define paths
,(
(nl n2) (n2 n3) (n3 n5) (n3 n6) (n6 nl0)
(n9 nl0) (n7 n9) (nl n4) (n4 n2) (n5 n8)
(n8 n4) (n7 nll)))

(define init-Iengths
(lambda (pathlist)
(let «new-path-list 'C))
(pathlength 0)
(path-with-Iength 'C)))
(do «path pathlist (cdr path)))
( (null? path))
(set! pathlength (slow-path-Iength (car path)))
(set! path-with-Iength (append (car path) (list pathlength)))
82 5. AN ITERATIVE ApPROACH TO ANALYSIS, DESIGN, ANO IMPLEMENTATION

(set! new-path-list (cons path-with-length new-path-list)))


new-path-list)))

; "As the crow flies" distance between


the starting and ending nodes on a path:

(define slow-path-length
(lambda (path)
(let «nodel (car path))
(node2 (cadr path)))
(let «nl (assoc nodel nodes))
(n2 (assoc node2 nodes)))
(dist-between-points (cadr nl) (cadr n2))))))

; Calculate the Cartesian distance between points:

(define dist-between-points
(lambda (pointl point2)
(let «x-dif (- (X-coord point2) (X-coord pointl)))
(y-dif (- (Y-coord point2) (Y-coord pointl))))
(sqrt (+ (* x-dif x-dif) (* y-dif y-dif))))))

(pp paths)

Change the global path list to include distance between


adjacent nodes:

(set! paths (init-lengths paths))

; (pp paths)

(define find-connected-nodes
(lambda (a-node)
(let «ret-list 'C)))
(do «1 paths (cdr 1)))
«null? 1))
(let «path (car 1))) (nodel node2 distance)=path
(if (equal? a-node (car path))
(set! ret-list (cons (cadr path) ret-list)))
(if (equal? a-node (cadr path))
(set! ret-list (cons (car path) ret-list)))))
ret-list)))

(find-connected-nodes 'n2)
5.3. IMPROVING OUR ANALYSIS, DESIGN, AND IMPLEMENTATION 83

(define search
(lambda (start-node goal-node)
(let. «a-good-path '(»
(visited-list (list start-node»
(search-list
(list (list start-node start-node 0.0»)
(search-func
(letrec
«next
(lambda (s-list)
(let «new-s-list '(»)
(do «1 s-list (cdr 1»)
«null? 1»
(let «path (car 1»)
(let «last-node
(car (last-pair (except-last-pair path»»)
(let «connected-nodes
(find-connected-nodes last-node»)
(do «n connected-nodes (cdr n»)
«null? n»
(if (not (member (car n) visited-list»
(begin
(set!
new-s-list
(cons
(append
(except-last-pair path)
(list (car n»
(list
(+
(car (last-pair path» old distance
(slow-path-length
(list
(car
(last-pair
(except-last-pair path»)
(car n»»»
new-s-list) )
(set!
visited-list
(cons (car n) visited-list»»»»)
new-s-list»)

(found-goal-node?
(lambda 0
(let «good-path '(»)
(do «1 search-list (cdr 1»)
«null? 1)
(not (null? good-path»)
84 5. AN ITERATIVE ApPROACH TO ANALYSIS, DESIGN, AND IMPLEMENTATION

(if (member goal-node (car 1»


(begin
(set!
good-path
(car 1»
(display "Found a good path:")
(pp good-path)
(newline»»
good-path) ) )

(search-l
ClambdaO
(do ((iter 0 (+ iter 1»)
((or
(equal? iter (length nodes»
(not (null? a-good-path»»
(set!
search-list
(next search-list»
(newline)
(display "search level=") (display iter) (newline)
(display "current visited list:") (newline)
(pp visited-list) (newline)
(display "current search list:") (newline)
(pp search-list) (newline)
(set! a-good-path (found-goal-node?»)
(cdr a-good-path»»
search-l» )
(search-func)
a-good-path»)

(search 'nl 'nll)

"J J J J J'" J J J J" Test code with graphics support:

(define testl
(lambda 0
(open-gr)
(pen-width 1)
(do ((p paths (cdr p»)
((null? p»
(display "(car p)=") (display (car p» (newline)
(let ((from (cadr (assoc (caar p) nodes»)
(to (cadr (assoc (cadar p) nodes»»
(plot-line
(x-coord from)
(y-coord from)
(x-coord to)
(y-coord to»»
5.3. IMPROVING OUR ANALYSIS, DESIGN, AND IMPLEMENTATION 85

(do ((n nodes (cdr n»)


((null? n»
(let ((n-val (cadar n»)
(plot-string
(+ 2 (x-coord n-val»
(y-coord n-val)
(symbol->string (caar n»»»)

(define test2
(lambda 0
(define draw-path (lambda (pI)
(pen-width 3)
(let ((nodel (cadr (assoc (car pI) nodes»»
(set! pI (cdr pI»
(do ((p pI (cdr p»)
((null? p»
(plot-line (x-coord nodel)
(y-coord node1)
(x-coord (cadr (assoc (car p) nodes»)
(y-coord (cadr (assoc (car p) nodes»»
(set! nodel (cadr (assoc (car p) nodes»»»)
(draw-path (except-Iast-pair (search 'nl 'nll»»)

(test1)
(test2)
The breadth-first search function search is considerably more com-
plex than the simple depth-first search function depth that it replaced.
Part of this added complexity is due to calculating and storing path
lengths during the search, but most of the additional complexity is
due to the requirements of storing all searched nodes and adding only
directly connected nodes to this list of searched nodes during each
iteration of the breadth-first search.
Listing 5.5 shows the output from executing the function search.

Listing 5.5
» (search 'nl 'nll)
search level=O
current visited list:

(n4 n2 nl)
current search list:

((nl nl n4 324.5242671973854) (nl nl n2 185.0837648201484»

search level=l
current visited list:
86 5. AN ITERATIVE ApPROACH TO ANALYSIS, DESIGN, AND IMPLEMENTATION

(n3 n8 n4 n2 nl)
current search list:

((nl nl n2 n3 699.2821701074461) (nl nl n4 n8 586.0371731886577))

search level=2
current visited list:

(n6 n5 n3 n8 n4 n2 nl)
current search list:

((nl nl n2 n3 n6 1000.9840096775753)
(nl nl n2 n3 n5 1059.3168906553399))

search level=3
current visited list:

(nl0 n6 n5 n3 n8 n4 n2 nl)
current search list:

((nl nl n2 n3 n6 nl0 1310.538528300709))

search level=4
current visited list:

(n9 nl0 n6 n5 n3 n8 n4 n2 nl)


current search list:

((nl nl n2 n3 n6 nl0 n9 1621.3905831435231))

search level=5
current visited list:

(n7 n9 nl0 n6 n5 n3 n8 n4 n2 nl)


current search list:

((nl nl n2 n3 n6 nl0 n9 n7 1746.6463215350236))

search level=6
current visited list:

(nll n7 n9 nl0 n6 n5 n3 n8 n4 n2 nl)


current search list:

((nl nl n2 n3 n6 nl0 n9 n7 nll 2049.406953057684))


Found a good path:
(nl nl n2 n3 n6 nl0 n9 n7 nll 2049.406953057684)
;Value 4: (nl n2 n3 n6 nl0 n9 n7 nll 2049.406953057684)
5.3. IMPROVING OUR ANALYSIS, DESIGN, AND IMPLEMENTATION 87

Figure 5.2 was produced from executing function test2 in the file
DEPTH.S. However, the same graph can be produced by executing
function test2 in the file BREADTH.s.
CHAPTER 6
NEURAL NETWORK
LIBRARY

Neural networks are useful for many pattern recognition and control
applications. In this chapter we design a library that supports "delta
rule," or backwards error propagation networks. For flexibility our
library supports any number of neuron layers. For efficiency our li-
brary uses momentum calculations to train networks. We will discuss
momentum in the discussion of requirements in Section 6.1.
We will also see how it is often useful to create separate low-
level utility libraries to support the library that we are designing and
implementing. Here we notice the requirement to manipulate two-
dimensional arrays. Since Scheme does not support two-dimensional
arrays, we are careful to provide this capability in a separate library
that will be reusable in other non-neural-network applications.
The code listed in this chapter can be found in the file NN.S on
the included disk. You will also need to load the array library in file
ARRAYS. The neural network library can optionally use the graphics
library developed in Chapter 4; if you use graphics, remember also to
load file GRAPH.s.

6. 1 REOUIREMENTS FOR A NEURAL NETWORK LIBRARY


The neural network library developed in this chapter can be used in
other Scheme programs that you write for solving pattern matching
and control problems. Real (biological) neural networks are systems
of relatively simple processing elements that are massively intercon-
nected. Long-term memory or information content in neural networks
is typically stored in the state of the connections between neurons. In
90 6. NEURAL NElWORK LIBRARY

a software simulation of a neural network we call the connections be-


tween neurons connection weights, or more commonly simply weights.
Neural networks are trained by exposure to data values specifying de-
sired system outputs for different system inputs. During the training
process the weights in a neural network are adjusted to minimize the
error for the set of input/output training data.
Neural networks are very fault-tolerant. They can be partially de-
stroyed and still function with some degradation of performance. This
fault-tolerance will someday make it possible to construct components
for computers with huge numbers of neurons and weights. Present
digital components, like memory and CPU chips, must be perfect, oth-
erwise they must be discarded. With neural network components, some
error rate in the sub-components can be tolerated, decreasing the cost
of manufacture and increasing the possible complexity of the devices.
To begin our discussion of supervised learning in neural networks,
we will define several terms. A neuron is a simple processor that sums the
outputs from one or more other neurons, applies some form of transfer
function to this sum, and outputs this transformed sum to the inputs of
other neurons. The transfer function used in the neural network library
is called a Sigmoid function. The first derivative of this function is also
used in training the weights during supervised learning. The output of
a neuron is called its activation energy. A weight is a numerical quantity
that determines how much of a neuron's output value reaches the input
to a neuron to which it is connected. A neuron layer is a set of similarly
connected neurons. Momentum refers to remembering how much each
weight was modified in a previous training cycle, so that weights that
are grossly incorrect can be corrected more quickly (i.e., their learning
rate gains momentum, thereby increasing). Figure 6.1 shows a neural
network with three layers (input, hidden, and output) with a total of
seven neurons and ten weights.
The requirements for the neural network library are:
• Support for any number of neuron layers
• Support of momentum to increase the rate of supervised learning
• Support for embedding trained neural networks in other programs

6.2 DESIGN OF A NEURAL NETWORK LIBRARY


The first design decision that we will make is how to store the following
data for a neural network. From Figure 6.1 we know that we will want
to store at least the following information:
• neuron activation energies
6.2. DESIGN OF A NEURAL NETWORK LIBRARY 91

2 Hidden 2 Output
Neurons Neurons
3 Input II
Neurons HI 01
accept 12
system
inputs 13 02
There are six There are four
weights which weights which
connect each connect each of
of the three the two hidden
input neurons neurons with
with each each of the two
of the two output neurons
hidden neurons

FIGURE 6.1. A neural network with three layers of neurons (input, hidden, and output) and two
weight sets (one connecting each input layer neuron to each hidden layer neuron and one
connecting each hidden layer neuron to each output layer neuron).

• weights
• number of neurons in each neuron layer

In practice there are other data structures that we will need to calcu-
late and store. We will want to create one data object that completely
represents the state of a neural network. This is the information that
we will need to store:

• number of neuron layers


• number of neurons in each layer
• a one-dimensional array of activation values for each neuron layer
• a one-dimensional array used as intermediate storage for calculat-
ing the sum of products of input neuron activation values times the
appropriate weight
• a two-dimensional array for each pair of connected neuron layers
for the weight values for each combination of connected neurons
• a two-dimensional array for each pair of connected neuron layers
for calculating and storing delta weight values (i.e., the change to
each weight since the last supervised learning cycle)
• a two-dimensional array for each pair of connected neuron layers
for storing the delta weight values from the last supervised learning
cycle (for momentum)
92 6. NEURAL NETWORK LIBRARY

• a one-dimensional array of data errors, which are propagated back-


wards from output neurons back towards the input neurons; these
are used for modifying the values of the weights in the network to
minimize the error of the output neuron activation energies during
training

6.2.1. DESIGNING A SEPARATE LIBRARY FOR TWO-DIMENSIONAL ARRAYS

Scheme does not natively support two-dimensional arrays. We will


want to implement two-dimensional arrays in a separate file module
so that two-dimensional arrays can be used in other programming
projects that do not require neural networks. Our functions for cre-
ating, accessing, and modifying two-dimensional arrays will have the
same form as the built-in Scheme functions for one-dimensional vec-
tors. The interface to our two-dimensional array library will be the
following functions:
(make-2D-array number-of-first-index number-of-second-index)
(2D-array-ref a-2D-array index-O index-l)
(2D-array-set! a-2D-array index-O index-l value)
(2D-array-Iength a-2D-array dimension-number)
The function 2D-array-length requires both a 2D-array data object and
a dimension index. If the dimension index is specified as zero, the size
of the first dimension is returned; if it is specified as one, then the size
of the second dimension is returned. This library is implemented in
Section 6.3.1.

6.2.2. ALGORITHM FOR SUPERVISED LEARNING IN NEURAL NETWORKS

We iisted the data required for maintaining the state of a neural network
object in Section 6.2. Figure 6.1 showed a simple three-layer neural
network with three input neurons (labeled 11,12, and 13), two hidden
layer neurons (labeled HI arid H2), and two output neurons (labeled
01 and 02). In this section we will describe how the ten weights in the
neural network shown in Figure 6.1 can be modified during supervised
learning.
There are two aspects to training a neural network:

1. design the neural network architecture (i.e., specify the number of


neuron layers and the number of neurons per layer)
2. collect a set of training data for use in supervised learning

In this example we already have the network architecture. Table 6.1


shows a set of training data for this network.
6.2. DESIGN OF A NEURAL NETWORK LIBRARY 93

Table 6.1

Input values Output values


000 1 0
001 o 1
010 o 1
o1 1 1 0
100 o1
101 1 0
110 1 0
1 1 1 o 1

The training data in Table 6.1 have a simple pattern: if an even num-
ber of input neuron values are equal to 1, then the first output neuron
is set to 1, otherwise the second output neuron is set to 1. The following
steps are followed to train the weights in this neural network to recog-
nize these input/output patterns (these steps are repeated for each row
in Table 6.1, and then this entire process is repeated several times):

1.Set the input neuron layer (Il, 12, and 13) activation values to the
input values in a row in Table 6.1 and calculate the activation values
for each neuron in the second layer (HI and H2).
2. Using the new activation values for neurons HI and H2, calculate
the activation values for the neurons in the output layer (01,02,
and 03).
3. Calculate the error at each output neuron as the calculated value
minus the target value from the second column in Table 6.1.
4. Based on the errors at the output neurons, modify the weights
connecting the hidden layer neurons with the output-layer neurons.
5. Calculate an estimated error at each hidden layer neuron based
on the hidden-to-output-Iayer weight values and the errors at the
output layer neurons.
6. Based on the estimated errors at the hidden layer neurons, modify
the weights connecting the input layer neurons and the hidden layer
neurons.

This same process works for neural networks without a hidden layer
(i.e., only input and output neurons that are directly connected) and
for networks with multiple hidden neuron layers.
The calculation for new activation values is simple. For example, in
Figure 6.1 the new activation value for the hidden layer neuron HI can
be calculated as

SumOfproducts-for-H1 = It *W11 + I2*W21 + 13*W31


Activation-value-for-HI = Sigmoid(SumOfproducts-for-HI)
94 6. NEURAL NETWORK LIBRARY

The weight W31 refers to the weight value for the connection be-
tween neuron 13 with neuron HI. In our implementation of a neural
network simulator in Section 6.3 we will use one-dimensional arrays
for the sum of products and activation values, and two-dimensional
arrays for the weights connecting any two neuron layers.
The weights connecting the hidden layer neurons and the output
layer neurons can be modified by
Delta-weight-H2-to-Ol = LearningRate * Error-at-Ol
* Calculated-activation-O 1
The backwards-propagated error for hidden layer neuron H2 can be
calculated as
Error-at-H2 = SigmoidP(SumOfProducts-for-H2)
* (Error-at-Ol * W-H2-to-Ol + Error-at-02 * W-H2-to-02)
Once all the errors have been estimated for every hidden layer neu-
ron, the delta-weight values for the input-Iayer-to-hidden-Iayer weights
can be calculated.
In order to make the learning process more efficient, we should re-
member the delta-weight values between training cycles (momentum
values). If the delta-weight values for a specific weight for two consec-
utive training cycles have the same sign so that the weight is changing
in the same "direction," then we increase the rate of learning for that
weight.

6.3 IMPLEMENTATION OF A NEURAL NETWORK LIBRARY


It is very simple to write a neural network simulator that supports only
two-layer networks (i.e., only input and output neurons). It is also easy
to write a simulator that handles a single hidden layer. However, a neu-
ral network simulator that supports momentum and any number of
neuron layers is a fairly complex program.
In reading through the implementation of the simulator in Section
6.3.2, it is important to understand the data arrays associated with each
neuron layer. As we loop through neuron layers, both in the forward
pass of input values propagating towards the output neurons and in the
reverse error propagation and weight modifications, we will use vari-
ables to reference the activation values, sum-of-product calculations,
and errors that are propagated backwards from the output neurons to
calculate changes in the weights for the entire neural network.
6.3. IMPLEMENTATION OF A NEURAL NETWORK LIBRARY 95

6.3.1. TWO-DIMENSIONAL ARRAY LIBRARY

We discussed the public interface for the two-dimensional array library


in Section 6.2.1. These two-dimensional arrays will be used to store
• weights for two connected neuron layers (e.g., input-layer to
hidden-layer weights)
• calculated delta weights for a specified weight array
• the calculated delta weights for the last learning cycle for a specified
weight array (for use in momentum calculations to increase the
learning rate)

Listing 6.1 shows the implementation of the two-dimensional array


library module ARRAY.S.

Listing 6.1
File: array. s

Description: This file contains an implementation


of two-dimensional arrays

Public functions:

(make-2D-array num-first-index num-second-index)


--> a 2D array data structure

(2D-array-ref a-2D-array index-l index-2)


--> array value

(2D-array-set! a-2D-array index-l index-2 new-value)


--> previous value stored in array element

(2D-array-length a-2D-array dimension)


--> returns the first dimension size if
dimension argument equals 0 and returns
the second dimension argument if the
dimension argument equals 1

Copyright 1995, Mark Watson

(define make-2D-array
(lambda (num-1 num-2)
(let «data-size (* num-1 num-2»
(return-value #f»
(set!
return-value
(make-vector (+ 2 data-size) 0.0»
96 6. NEURAL NETWORK LIBRARY

(vector-set! return-value 0 num-1)


(vector-set! return-value 1 num-2)
return-value»)

(define 2D-array-ref
(lambda (an-array index-1 index-2)
(let ((size-1 (vector-ref an-array 0»
(size-2 (vector-ref an-array 1»)
(if (or
« index-1 0)
(>= index-1 size-1»
(begin
(display "illegal first index for 2D-array:")
(display index-1)
(newline)
0)
(if (or
« index-2 0)
(>= index-2 size-2»
(begin
(display "illegal first index for 2D-array:")
(display index-2)
(newline)
0)
;; indices are OK, proceed:
(vector-ref
an-array
(+
2
index-2
(* index-1 size-2»»»»

(define 2D-array-set!
(lambda (an-array index-1 index-2 new-value)
(let ((size-1 (vector-ref an-array 0»
(size-2 (vector-ref an-array 1»)
(if (or
« index-1 0)
(>= index-1 size-1»
(begin
(display "illegal first index for 2D-array:")
(display index-1)
(newline)
0)
(if (or
« index-2 0)
(>= index-2 size-2»
(begin
(display "illegal first index for 2D-array:")
6.3. IMPLEMENTATION OF A NEURAL NETWORK LIBRARY 97

(display index-2)
(newline)
0)
;; indices are OK, proceed:
(let ((old-value
(vector-ref
an-array
(+
2
index-2
(* index-l size-2)))))
(vector-set!
an-array
(+
2
index-2
(* index-l size-2))
new-value)
old-value))))))

(define 2D-array-Iength
(lambda (a-2D-array dimension)
(vector-ref a-2D-array dimension)))

Test code:

(define a)
(set! a (make-2D-array 3 4))
(pp a)

(define test
(lambda 0
(do ((i 0 (+ i 1)))
((equal? i 3))
(do ((j 0 (+ j 1)))
((equal? j 4))
(2D-array-set! a i j (+ i (* j 1000)))))
(do ((i 0 (+ i 1)))
((equal? i 3))
(do ((j 0 (+ j 1)))
((equal? j 4))
(display "a [")
(display i)
(display :OJ [")
(display j)
(display "] = ")
(display (2D-array-ref a i j))
(newline)))))
98 6. NEURAL NETWORK LIBRARY

(test)
(2D-array-ref a 4 5)

(2D-array-length a 0)
(2D-array-length a 1)

The two-dimensional array library is designed to work like the


Scheme vector functions. Table 6.2 shows the correspondence between
the vector and two-dimensional array functions. Since the naming con-
vention for the two-dimensional array library functions is analogous to
that for the built-in vector functions, it is easy for users of the library
to remember the interface.

Table 6.2
20 array functions vector functions
make-2D-array make-vector
2D-array-ref vector-ref
2D-array-set! vector-set!
2D-array-Iength vector-length

The function make-20-array constructs a new two-dimensional array


of a specified size. Users of the two-dimensional an-ay library need to
understand only the public interface for the library, not the internal
implementation. For example. the function make-20-array allocates a
Scheme vector to hold the following information:
• First-dimension size of the array
• Second-dimension size of the array
• Data for the array

If we want to change the internal format for a two-dimensional array


object. users of the library will not be affected by the change unless we
change the public interface shown in Table 6.1.
The functions 20-array-ref and 20-array-set! check the values of their
arguments to make sure that the requested array indices are within
the bounds of the array. Functions 20-array-ref and 20-array-set! need to
convert the two input indices to the index into the Scheme vector used
to store the data for the two-dimensional array. The function 20-array-
length is called with an argument 0 or 1; calling with the argument of
o returns the size of the first dimension of the array; calling with the
argument value of 1 returns the size of the second dimension of the
array.
The test code at the bottom of file ARRAY.S sets the value of global
variable a to a new 3-by-4 array, and demonstrates the use of the 20-
6.3. IMPLEMENTATION OF A NEURAL NETWORK LIBRARY 99

array-set! and 2D-array-ref functions. You must evaluate the following


expressions before running the function test:
(define a)
(set! a (make-2D-array 3 4))

6.3.2. NEURAL NETWORK LIBRARY

The complete implementation of the neural network library is shown in


Listing 6.2. This library optionally uses the graphics library developed
in Section 4.3 to plot activation and weight values during supervised
learning. The function DeltaLearn takes an optional argument that en-
ables these plots during training. There are four test functions at the
end of Listing 6.2:
1. test2 - creates and trains a two-layer network
2. test3 - creates and trains a three-layer network
3. test4 - creates and trains a four-layer network (two hidden layers)
4. test-gr - creates and trains a three-layer network with graphics
output

I recommend loading the files ARRAYS, GRAPH.S, and NN.S and


running these test functions while reading through the program in List-
ing 6.2. The individual functions shown in Listing 6.2 are discussed in
detail after the program listing.
In Listing 6.2, the function NewDeltaNetwork is called with a list of
integer values, each of which specifies the number of neurons in a simu-
lated layer of neurons. For example, the list (120 7 5) would indicate that
NewDeltaNetwork will create a neural network with 120 input layer neu-
rons, 7 hidden layer neurons, and 5 output layer neurons. The function
NewDeltaNetwork returns a list of arrays that maintain the state of the
network. Note that the entire state of the neural network is maintained
in the data structure returned by function NewDeltaNetwork.
The function DeltaLearn is called with two required arguments and an
optional argument that indicates that plots are to be generated during
training, using the graphics library developed in Section 4.3. The first
argument passed to DeltaLearn is the network description list that was
returned as the value of function NewDeltaNetwork. The second required
argument is a list of training data.
The function DeltaRecall is used to generate network outputs from
a set of network inputs and a trained neural network object. The re-
turn value of function DeltaRecall is a list of generated output activation
values.
The four test functions listed at the end of Listing 6.2 show how to
create a new network object with NewDeltaNetwork, perform supervised
100 6. NEURAL NETWORK LIBRARY

learning with function DeltaLearn, and use the trained neural network
object with function DeltaRecall.
After a neural network is trained you can use function Writ-
eDeltaNetwork to save the network data to a disk file. The function
ReadDeltaNetwork reads a network data file from disk and generates a
new network object that can be used with function DeltaRecall in other
programs.

Listing 6.2
File NN.s

Description: This file contains a library for Back Propagation


(also known as "Delta Rule") neural networks.
Any number of neuron layers (with momentum) are
supported.

This software can be used in compiled form without


restriction. The source code can not be distributed
without the author's permission.

Copyright 1995 by Mark Watson

MODULE DEPENDENCIES:

Load array.s (or array.com) for 2D array support


(reqUired)

Load graph.s (or graph. com) for graphics support


(optional)

Externally (callable) functions in this file:

(NewDeltaNetwork sizeList)
Args: sizeList list of sizes of slabs. This also defines
the number of slabs in the network.
(e. g. , ' (10 5 4) ==> a 3-slab network
with 10 input neurons, 5 hidden
neurons, and 4 output neurons).

Returned value = a list describing the network:


(nLayers sizeList
(activation-array [1] .. activation-array[nLayers])
(sum-of-products[2] .. sum-of-products[nLayers[nLayers])
(weight-array [2] .. weight-array[nLayers])
(delta-weight-array[2] .. delta-weight-array[nLayers])
(back-prop-error[2] .. back-prop-error[nLayers]))
6.3. IMPLEMENTATION OF A NEURAL NETWORK LIBRARY 101

(old-delta-weights[2] .. for momentum term

(DeltaLearn networkList trainingList . plotFlag)


Args: networkList list returned from function
NewDeltaNetwork
trainingList a list of lists of training exemplars.
For example, a list might be:
«((0 1) C1 0)) ; first exemplar
(1 0) (0 1))) ; second exemplar
Note: the inner sub-lists can
also be arrays.
nlterations number of complete training iterations

Returned value average error at output neurons for last


training cycle.

(DeltaRecall networkList inputList)


Args: networkList list returned from function
'NewDeltaNetwork'
inputList list OR array of input activation values

Returned value = list of output neuron values

(DeltaPlot networkList) ==> plots a network. Must call


'(open-gr) first.

(WriteDeltaNetwork fileName networkList) ==> saves a


network to disk

(ReadDeltaNetwork fileName) ==> returns a network list

;; Define default learning rates for each layer of neurons:

(define defaultEidaList '(0.5 0.4 0.3 0.2 0.08 0.07))

;; Define the default noise to add to each input neuron:

(define *delta-default-input-noise-value* 0.08)


(define *delta-rule-debug-flag* 'C))

;; Local utility: generate floating point random numbers:

(define frandom
(lambda (lower upper)
(+ lower
(random (- upper lower)))))

; Create a new delta network:


102 6. NEURAL NETWORK LIBRARY

alpha coefficient for new weight change


beta coefficient for adding in last weight change

(define alpha 0.2)


(define beta 0.8)

(define eidaList '())

; (NewDeltaNetwork '(2 2))

(define NewDeltaNetwork
(lambda (sizeList)
(let «numLayers (length sizeList))
(w-list '0) weights
(dw-list '0) delta weights
(old-dw-list '()) old delta weights for
momentum terms
(a-list' 0) activation values
(s-list '0) sum of products
(d-list '0)) back propagated deltas

(set! eidaList defaultEidaList)

; Initialize storage for activation energy for all slabs:

(set! a-list
(map
(lambda (size) (make-vector size 0.0))
sizeList))

; Initialize storage for sum of products arrays:

(set! s-list
(map
(lambda (size) (make-vector size 0.0))
(cdr sizeList)))

; Initialize storage for delta arrays:

(set! d-list
(map
(lambda (size) (make-vector size 0.0))
(cdr sizeList)))
6.3. IMPLEMENTATION OF A NEURAL NETWORK LIBRARY 103

; Initialize storage for the weights:

(do «i 0 (+ i 1»)
«equal? i (- numLayers 1»)
(set!
w-list
(cons
(list
(list-ref sizeList i)
(list-ref sizeList (+ i 1»)
w-list) »

(set! w-list
(map
(lambda (size)
(make-2D-array (car size) (cadr size»)
(reverse w-list»)

; Initialize the storage for delta weights:

(do «i 0 (+ i 1»)
«equal? i (- numLayers 1»)
(set!
dw-list
(cons
(list (list-ref sizeList i)
(list-ref sizeList (+ i 1») dw-list»)
(set! dw-list
(map
(lambda (size)
(make-2D-array (car size) (cadr size»)
(reverse dw-list»)

; Initialize the storage for old delta weights:

(do «i 0 (+ i 1»)
«equal? i (- numLayers 1»)
(set!
old-dw-list
(cons
(list (list-ref sizeList i)
(list-ref sizeList (+ i 1») old-dw-list»)
(set! old-dw-list
(map
(lambda (size)
(make-2D-array (car size) (cadr size»)
104 6. NEURAL NETWORK LIBRARY

(reverse old-dw-list)))

Initialize values for all activations:

(map
(lambda (x)
(let «num (vector-length x)))
(do «n 0 (+ n 1)))
«equal? n num))
(vector-set! x n (frandom 0.01 0.1)))))
a-list)

Initialize values for all weights:

(map
(lambda (x)
(let «numI (2D-array-Iength x 0))
(numJ (2D-array-Iength xl)))
(do «j 0 (+ j 1)))
«equal? j numJ))
(do «i 0 (+ i 1)))
«equal? i numI))
(2D-array-set! x i j (frandom -0.5 0.5))))))
w-list)
(list numLayers sizeList a-list s-list w-list dw-list
d-list old-dw-list alpha beta))))

Utility function for training a delta rule neural network.


The first argument is a network definition (as returned from
NewDeltaNetwork), the second argument is a list of training
data cases (see the example test functions at the end of this
file for examples, the third (optional) argument is a flag for
automatic plotting of the state of the network. Call
DeltaLearn with a third argument equal to #t for plotiing.

(define (DeltaLearn netList trainList . plotFlag)


(let «nLayers (car netList))
(sizeList (cadr netList))
(activationList (caddr netList))
(sumOfProductsList (car (cdddr netList)))
(weightList (cadr (cdddr netList)))
(deltaWeightList (caddr (cdddr netList)))
(deltaList lcadddr (cdddr netList)))
(oldDeltaWeightList (cadddr (cdddr (cdr netList))))
(alpha (cadddr (cdddr (cddr netList))))
6.3. IMPLEMENTATION OF A NEURAL NETWORK LIBRARY 105

(beta (cadddr (cdddr (cdddr netList))))


(inputs '0)
(targetOutputs 'C))
(iDimension 'C))
(jDimension '0)
(iActivationVector 'C))
(jActivationVector 'C))
(weightArray 'C))
(sumOfProductsArray 'C))
(iDeltaVector 'C))
(jDeltaVector 'C))
(deltaWeightArray 'C))
(oldDeltaWeightArray 'C))
(sum' 0)
(iSumOfProductsArray 'C))
(error '0)
(outputError 0)
(delta' 0)
(eida '0)
(inputNoise 0)
(n 0))

; Zero out deltas:

(do ((n 0 (+ n 1)))


((equal? n (- nLayers 1)))
(let* ((dw (list-ref deltaList n))
(len1 (vector-length dw)))
(do((i 0 (+ i 1)))
((equal? i len1))
(vector-set! dw i 0.0))))

; Zero out delta weights:

(do ((n 0 (+ n 1)))


((equal? n (- nLayers 1)))
(let* ((dw (list-ref deltaWeightList n))
(len1 (2D-array-length dw 0))
(len2 (2D-array-length dw 1)))
(do ((i 0 (+ i 1)))
((equal? i len1))
(do ((j 0 (+ j 1)))
((equal? j len2))
(2D-array-set! dw i j 0.0)))))

(set! inputNoise *delta-default-input-noise-value*)


106 6. NEURAL NElWORK LIBRARY

Main loop on training examples:

(do «tl-list trainList (cdr tl-list)))


«null? tl-list))
(let «tl (car tl-list)))
(set! inputs (car tl))
(set! targetOutputs (cadr tl))

(if *delta-rule-debug-flag*
(display (list "Current targets:" targetOutputs)))

get the size of the input slab:


(set! iDimension (car sizeList))

; get array of input activations:


(set! iActivationVector (car activationList))

; copy training inputs to input slab:


(do «i 0 (+ i 1)))
«equal? i iDimension))
(vector-set!
iActivationVector
i
(+
(list-ref inputs i)
(frandom (- inputNoise) inputNoise))))

; Propagate activation through all of the slabs:

; update layer i to layer flowing to layer j


(do «n-1 0 (+ n-1 1)))
«equal? n-1 (- nLayers 1)))
(set! n (+ n-1 1))
; get the size of the j'th layer:
(set!
jDimension
(list-ref sizeList n))
; activation array for slab j:
(set!
jActivationVector
(list-ref activationList n))
(set! weightArray (list-ref weightList n-l))
(set!
sumOfProductsArray
(list-ref sumOfProductsList n-1))
; process each neuron in slab j:
6.3. IMPLEMENTATION OF A NEURAL NETWORK LIBRARY 107

(do ((j 0 (+ j 1)))


((equal? j jDimension))
(set! sum 0.0) ; init sum of products to zero
; to get activation from each neuron in
; previous slab:
(do ((i 0 (+ i 1)))
((equal? i iDimension))
(set!
sum
(+
sum
(* (2D-array-ref weightArray i j)
(vector-ref iActivationVector i)))))
save sum of products:
(vector-set! sumOfProductsArray j sum)
(vector-set! jActivationVector j (Sigmoid sum)))
reset index for next slab pair:
(set! iDimension jDimension)
(set! iActivationVector jActivationVector))

Activation is spread through the network


and sum of products calculated.
Now modify the weights in the network
using back error propagation. Start
by calculating the error signal for each
neuron in the output layer:

; size of last layer:


(set! jDimension (list-ref sizeList (- nLayers 1)))
(set!
jActivationVector
(list-ref activationList (- nLayers 1)))
(set! jDeltaVector (list-ref deltaList (- nLayers 2)))
(set!
sumOfProductsArray
(list-ref sumOfProductsList (- nLayers 2)))
(set! outputError 0)
(do ((j 0 (+ j 1)))
((equal? j jDimension))
(set!
delta
(-
(list-ref targetOutputs j)
(vector-ref jActivationVector j)))
(set! outputError (+ outputError (abs delta)))
(vector-set!
jDeltaVector
j
(+
108 6. NEURAL NETWORK LIBRARY

(vector-ref jDeltaVector j)
(*
delta
(dSigmoid (vector-ref sumOfProductsArray j))))))

; Now calculate the backpropagated error signal


; for all hidden slabs:

(do «nn 0 (+ nn 1)))


«equal? nn (- nLayers 2)))
(set! n (- nLayers 3 nn))
(set! iDimension (list-ref sizeList (+ n 1)))
(set!
iSumOfProductsArray
(list-ref sumOfProductsList n))
(set! iDeltaVector (list-ref deltaList n))
(do «i 0 (+ i 1)))
«equal? i iDimension))
(vector-set! iDeltaVector i 0.0))
(set! weightArray (list-ref weightList (+ n 1)))
(do «i 0 (+ i 1)))
«equal? i iDimension))
(set! error 0.0)
(do «j 0 (+ j 1)))
«equal? j jDimension))
(set!
error
(+
error
(*
(vector-ref jDeltaVector j)
(2D-array-ref weightArray i j)))))
(vector-set!
iDeltaVector
i
(+
(vector-ref iDeltaVector i)
(*
error
(dSigmoid
(vector-ref iSumOfProductsArray i))))))
(set! jDimension iDimension)
(set! jDeltaVector iDeltaVector))

; Update all delta weights in the network:

(set! iDimension (car sizeList))


(do «n 0 (+ n 1)))
6.3. IMPLEMENTATION OF A NEURAL NETWORK LIBRARY 109

((equal? n (- nLayers 1»)


(set! iActivationVector (list-ref activationList n»
(set! jDimension (list-ref sizeList (+ n 1»)
(set! jDeltaVector (list-ref deltaList n»
(set! deltaWeightArray (list-ref deltaWeightList n»
(set! weightArray (list-ref weightList n»
(set! eida (list-ref eidaList n»
;; new stuff:
(set! iDimension (vector-length iActivationVector»
(set! jDimension (vector-length jDeltaVector»

(do ((j 0 (+ j 1»)


((equal? j jDimension»
(do ((i 0 (+ i 1»)
((equal? i iDimension»
(set!
delta
(*
eida
(vector-ref jDeltaVector j)
(vector-ref iActivationVector i»)
(2D-array-set!
DeltaWeightArray i j
(+
(2D-array-~ef DeltaWeightArray i j)
delta»» ; remember delta weight change

(set! iDimension jDimension»

(if (equal? plotFlag '(#t»


(DeltaPlot netList»)

; Update all weights in the network:

(set! iDimension (car sizeList»


(do ((n 0 (+ n 1»)
((equal? n (- nLayers 1»)
(set! iActivationVector (list-ref activationList n»
(set! jDimension (list-ref sizeList (+ n 1»)
(set! jDeltaVector (list-ref deltaList n»
(set! deltaWeightArray (list-ref deltaWeightList n»
(set!
oldDeltaWeightArray
(list-ref oldDeltaWeightList n»
(set! weightArray (list-ref weightList n»
(do ((j 0 (+ j 1»)
((equal? j jDimension»
(do ((i 0 (+ i 1»)
110 6. NEURAL NETWORK LIBRARY

«equal? i iDimension))
(2D-array-set!
weightArray i j
(+ (2D-array-ref weightArray i j)
(*
alpha
(2D-array-ref deltaWeightArray i j))
(*
beta
(2D-array-ref oldDeltaWeightArray i j))))
save current delta weights for next
momentum term:
(2D-array-set!
oldDeltaWeightArray 1 J
(2D-array-ref deltaWeightArray i j))))
(set! iDimension jDimension)))
•• return the average error of the output neurons:
(/ outputError jDimension)))

Utility for using a trained neural network in the


recall mode. The first argument to this function
is a network definition (as returned from NewDeltaNetwork)
and the second argument is a list of input neuron
activation values to drive through the network.

(define DeltaRecall
(lambda (net List inputs)
(let «nLayers (car netList))
(sizeList (cadr netList))
(activationList (caddr netList))
(weightList (cadr (cdddr netList)))
(iDimension 'C))
(jDimension 'C))
(iActivationVector 'C))
(jActivationVector .())
(n • 0)
(weightArray .())
(returnList '0)
(sum '0))
get the size of the input slab:
(set! iDimension (car sizeList))
; get array of input activations:
(set! iActivationVector (car activationList))
; copy training inputs to input slab:
(do «i 0 (+ i 1)))
«equal? i iDimension))
(vector-set! iActivationVector i (list-ref inputs i)))
update layer j to layer i
6.3. IMPLEMENTATION OF A NEURAL NETWORK LIBRARY 111

(do «n-1 0 (+ n-1 1)))


«equal? n-1 (- nLayers 1)))
(set! n (+ n-1 1))
; get the size of the j'th layer:
(set! jDimension (list-ref sizeList n))
; activation array for slab j:
(set! jActivationVector (list-ref activationList n))
(set! weightArray (list-ref weightList n-1))
; process each neuron in slab j:
(do «j 0 (+ j 1)))
«equal? j jDimension))
(set! sum 0.0) ; init sum of products to zero
; get activation from each neuron in previous slab:
(do «i 0 (+ i 1)))
«equal? i iDimension))
(set!
sum
(+
sum
(*
(2D-array-ref weightArray i j)
(vector-ref iActivationVector i)))))
(if *delta-rule-debug-flag*
(display (list "sum=" sum)))
(vector-set! jActivationVector j (Sigmoid sum)))
get ready for next slab pair:
(set! iDimension jDimension)
(set! iActivationVector jActivationVector))
(do «j 0 (+ j 1)))
«equal? j jDimension))
(set!
returnList
(append
returnList
(list (vector-ref jActivationVector j)))))
returnList)))

Utilities to plot a network

(define plotActivations
(lambda (title x y data dmin dmax)
(let «size (vector-length data))
(ypos 0) (xpos x))
(plot-string x (- y 60) title)
(do «i 0 (+ i 1)))
«equal? i size))
(if « size 20)
112 6. NEURAL NETWORK LIBRARY

(begin
(set! ypos y)
(set! xpos (+ x (* i 29))))
(if « i (/ size 2))
(begin
(set! ypos (- y 7))
(set! xpos (+ x (* i 29))))
(begin
(set! ypos (+ y 2))
(set! xpos (+ x (* (- i (/ size 2)) 29))))))
(plot-fill-rect-gray-scale
(truncate xpos)
(truncate ypos)
28 28
(truncate
(*
U
(- (vector-ref data i) dmin)
(- dmax dmin))
255)))))))

(define plotWeights
(lambda (title x y data dmin dmax deltaWeights)
(let «Xsize (2D-array-length data 0))
(YSize (2D-array-length data 1)))
; don't try to plot very large weight sets:
(if « (* Xsize Ysize) 200)
(begin
(plot-string (+ x 20) (- Y 60) title)
(do «i 0 (+ i 1)))
«equal? i xSize))
(do «j 0 (+ j 1)))
«equal? j Ysize))
(plot-fill-rect-gray-scale
(+ x (* i 29)) (+ y (* j 29)) 28 28
(truncate
(*
U
(- (2D-array-ref data i j) dmin)
(- dmax dmin)) 255)))
(plot-fill-rect-gray-scale
(+ (* Xsize 28) 30 x (* i 29))
(+ y (* j 29))
28 28
(truncate
(*
U
(- (2D-array-ref deltaWeights i j) -.05)
(- .05 -.05))
6.3. IMPLEMENTATION OF A NEURAL NETWORK LIBRARY 113

255))))))))))

(define DeltaPlot
(lambda (net List)
(let «nLayers (car netList))
(sizeList (cadr netList))
(activationList (caddr netList))
(sumOfProductsList (car (cdddr netList)))
(weightList (cadr (cdddr netList)))
(deltaWeightList (caddr (cdddr netList)))
(minScale -0.3)
(maxScale 0.3)
(n 0)
(y-start 0))
(plot-string 100 960 "Delta Network")
(set! y-start 100)
(plotActivations "slabl"
100
y-start
(list-ref ActivationList 0)
-0.5
0.5)
(do «n-l 0 (+ n-l 1)))
«equal? n-l (- nLayers 1)))
(set! n (+ n-l 1))
(if (equal? n (- nLayers 1))
(begin
(set! minScale -0.2)
(set! maxScale 0.2))) ; scale up output display
(plotActivations
(list-ref '("slab1" "slab2" "slab3" "slab4" "slab5") n)
100 ; x location for subplot
(+ y-start (* n 400)) ; y location for subplot
; data to plot as gray scale:
(list-ref ActivationList n)
minScale
maxScale))
(if « nLayers 4)
(begin
(set! y-start -140)
(do «n 0 (+ n 1)))
«equal? n (- nLayers 1)))
(set! y-start (+ y-start 310))
(plot-string 100 y-start "Weights and Delta Weights")
(set! y-start (+ y-start 90))
(plotWeights
(list-ref
'("slabl -> slab2" "slab2 -> slab3"
"slab3 -> slab4")
114 6. NEURAL NETWORK LIBRARY

n)
100 y-start ; x,y position of subplot
(list-ref WeightList n)
-1.0 1.0
(list-ref deltaWeightList n))))))))

Calculate Sigmoid and derivative of Sigmoid functions:

(define Sigmoid
(lambda (x)
(/ 1.0 (+ 1.0 (exp (- x))))))

(define dSigmoid
(lambda (x)
(let ((temp (Sigmoid x)))
(* temp (- 1.0 temp)))))

; Save a Delta Network to a disk file:

(define WriteDeltaNetwork
(lambda (fileName netList)
(let ((fileStream (open-output-file fileName))
(nLayers (car netList))
(sizeList (cadr netList))
(activationList (caddr netList))
(weightList (cadr (cdddr netList)))
(deltaWeightList (caddr (cdddr netList)))
(oldDeltaWeightList (car (cddddr (cdddr netList))))
(alpha (cadr (cddddr (cdddr netList))))
(beta (caddr (cddddr (cdddr netList)))))

; Write out header:

(write-string";; number of layers" fileStream)


(newline fileStream)
(write nLayers fileStream)
(newline fileStream)
(write-string It;; number of neurons in each layer" fileStream)
(newline fileStream)
(write sizeList fileStream)
(newline fileStream)

; Write out activations:


6.3. IMPLEMENTATION OF A NEURAL NETWORK LIBRARY 115

(write-string ";; activation values" fileStream)


(newline fileStream)

(do ((n 0 (+ n 1)))


((equal? n (- nLayers 1)))
(do ((i 0 (+ i 1)))
((equal? i (list-ref sizeList n)))
(write
(vector-ref
(list-ref activationList n) i)
fileStream)
(newline fileStream)))

; Write out weights:

(write-string ";; weights" fileStream)


(newline fileStream)

(do ((n 0 (+ n 1)))


((equal? n (- nLayers 1)))
(let ((w (list-ref weightList n)))
(do ((i 0 (+ i 1)))
((equal? i (2D-array-length w 0)))
(do ((j 0 (+ j 1)))
((equal? j (2D-array-length w 1)))
(write (2D-array-ref w i j) fileStream)
(newline fileStream)))))

; Write out delta weights:

(write-string ";; delta weights" fileStream)


(newline fileStream)

(do ((n 0 (+ n 1)))


((equal? n (- nLayers 1)))
(let ((w (list-ref deltaWeightList n)))
(do ((i 0 (+ i 1)))
((equal? i (2D-array-length w 0)))
(do ((j 0 (+ j 1)))
((equal? j (2D-array-length w 1)))
(write (2D-array-ref w i j) fileStream)
(newline fileStream)))))

; Write out old delta weights:


116 6. NEURAL NETWORK LIBRARY

(write-string ";; old delta weights" fileStream)


(newline fileStream)

(do «n 0 (+ n 1)))
«equal? n (- nLayers 1)))
(let «w (list-ref oldDeltaWeightList n)))
(do «i 0 (+ i 1)))
(equal? i (2D-array-length w 0)))
(do «(j 0 (+ j 1)))
(equal? j (2D-array-length wi)))
(write (2D-array-ref w i j) fileStream)
(newline fileStream)))))

; Write alpha, beta terms (used for momentum):

(write-string ";; alpha" fileStream)


(newline fileStream)

(write alpha fileStream)


(newline fileStream)

(write-string";; beta" fileStream)


(newline fileStream)

(write beta fileStream)


(newline fileStream)

(close-output-port fileStream))))

; Read a delta network from a disk file:

(define ReadDeltaNetwork
(lambda (fileName)
(let «fileStream (open-input-file fileName))
(numLayers '0)
(sizeList '0)
(a-list '0)
(s-list '0)
(w-list '0)
(dw-list '0)
(old-dw-list '())
(d-list '0)
(alpha 0.0)
(beta 0.0))
6.3. IMPLEMENTATION OF A NEURAL NETWORK LIBRARY 117

; Read in header:

(set! numLayers (read fileStream»


(set! sizeList (read fileStream»

; Allocate array storage:

(set! a-list
(map
(lambda (size) (make-vector size 0.0»
sizeList»
(set! s-list
(map
(lambda (size) (make-vector size 0.0»
(cdr sizeList»)
(set! d-list
(map
(lambda (size) (make-ve~tor size 0.0»
(cdr sizeList»)
(do ((i a (+ i 1»)
((equal? i (- numLayers 1»)
(set!
w-list
(cons
(list (list-ref sizeList i)
(list-ref sizeList (+ i 1») w-list»)
(set! w-list
(map
(lambda (size) (make-2D-array (car size) (cadr size»)
(reverse w-list»)

(do ((i 0 (+ i 1»)


((equal? i (- numLayers 1»)
(set!
dw-list
(cons
(list
(list-ref sizeList i)
(list-ref sizeList (+ i 1»)
dw-list»)

(do ((i 0 (+ i 1»)


((equal? i (- numLayers 1»)
(set!
old-dw-list
(cons
(list
118 6. NEURAL NETWORK LIBRARY

(list-ref sizeList i)
(list-ref sizeList (+ i 1»)
old-dY-list»)

(set! dy-list
(map
(lambda (size) (make-2D-array (car size) (cadr size»)
(reverse dY-list»)

(set! old-dY-list
(map
(lambda (size) (make-2D-array (car size) (cadr size»)
(reverse old-dY-list»)

; Read in activations:

(do «n 0 (+ n 1»)
«equal? n (- numLayers 1»)
(do «i 0 (+ i 1)))
«equal? i (list-ref sizeList n»)
(vector-set!
(list-ref a-list n) i (read fileStream»)))

; Read in Yeights:

(do «n 0 (+ n 1)))
«equal? n (- numLayers 1)))
(let «y (list-ref Y-list n)))
(do «i 0 (+ i 1»))
«equal? i (2D-array-length yO»)
(do «j 0 (+ j 1))
«equal? j (2D-array-length Y 1))
(2D-array-set! Y i j (read fileStream»»»

; Read in delta yeights:

(do «n 0 (+ n 1))
«equal? n (- numLayers 1)))
(let «y (list-ref dY-list n»)
(do «i 0 (+ i 1)))
«equal? i (2D-array-length yO»)
(do «j 0 (+ j 1))
«equal? j (2D-array-length Y 1»)
(2D-array-set! Y i j (read fileStream»))))
6.3. IMPLEMENTATION OF A NEURAL NETWORK LIBRARY 119

; Read in old delta weights:

(do ((n 0 (+ n 1)))


((equal? n (- numLayers 1)))
(let ((w (list-ref old-dw-list n)))
(do ((i 0 (+ i 1)))
((equal? i (2D-array-Iength w 0)))
(do ((j 0 (+ j 1)))
((equal? j (2D-array-Iength w 1)))
(2D-array-set! w i j (read fileStream))))))

(set! alpha (read fileStream))


(set! beta (read fileStream))

(close-input-port fileStream)
(list numLayers sizeList a-list s-list
w-list dw-list d-list
old-dw-list alpha beta))))

Throwaway test functions for two, three and


four layer networks:

(define (test2 . old-network)


(let ((RMSerror 0.0))
(if (null? old-network)
(begin
(display "Creating a new network")
(newline)
; specify a two layer network (2x2)
(set! old-network (newdeltanetwork '(2 2))))
(begin
(display "Using old network:")
(newline)
(set! old-network (car old-network))
(pp old-network)
(newline)))
(do ((ii 0 (+ ii 1)))
((equal? ii 20))
(set!
RMSerror
(deltalearn
old-network
'(((1 0) (0 1))
((0 1) (1 0)))))
(if (equal? (modulo ii 5) 0) ;; print out every 5 cycles
(begin
(display" .... training cycle \#")
120 6. NEURAL NETWORK LIBRARY

(display ii)
(display " RMS error II)
(display RMSerror)
(newline))))
old-network))

(define save-net)
(set! save-net (test2))
(pp save-net)
(test2 save-net) ;; try restarting the learning process

(define (test3 . old-network)


(let ((RMSerror 0.0))
(if (null? old-network)
(begin
(display "Creating a new network")
(newline)
; specify a two layer network (2x2)
(set! old-network (newdeltanetwork '(5 4 5))))
(begin
(display "Using old network:")
(newline)
(set! old-network (car old-network))
(pp old-network)
(newline)))
(do ((ii 0 (+ ii 1)))
((equal? ii 1000))
(set!
RMSerror
(deltalearn
old-network
'(((1 0 0 0 0) (0 1 000))
((0 1 0 0 0) (0 0 1 0 0))
((0 0 1 0 0) (0 0 0 1 0))
((0 0 0 1 0) (0 0 0 0 1))
((0 0 0 0 1) (1 0 0 0 0)))))
(if (equal? (modulo ii 5) 0) ;; print out every 5 cycles
(begin
(display" .... training cycle \#")
(display ii)
(display " RMS error II)
(display RMSerror)
(newline))) )
temp) )

(test3)

(define (test4 . old-network)


(let ((RMSerror 0.0))
6.3. IMPLEMENTATION OF A NEURAL NETWORK LIBRARY 121

(if (null? old-network)


(begin
(display "Creating a new network")
(newline)
; specify a two layer network (2x2)
(set! old-network (newdeltanetwork '(4 5 5 4))))
(begin
(display "Using old network:")
(newline)
(set! old-network (car old-network))
(pp old-network)
(newline)))
(do «ii 0 (+ ii 1)))
«equal? ii 1200))
(set!
RMSerror
(deltalearn
old-network
'«(1000) (0100))
«0 1 0 0) (0 0 1 0))
«0 0 1 0) (0 0 0 1))
«0 0 0 1) (1 0 0 0)))))
(if (equal? (modulo ii 5) 0) ;; print out every 5 cycles
(begin
(display " .... training cycle \#")
(display ii)
(display " RMS error II)
(display RMSerror)
(newline))))))
; (test4)

Throwaway test functions for graphics support:

(define (test-gr . restart)


(let «plotFlag 'C))
(temp #f))
(open-gr)
(clear-plot)
(let «RMSerror 0.0))
(if (null? restart)
(set! temp (newdeltanetwork '(5 4 5)))
(set! temp (car restart)))
(do «ii 0 (+ ii 1)))
«equal? ii 500))
(set!
RMSerror
(deltalearn
122 6. NEURAL NETWORK LIBRARY

temp
'«(1 0 0 0 0) (0 1 000»
«0 1 0 0 0) (0 0 1 0 0»
«0 0 1 0 0) (0 0 0 1 0»
«0 0 0 1 0) (0 0 0 0 1»
«0 0 0 0 1) (1 0 0 0 0»)
plotFlag»
;; print out every 5 cycles:
(if (equal? (modulo ii 10) 0)
(begin
(set! plotFlag #t)
(display " .... training cycle \#")
(display ii)
(display " RMS error = II)
(display RMSerror)
(newline»
(set! plotFlag #f»»
temp» ;; return neural network object for use in restarting

(open-gr) ;; just do this once


(test-gr)

Throwaway test code for testing save/load options:

(define save-net)
(set! save-net (test2»
,. (WriteDeltaNetwork "test.net" save-net)
(set! save-net (ReadDeltaNetwork "test.net"»
(pp save-net)
(test2 save-net) ;; try restarting the learning process

The variable defaultEldaLlst in Listing 6.2 sets the default values for
the neural network learning rates. Each time function DeltaLearn is
called, the weights connecting each neuron layer are modified to re-
duce the error for a set of training data. The learning rate values in the
variable defaultEldaLlst specify how fast each set of weights is modified.
From experience I have learned that it is better to use relatively high
learning rates for weights connected to the input neurons, and lower
learning rates for weights connected to the output neurons. There are
six values in the list assigned to the variable defaultEldaLlst: the first
value is the learning rate for the weights connecting the input neurons
to the first hidden neuron layer; the second value specifies the learning
rate for the weights connecting the first hidden layer to the second hid-
den layer (or the output neuron layer if there is only one hidden neuron
layer defined in the call to function NewDeltaNetwork), etc.
6.3. IMPLEMENTATION OF A NEURAL NETWORK LIBRARY 123

The function frandom is used to generate random numbers within a


specified range. Function frandom is useful for assigning small random
values to weights when one is constructing a new delta rule neural
network with function NewDeltaNetwork. Function frandom is also used
to add small random numbers to neural network training data.
The variable *delta-default-input-nolse-value* defines the range of ran-
dom values to add to neural network training data. The function
DeltaLearn returns an accumulated error for training a network with
a training data set. Usually the return value from function DeltaLearn
gets smaller as DeltaLearn is called repetitively with the same training
data set (often this requires thousands, or even hundreds of thousands,
of calls to DeltaLearn for moderately large or large neural networks
with large training data sets). I use a trick to train complex networks
that do not converge to a small error for a training data set: I manu-
ally set the variable *delta-default-input-noise-value* to a relatively large
value (e.g., 0.2) and train the network with a set of training data.
I repeat this process while manually reducing the value assigned to
*delta-default-input-noise-value*.
The function NewDeltaNetwork is long, but simple. The following vari-
ables are defined, and a list of these variables is returned as the value
of a function call to NewDeltaNetwork:

• numlayers: the number of neuron layers in the network


• w-Iist: a list of two-dimensional arrays created by calling function
make-2D-array. The size of each two-dimensional array in this list
is determined by the number of neurons in each of the two layers
connected by the weights represented by a single two-dimensional
array
• dw-Iist: a list of delta weight values used during training. This list
contains a new set of two-dimensional arrays that are of the same
sizes as the two-dimensional arrays in the variable w-Iist
• old-dw-Iist: used to copy the weights in dw-Iist for a previous train-
ing cycle; using these so-called "momentum" terms can drastically
decrease training time
• a-list: a list of Scheme vectors, one vector for each neuron in a layer
• s-Iist: temporary working storage for calculating sum-of-products
terms used for both network training and recall
• d-Iist: working storage for calculating back propagated deltas for
network training

The function DeltaLearn is the most complex function in Listing 6.2.


The algorithm for training a backwards error propagation (or delta)
network was discussed in Section 6.2. The complexity of function
DeltaLearn comes from the requirement to handle any number of neu-
ron layers (although 5 or 6 layers is probably the maximum that you
124 6. NEURAL NETWORK LIBRARY

would ever use; a four-layer network is capable of learning arbitrary


mappings). When training a network with a training data set consist-
ing of many individual input, and matching target output, patterns,
we want to accumulate changes to the weights in the network over
all training examples. The weights in the network are updated after
seeing all training data sets. Function DeltaLearn uses the local vari-
able deltaWeightList to reference the list of two-dimensional arrays for
accumulating changes to the weights; this list of weight deltas was allo-
cated in the function NewDeltaNetwork (using variable dw-list). Function
DeltaLearn sets all of theses delta weight values for the entire network to
zero, then enters a loop over all of the training examples. The following
process is repeated for each training case:

• Copy the target input values to the activation values of the neurons
in the input layer
• Propagate the input layer activation energies to the next layer by
scaling the activation values by the appropriate weight value. Re-
member that each neuron in a layer is connected to each neuron
in the next layer by a different weight value. Continue propagating
activation energies from layer to layer until this process stops after
the new activation values of the output layer neurons have been
calculated
• Calculate the error for each output neuron by comparing the prop-
agated value of each neuron to the target value from the training
data. The derivative of the Sigmoid function is used to scale the sum
of products before calculating the delta weight values
• Calculate a back-propagated error to all hidden layers
• Use the back-propagated errors to calculate the delta weights for
the entire network. The delta weights for the current training data
case are summed with the delta weights for all other training cases

After this process is repeated for all training cases, all weights in the
network are updated by adding to each weight its corresponding delta
weight value.
The function DeltaRecall is fairly simple. A trained network is used
to calculate the output neuron values obtained by setting the input
layer neuron activation values to a specified set of values and using the
following steps to calculate the output layer neuron activation values:

• Copy the target input values to the activation values of the neurons
in the input layer
• Propagate the input layer activation energies to the next layer by
scaling the activation values by the appropriate weight value. Con-
tinue propagating activation energies from layer to layer until this
6.3. IMPLEMENTATION OF A NEURAL NETWORK LIBRARY 125

process stops after the new activation values of the output layer
neurons have been calculated

In both functions DeltaLearn and DeltaRecall new neuron activation


values are always calculated by summing over the values of each neuron
activation in the previous layer multiplied by the appropriate weight
value. This sum of products is passed through the Sigmoid function to
calculate the actual activation values.
Function plotActivations is used to plot the activation values of net-
work neurons, and the function plotWeights plots the values of the
weights connecting the neuron layers. Figure 6.2 shows the MIT
Scheme listener window during execution of the test-gr test program de-
fined at the end of file NN.S. Function test-gr uses the function DeltaPlot,
which uses the utility functions plotActivations and plotWeights.
Figures 6.3 and 6.4 show the MIT Scheme graphics window while
the test-gr sample program is executing. Gray-scale values are used to
represent the values of the activation values and weights. A neutral gray
color represents a zero value. Darker grays and black represent larger
values. Lighter grays and white represent progressively larger negative
values.
Saving trained neural networks to a file permits neural networks to
be used in your application programs. Listing 6.3 shows the output file
generated by function WriteDeltaNetwork for a very small test network:

1 )-) (load "nn.s")


;Loading "nn.s" -- done
;Ualu est-gr
1 )-) (load "nn.s")
;Loading "nn.s" -- done
;Ualue: test-gr
1 ]-) (test-gr)
.... raining cycle 10 RMS error - .4762654516928603
.... training cycle 110 RMS error· .3961339087865075

FIGURE 6.2. Listener window after loading source files (ARRAY.S, GRAPH.S, and NN.S) and
while executing the function test-gr. Figures 6.3 and 6.4 show the MIT Scheme graphics
windows while function test-gr is running.
126 6. NEURAL NETWORK LIBRARY

Delta Network

slab3

slab2 -) slab3
Weights and Delta Weights

slab2

• •
slabl -) slab2
Weights and Delta Weights

slabl

FIGURE 6.3. MIT Scheme graphics window shown after calling function DeltaLearn twenty
times. The output neuron layer is not yet producing the desired output pattern. The delta
weight arrays contain large values because the backwards-propagated errors are large, so the
corrections to the weights are large.

Listing 6.3
number of layers
2
number of neurons in each layer
(2 2)
;; activation values
-3.7871714036296716e-2
.9700991925062835
;; weights
-1.92433999441247
1.9013980514965736
1.0142383744643486
-1.0122338425505022
6.3. IMPLEMENTATION OF A NEURAL NETWORK LIBRARY 127

.,
Delta Network

I I II II
slab3

slab2 -) slab3
Weights and Delta Weights

slab2

slabl -) slab2
Weights and Delta Weights

slabl

FIGURE 6.4. MIT Scheme graphics window shown after calling function DeltaLearn four hun-
dred times. The output neuron layer is starting to show the desired output patterns. The delta
weight arrays have small values, since the backwards-propagated errors are now small, and
so the corrections to the weight arrays are small.

;; delta weights
-8. 573423663385411e-3
8.849681510894006e-3
1.5986012182692088e-2
-1.5779824336095313e-2
;; old delta weights
-8. 573423663385411e-3
8. 849681510894006e-3
1.5986012182692088e-2
-1.5779824336095313e-2
;; alpha
.2
;; beta
.8
128 6. NEURAL NETWORK LIBRARY

The function ReadDeltaNetwork is used to create a new network object


by reading a network dump file like the one seen in Listing 6.3.

6.4 EXAMPLE APPLICATION: CHARACTER RECOGNITION


Recognizing roughly drawn characters is a difficult pattern recognition
task. As we will see in the example program listed in this section, using
neural networks makes this recognition task easy; most of the work is
in setting up training data for the neural network.
Listing 6.4 shows the contents of the file HAND.S on the example
disk.

Listing 6.4
File: hand. s

Oesciption: This file contains an example character


recognition program that uses the neural
network library in file "nn.s" and the
20 array library in file "array.s". The
characters are encoded as a list of strings.
For example:

(define example-t
(list
xx
x x
xx
x x
x x
xx II))
might encode an "8". This example is set
to work with 6x6 bit images of characters,
but you can easily change this to finer
resolution by changing the two constants
P-WIOTH and P-HEIGHT, and changing the training
and testing data.

(define P-WIOTH 6)
(define P-HEIGHT 6)

(define training-data #f)

(set! training-data
6.4. EXAMPLE APPLICATION: CHARACTER RECOGNITION 129

(list
(list
(list o0 0) 0 in binary
(list
xx
" x X
"x x"
"x x"
" x x
xx "))
(list
(list 0 0 1) 1 in binary
(list
x
x
x
x
x
X "))
(list
(list o0 1) 1 in binary
(list
x
xx
x
x
x
xxx "))
(list
(list 0 1 0) 2 in binary
(list
" xxxx "
"x x"
x
x
X
" XXXXX"))
(list
(list 0 1 0) 2 in binary
(list
" xxx "
" X x"
X "
X "
X
"XXXXXX"))
(list
(list o1 1) 3 in binary
(list
130 6. NEURAL NETWORK liBRARY

" xxx
"x x
X
X
X X "
"xxxx "))
(list
(list 0 1 1) 3 in binary
(list
II xxx
"x X
"X
II xx "
"x x "
"XXXX "))
(list
(list 1 0 0) 4 in binary
(list
"x "
II
"X X
"x X
"XXXXXX It
" X
II X "))
(list
(list 1 o 0) 4 in binary
(list
x "
" xx
x x
"XXXXXX"
x "
II X II))))

(define convert-training-data
(lambda (input)
(let «output-pattern (car input))
(str-list (cadr input))
(input-list '()))
(do «a-str str-list (cdr a-str)))
«null? a-str) )
(do «i 0 (+ i 1)))
«equal? i P-WIDTH))
(if (equal? (string-ref (car a-str) i) #\
(set! input-list (cons 0 input-list))
(set! input-list (cons 1 input-list)))))
(list (reverse input-list) output-pattern))))

test:
6.4. EXAMPLE APPLICATION: CHARACTER RECOGNITION 131

; (pp (convert-training-data (cadr training-data)))

(define train
(lambda 0
;; create the training data:
(let «t-data (map convert-training-data training-data))
(a-network (NewDeltaNetwork '(36 4 3)))
(error 0.0))
(do «i 0 (+ i 1)))
«equal? i 500))
(set!
error
(DeltaLearn a-network t-data))
;; print out every 4 cycles:
(if (equal? (modulo i 4) 0)
(begin
(display " .... training cycle \#")
(display i)
(display II error = II)
(display error)
(newline))))
a-network)))

(define test-net)
(set! test-net (train))

(define test-data
(list
(list
xx
II
X X

"X x"
"X x"
II II
X X
xx II)

(list
x
II
X
II II
X
X
X
II
X II)

(list
x II

xx
II
X
X
II
X
xxx II)
132 6. NEURAL NETWORK LIBRARY

(list
II xxxx II

"x XII

x
x
x
" xxxxx")
(list
xxx
X XU

x
x
x
"xxxxxx")
(list
IIxxx
II X X
x
x
x x
"xxxx II)
(list
" xxx
"x x
"x
" xx
"x X
t1
xxxx II)
(list
"x
"x x
"x x
II XXXXXX II
x
x II)
(list
x
xx
x x
"xxxxxx"
x
x ')))

(define convert-test-data
(lambda (input)
(let «input-list 'C)))
(do «a-str input (cdr a-str)))
«null? a-str))
(do «i 0 (+ i 1)))
6.4. EXAMPLE APPLICATION: CHARACTER RECOGNITION 133

((equal? i P-WIDTH»
(if (equal? (string-ref (car a-str) i) #\
(set! input-list (cons 0 input-list»
(set! input-list (cons 1 input-list»»)
(reverse input-list»»

test:
; (pp (convert-test-data (cadr test-data»)

(define test
(lambda (a-network)
;; create the training data:
(let ((data (map convert-test-data test-data»
(outputs #f»
(define test-helper
(lambda (inputs)
(set! outputs (DeltaRecall a-network inputs»
(let ((count 0»
(do ((i 0 (+ i 1»)
((equal? i P-HEIGHT»
(newline)
(do ((j 0 (+ j 1))
((equal? j P-WIDTH»
(display (list-ref inputs count»
(set! count (+ count 1»»)
(newline)
(display outputs)
(newline» )
(map test-helper data»»

You need to run 'train' test before this:

; (test test-net)

You can change the grid size for representing characters by changing
the values of the variables P-WIDTH and P-HEIGHT in Listing 6.4. The
list assigned to the variable training-data is a list of lists, each sub-list
containing
• a list of target output activation values
• a list of P-HEIGHT character strings, each of length P-WIDTH (a blank
character represents an input activation value of zero, while a non-
blank character represents an input activation value of one)

The utility function convert-training-data simply converts each sub-list


in the variable training-data into a list containing values of zero or one
for the input activation training values. The function train uses func-
tion convert-training-data to generate numerical training data, creates
a new neural network object with the correct number of neurons in
134 6. NEURAL NETWORK LIBRARY

each layer, and uses the function DeltaLearn to train the network object.
The variable test-data is assigned a list of test data sets that will be
converted from numeric values to input neuron activation values. The
function convert-test-data converts the string data in variable test-data
to numeric data. The function test applies a trained network object to
these numeric test data using the function DeltaRecal1.
Listing 6.5 shows both the training and recall of roughly drawn
characters.

Listing 6.5
> (load "array")
;Loading "array.com" -- done
;Value: test

> (load "nn")


;Loading "nn.com" -- done
;Value: test-gr

> (load "hand")


;Loading "hand.ctlm" -- done
;Value: test

> (define test-net #f)


;Value: test-net

> (set! test-net (train) )


.... training cycle #0 error .5028562365547626
.... training cycle #4 error .475235192439857
.... training cycle #8 error .5362885163037966
.... training cycle #12 error .44537318858656016
.... training cycle #16 error .5568914325829556
.... training cycle #20 error .4928533370748001
.... training cycle #24 error .5428123908669709
.... training cycle #28 error .5368579957679164
.... training cycle #32 error .4672480169138619
.... training cycle #36 error .5601220659711005
.... training cycle #40 error .624135595648844
.... training cycle #44 error .5283345154941789
.... training cycle #48 error .4797993500302591
.... training cycle #52 error .41533656011605014
.... training cycle #56 error .40515195534711596
.... training cycle #60 error .40164331810674053
.... training cycle #64 error .4079304560188999
.... training cycle #68 error .397872862733792
.... training cycle #72 error .3897774738325827
.... training cycle #76 error .3924652621373146
.... training cycle #80 error .3839821747802256
6.4. EXAMPLE APPLICATION: CHARACTER RECOGNITION 135

.... training cycle #84 error .3873141991165859


· ... training cycle #88 error .3784136955900479
.... training cycle #92 error .3786305029440533
.... training cycle #96 error .37840272004841413
.... training cycle #100 error .376771875267696
.... training cycle #104 error .37205809597574196
.... training cycle #108 error .37399557425505453
.... training cycle #112 error .37357879028482915
.... training cycle #116 error .36763160493178976
.... training cycle #120 error .3686297761730108
.... training cycle #124 error .36684158500440395
.... training cycle #128 error .3665543420473496
· ... training cycle #132 error .35905913896173397
· ... training cycle #136 error .36489517027635654
.... training cycle #140 error .36216536443295727
.... training cycle #144 error .36501593866154675
.... training cycle #148 error .36248067215168217
.. , . training cycle #152 error .36062246999350694
.... training cycle #156 error .35990537610205137
.... training cycle #160 error .359269144282841
.... training cycle #164 error .35826673877695075
.... training cycle #168 error .3579561750513607
.... training cycle #172 error .3570548732714686
· ... training cycle #176 error .3534295149578141
.... training cycle #180 error .3566982917916562
.... training cycle #184 error .3555868020628646
.... training cycle #188 error .35466188040415597
.... training cycle #192 error .3530822709831128
.... training cycle #196 error .3492921210706921
.... training cycle #200 error .3458844430896974
.... training cycle #204 error .34436147088358177
.... training cycle #208 error .3393772890589974
.... training cycle #212 error .3359272591327509
.... training cycle #216 error .3319902020927823
.... training cycle #220 error .3283248061424676
· ... training cycle #224 error .3266519723374745
.... training cycle #228 error .3243022406210582
.... training cycle #232 error .3197823826689011
.... training cycle #236 error .31998704245106174
.... training cycle #240 error .3189981636217799
.... training cycle #244 error .31878698392832444
.... training cycle #248 error .30832794564879623
.... training cycle #252 error .30287726921766933
.... training cycle #256 error .30450038014019115
.... training cycle #260 error .31205093615777074
.... training cycle #264 error .2912295404714828
· ... training cycle #268 error .29324212722382476
.... training cycle #272 error .28649092572182516
· .,. training cycle #276 error .2762934516684063
136 6. NEURAL NETWORK LIBRARY

.... training cycle #280 error .2860582358545704


.... training cycle #284 error .30632767947916906
., .. training cycle #288 error .30141204772736246
· ... training cycle #292 error .28102577060120754
· " . training cycle #296 error .2601380589199112
· ... training cycle #300 error .2165689539750606
· ... training cycle #304 error .2922234783638669
.... training cycle #308 error .2951551057105587
.... training cycle #312 error .2359809437413148
.... training cycle #316 error .15413414692815755
.... training cycle #320 error .110364089179715
.... training cycle #324 error .08664271247606209
· ... training cycle #328 error .09807498785249737
· ... training cycle #332 error .07755677842268315
· ... training cycle #336 error .08266378275501204
· ... training cycle #340 error .06759777634558221
.. , . training cycle #344 error .20575616811136968
· ... training cycle #348 error .3297376124671234
'" . training cycle #352 error .3148773100543428
· ... training cycle #356 error .28858247134229
.... training cycle #360 error .20721902514739607
· ... training cycle #364 error .26083545466997554
.... training cycle #368 error .2819755044883786
.. , . training cycle #372 error .27961679866826494
'" . training cycle #376 error .13593845679758812
.... training cycle #380 error .10924588776024308
· ... training cycle #384 error .08894466458298118
· ... training cycle #388 error .09173744530563958
" .. training cycle #392 error .0670483287342059
· ... training cycle #396 error .05819075246389573
· ... training cycle #400 error 5. 3962644352706986e-2
· ... training cycle #404 error .05527349691999955
· ... training cycle #408 error .05452480556790088
.... training cycle #412 error .054592062029712
.... training cycle #416 error .05414203676312083
.... training cycle #420 error 5.0612882674795336e-2
· ... training cycle #424 error .04784940020953917
.... training cycle #428 error .04741723214939326
· ... training cycle #432 error .04752128003775829
.... training cycle #436 error .04779817491558644
.... training cycle #440 error 5. 3328606998131835e-2
.... training cycle #444 error .04462688113995079
.... training cycle #448 error .04566389362553875
· ... training cycle #452 error .04544160073125756
· ... training cycle #456 error 4.4024175089843416e-2
.... training cycle #460 error 4. 2500512911217446e-2
.... training cycle #464 error 4. 3763239597274545e-2
· ... training cycle #468 error 4. 1141407647040426e-2
.... training cycle #472 error .04042510550587852
6.4. EXAMPLE APPLICATION: CHARACTER RECOGNITION 137

· ... training cycle #476 error .04238678441649026


· ... training cycle #480 error 4.0958956156427445e-2
· ... training cycle #484 error .04619767752210263
· ... training cycle #488 error .04483271080475718
· ... training cycle #492 error .04003463362940062
· ... training cycle #496 error 3. 8467709071659806e-2
;No value

> (WriteDeltaNetwork "hnd.net" test-net)


;No value

> (test test-net)


000100
001100
010100
111111
000100
000100
(.9631338557596915 2.3022904605130915e-4 .07959291347839423)

100000
100100
100100
111111
000100
000100
(.9629369130119926 2.3591652822295566e-4 .07958494918278963)

011100
100010
100000
011000
100010
111100
(1.5008819602605697e-9 .9999999999999056 .9660293993948157)

011100
100010
010000
001000
010010
111100
(1.495301151263447e-9 .9999999999999052 .9659197873301495)

001110
010001
000010
000100
010000
138 6. NEURAL NETWORK LIBRARY

111111
(1.4278818438326796e-5 .982628810308463 .04224408466874724)

011110
100001
000010
000100
001000
011111
(1.427098181190951ge-5 .9826606308520179 .04226754800184698)

000100
001100
000100
000100
000100
001110
(1.1912104070509307e-8 4.190293098904157e-16 .9678453383799069)

000100
000100
000100
000100
000100
000100
(2.044963654211298e-8 3.419161877936915e-16 .9721188341697696)

001100
010010
100001
100001
010010
001100
(1.6645816482122926e-2 3.0009938718789622e-3 1.1384988204427855e-2)

Remember that the output neuron values printed in Listing 6.5 rep-
resent the binary encoding of the first five non-negative integers, as seen
in Table 6.3.

Table 6.3
Decimal number Binary encoding
o 000
1 001
2 010
3 011
4 100
6.4. EXAMPLE APPLICATION: CHARACTER RECOGNITION 139

The character recognition program recognized all of the training


patterns in Listing 6.5. This test is rather too easy, however: we also
expect a properly trained neural network to recognize the original pat-
terns that it was trained with. The reader should make a copy of the
file HAND.s (e.g., HAND2.S) and edit the definition of the variable test-
data to add additional noise to the pattern representing the numbers 0
through 4. For example, Listing 6.6 shows both the original pattern for
the number 2 and the same pattern with additional noise.

Listing 6.6
;; original pattern:
(list
" XXXX It

"x x"
" x "
x "
x
" xxxxx")

original pattern, with additional noise:


(list
" x x "
"X XII

x
x "
x
"x x xx")
CHAPTER 7
COMPLEX DATA
STRUCTURES

Data structures are the heart of computer programming. There is


no excuse for starting a programming project without first carefully
considering what data structures are required to express and solve a
problem. We should naturally think of problem solving as starting with
naming types of objects in the problem domain, thinking about what
information instances of each type of object needs to store, and how
data objects will interact.

7. 1 USING SCHEME EFFECTIVELY TO PROTOTYPE


AND TEST COMPLEX DATA STRUCTURES
Dynamic interpreted languages like Scheme are extremely useful tools
for exploring complex data structures. As we will see in this chapter,
we can quickly build interesting programs by designing data structures
for expressing a problem or relationship, and then writing Scheme
code "from the bottom up" to build, maintain, and access these data
structures.
Often the analysis and design activities are usefully augmented with
interactive experimentation with data structures in a Scheme environ-
ment. The advice to alternate between low-level experimentation with
data structures and top down analysis and design should not be taken
as an excuse to "hack" code, forsaking the design process.
142 7. COMPLEX DATA STRUCTURES

7.2 EXAMPLE APPLICATION: NATURAL LANGUAGE PROCESSING


Natural language processing is an extremely complex task. There is a
wide spectrum of theories and techniques for accomplishing natural
language processing; at extreme ends of this spectrum we see
1. Syntax-directed parsers that care about sentence structure but not
meaning
2. Knowledge-based parsers that attempt to "understand" natural
language at some deep level

Simpler to implement, syntax-directed parsers are much more com-


mon that knowledge-based parsers. In this chapter we implement a
knowledge-based parser; it is left as an exercise for the reader to
determine how well this parser "understands" natural language.

7.2.1. ANALYSIS

What are the requirements for a' knowledge-based software program


to parse and "understand" natural language? We want our program to
provide the following functionality:
1. The parser should convert sentences with arbitrary sentence
structure, or morphology, to a highly structured form
2. The output from the parser should be tailored so other programs
can use it relatively easily for mapping natural language commands
into a well defined set of commands for another program to execute

What we want is to design and implement a flexible library that can


be added to other programs that we write to provide a natural language
"front end." The following features will provide this flexibility:
1. The ability to add new words to parser's lexicon
2. A simple interface for parsing sentences and interpreting the
parser's output

The key to meeting these requirements will be the use of Conceptual


Dependency (CD) Theory (Schank and Riesbeck, 1981). Conceptual De-
pendency Theory assumes that there is a small number of primitive
actions, such as
ATRANS transfer possession of something
MTRANS transfer an idea or concept
PTRANS move something

The capitalized words are a short notation for primitive concepts.


The same simple concepts can be expressed in natural language in an
7.2. EXAMPLE APPLICATION: NATURAL LANGUAGE PROCESSING 143

almost infinite number of ways by changing sentence structure and


word choice. We want our parser to be able to produce identical output
from sentences that have the same meaning; for example:
Mark gave Carol a book yesterday.
Yesterday, Carol received a book from Mark.
We want our parser to provide the following structured output from
either of these two sentences:
Action: ATRANS
Object: book
Actor: Mark
Recipient: Carol
Time: -24 hours

7.2.2. DESIGN

In Section 7.2.1 we analyzed the requirements for a program to convert


natural language text to a more structured form. We have a rough but
adequate understanding of the requirements. This understanding will
improve, and we can update our analysis of the problem, after we make
a first pass at designing the data structures and algorithms and write a
first-stage implementation of our design.
We need a data structure for representing the output of our natural
language parser. This data structure can be used throughout the parsing
process to store intermediate results. We will use a frame data structure
which has named data slots for the following concepts:
1. action
2. object
3. actor
4. place
5. time
6. recipient

These data are easily implemented using a Scheme vector with six
elements.
We also want a mapping of verbs to CD actions. We can use a Scheme
list to hold verb-CD action pairs. Another simple list will contain names
of objects.

7.2.3. IMPLEMENTATION

We will start the implementation of our parser from the "bottom up,"
starting with:
144 7. COMPLEX DATA STRUCTURES

1. writing a function make-frame to create a new, empty frame data


structure
2. writing utility functions to retrieve the data for any "slot" in a frame
3. writing utility functions for changing the value of any "slot" in a
frame

These functions are easily implemented in Listing 7.1.

Listing 7.1
(define (make-frame) (make-vector 6 #f))

(define (actor-set! frame actor) (vector-set! frame 0 actor))


(define (actor-ref frame) (vector-ref frame 0))

(define (action-set! frame action) (vector-set! frame 1 action))


(define (action-ref frame) (vector-ref frame 1))

(define (object-set! frame object) (vector-set! frame 2 object))


(define (object-ref frame) (vector-ref frame 2))

(define (time-set! frame time) (vector-set! frame 3 time))


(define (time-ref frame) (vector-ref frame 3))

(define (place-set! frame place) (vector-set! frame 4 place))


(define (place-ref frame) (vector-ref frame 4))

(define (recipient-set! frame recipient)


(vector-set! frame 5 recipient))
(define (recipient-ref frame) (vector-ref frame 5))
We need to map English verbs to CD actions. The Scheme list in
Listing 7.2 looks as though it might work.

Listing 7.2
(define verb-list
'((gave ATRANS)
(give ATRANS)
(received ATRANS)))

We can map a verb stored in the Scheme variable word into its CD
equivalent (assume that the Scheme variable current-frame contains a
CD frame data object); this is shown in Listing 7.3.

Listing 7.3
(let ((action-item (assoc word verb-list)))
(if (not (null? action-item))
(let ((verb (list-ref action-item 0))
7.2. EXAMPLE APPLICATION: NATURAL LANGUAGE PROCESSING 145

(cd-primitive (list-ref action-item 1»)


(action-set! cu"en~"ame cd-primitive»»
You should try typing the contents of Listing 7.3 in a Scheme listener
window. Unfortunately, this implementation does not handle cases
where the object in a sentence occurs before the verb and the actor
in the sentence occurs after the verb (e.g., "Carol received a book from
Mark"). We need a notation for tagging verbs that require switching the
actor/recipient slots in the parser's frame data structure.
We can modify the data structure for verb-list, adding a "reverse" tag
for each verb; this is seen in Listing 7.4.

Listing 7.4
(define verb-list
'«gave ATRANS #f)
(give ATRANS #f)
(received ATRANS #t»)

Function parse-verbs in Listing 7.5 shows how the "reverse" tags are
used. Listing 7.5 shows the contents offile CD.S.

Listing 7.5
File: CD.S

Description: This file contains a parser for Roger Schank's


and Chris Riesbeck's Conceptual Dependency (CD) theory.

Copyright 1995, Mark Watson

Definition of a CD frame data structure:

actor
action
object
time (units of hours)
place
recipient

(define make-frame
(lambda 0
(make-vector 6 #f»)

(define actor-set!
(lambda (frame actor)
(vector-set! frame 0 actor»)
146 7. COMPLEX DATA STRUCTURES

(define actor-ref
(lambda (frame)
(vector-ref frame 0)))

(define action-set!
(lambda (frame action)
(vector-set! frame 1 action)))

(define action-ref
(lambda (frame)
(vector-ref frame 1)))

(define object-set!
(lambda (frame object)
(vector-set! frame 2 object)))

(define object-ref
(lambda (frame)
(vector-ref frame 2)))

(define time-set!
(lambda (frame time)
(vector-set! frame 3 time)))

(define time-ref
(lambda (frame)
(vector-ref frame 3)))

(define place-set!
(lambda (frame place)
(vector-set! frame 4 place)))

(define place-ref
(lambda (frame)
(vector-ref frame 4)))

(define recipient-set!
(lambda (frame recipient)
(vector-set! frame 5 recipient)))

(define recipient-ref
(lambda (frame)
(vector-ref frame 5)))

(define human-list '(Mark Carol))


(define object-list '(car boat book))

;; Format of verb-list items:


7.2. EXAMPLE APPLICATION: NATURAL LANGUAGE PROCESSING 147

english word
CD primative
actor <--> recipient reversal flag

(define verb-list '«gave ATRANS #f) (give ATRANS #f)


(received ATRANS #t)))

;; Time:

(define time-list '«(yesterday) -24) «last week) -168)


«tommorrow) 24)))

(define noun-list '(car book boat street))

(define parse-human-name
(lambda (current-frame rest-of-sentence)
(let «word (car rest-of-sentence)))
(if (member word human-list)
(if (equal? (actor-ref current-frame) #f)
(actor-set! current-frame word)
(if (equal? (recipient-ref current-frame) #f)
(recipient-set! current-frame word)))))))

(define parse-verbs
(lambda (current-frame rest-of-sentence)
(let «action-item (assoc (car rest-of-sentence) verb-list)))
(if (not (null? action-item))
(let «verb (list-ref action-item 0))
(cd-primitive (list-ref action-item 1))
(actor-recipient-reversal-flag (list-ref action-item 2)))
(action-set! current-frame cd-primitive)
(if actor-recipient-reversal-flag
(let «temp (actor-ref current-frame)))
(actor-set! current-frame (recipient-ref current-frame))
(recipient-set! current-frame temp))))))))

(define parse-nouns
(lambda (current-frame rest-of-sentence)
(let «word (car rest-of-sentence)))
(if (member word noun-list)
(if (equal? (object-ref current-frame) #f)
(object-set! current-frame word))))))

(define parse-time
(lambda (current-frame rest-of-sentence)
(let «time-item (assoc (car rest-of-sentence) time-list)))
(if (not (null? time-item))
(time-set! current-frame (cadr time-item))))
(if (> (length rest-of-sentence) 1)
148 7. COMPLEX DATA STRUCTURES

(let ((time-item
(assoc
(list (car rest-of-sentence) (cadr rest-of-sentence))
time-list)))
(if (not (null? time-item))
(time-set! current-frame (cadr time-item)))))))

(define parse-all
(lambda (current-frame rest-of-sentence)
(parse-human-name current-frame rest-of-sentence)
(parse-verbs current-frame rest-of-sentence)
(parse-nouns current-frame rest-of-sentence)
(parse-time current-frame rest-of-sentence)))

test code:

(define sentence '(Mark gave Carol a book last week))

(define test
(lambda 0
(let ((f (make-frame)))
(do ((sen sentence (cdr sen)))
((null? sen))
(display "Sentence: II)
(display sen)
(newline)
(parse-all f sen))
(display "Parse: ")
(display f)
(newline))))

The function make-frame defined in Listing 7.5 creates a frame data


structure containing six slots. The following utility functions are used
to set slot values in a CD parsing frame:
• actor-set!
• action-set!
• object-set!
• time-set!
• place-set!
• recipient-set!

The following functions are used to read slot values from a CD


parsing frame:

• actor-ref
• action-ref
• object-ref
• time-ref
7.2. EXAMPLE APPLICATION: NATURAL LANGUAGE PROCESSING 149

• place-ref
• recipient-ref
The function parse-all has two required arguments: a CD parser frame
object created with function make-frame and a list of words that are the
remainder of a sentence to parse. Function parse-all processes only the
first word passed in the second argument. It would be more convenient
in a production parser to process an entire sentence in a single func-
tion call. However, the parser shown in Listing 7.5 is useful only as an
example of building a natural language parser. For this intended use
function parse-all more conveniently parses a single word each time it
is called. The function test at the bottom of Listing 7.5 shows how to
iteratively call function parse-all for each word in a test sentence; the
current frame is printed after each word is processed. Function parse-all
calls the functions
• parse-human-name: can fill in either the actor or recipient slot
• parse-verbs: can fill in the verb slot, and can swap the actor and
object slots if the verb's reverse flag is set
• parse-nouns: can fill in the object slot
• parse-time: can fill in the time slot

Listing 7.6 shows sample output from the CD parser.

Listing 7.6
> (load "cd. s")
;Loading "cd.s" -- done
;Value: test
> (test)
Sentence: (mark gave carol a book last week)
Parse: #(mark 0 0 0 0 0)
Sentence: (gave carol a book last week)
Parse: #(mark atrans () () () (»
Sentence: (carol a book last week)
Parse: #(mark atrans () () () carol)
Sentence: (a book last week)
Parse: #(mark atrans () () () carol)
Sentence: (book last week)
Parse: #(mark atrans book () () carol)
Sentence: (last week)
Parse: #(mark atrans book -168 () carol)
Sentence: (week)
Parse: #(mark atrans book -168 () carol)
;Value: #t
> {set! sentence '(Last week Carol received a book from Mark»
;Value 4: (mark gave carol a book last week)
> (test)
150 7. COMPLEX DATA STRUCTURES

Sentence: (last week carol received a book from mark)


Parse: #(0 0 0 -168 0 0)
Sentence: (week carol received a book from mark)
Parse: #(0 0 0 -168 0 0)
Sentence: (carol received a book from mark)
Parse: #(carol 0 0 -168 0 0)
Sentence: (received a book from mark)
Parse: #(() atrans () -168 () carol)
Sentence: (a book from mark)
Parse: #(() atrans () -168 () carol)
Sentence: (book from mark)
Parse: #(() atrans book -168 () carol)
Sentence: (from mark)
Parse: #(() atrans book -168 () carol)
Sentence: (mark)
Parse: #(mark atrans book -168 () carol)
;Value: #t

In Listing 7.6 the CD parser produced the same output for the two
sentences "Mark gave Carol a book last week" and "Last week Carol
receiv.ed a book from Mark." The output from the CD parser is identical
for both sentences because both sentences have the same meaning.
CHAPTER 8
CHESS PLAYING PROGRAM

Computer chess programs have long been a testing ground for devel-
oping search techniques. Chess programs use some form of lookahead
(prediction of future moves) search combined with static evaluation
(judging the relative worth of a chess position without lookahead, that
is, by counting material worth of both black and white pieces and com-
paring the relative mobility of the black and white pieces). We will
develop a chess playing program in this chapter that relies mostly on
static evaluation using heuristic chess knowledge. The program will
usually give the impression of strong positional play, although it plays
a poor tactical game. The static evaluation function uses a two-ply (i.e.,
two half moves) lookahead to evaluate piece capture.

8. 1 ANALYSIS
Claude Shannon described two search techniques that he called type A
and type B. The type A search technique looked at all possible moves
from a given position. For example, there are usually about 40 possi-
ble (legal) chess moves from a typical board position; to look three
moves ahead three complete moves (one moves for each player for
each complete move) requires the examination of about 40(3*2), or
406 (4,096,000,000) board positions. Because of the impracticality of
deep searches using the type A strategy, Shannon predicted that high-
performance game playing programs would use the human-style type B
strategy, in which the program would not examine all possibilities, but
use heuristic rules derived from consultation with human chess experts
to reduce the number of moves searched from a given board position.
In this way a program would be able to examine only "good" moves,
without, it was hoped, overlooking too many strong moves. If a chess
program searches only six plausible moves per tum, then a three-move
152 8. CHESS PLAYING PROGRAM

lookahead requires the examination of 6(3*2), or 66 (46,656), board posi-


tions. While he was a student at MIT, Richard Greenblatt, a world-class
LISP programmer, wrote an excellent chess-playing program (which
used to clobber me when I was learning to play chess) that used Shan-
non's type B strategy. However, Greenblatt's program, and all other type
B search strategy chess programs, never considered, and thus missed,
many good moves.
Commercial chess playing programs are certainly not written in LISP
or Scheme for performance reasons. Even with a good compiler, a chess
playing program written in Scheme will play much more slowly than
one coded in assembler language or a language like C or C++. Still, the
program developed in this chapter can calculate a move in about five
seconds when running in the MIT Scheme system on a 486 PC.

8.2 DESIGN
The following seven parameters are considered in the static evalua-
tion of a given board position. The Chess program developed in this
chapter attempts to maximize a static evaluation score based on these
parameters.
1. Material (value of pieces left on the board)
2. Number of times pieces are guarded
3. Mobility of playing pieces
4. Control of central squares by pawns
5. Control of central squares by pieces
6. Control of squares adjacent to both kings
7. Attack on pieces by less valuable pieces

The function value implements the calculation of the static evalua-


tion for a given board position. The Scheme chess program is fairly
simple, consisting of only about six pages of code, not including
comments. The program plays only the black pieces, and does not
understand enpassant pawn captures.

8.3 IMPLEMENTATION
The chess program developed in this chapter uses algebraic chess no-
tation for entering chess moves. Figure 8.1 shows the labeling of the
squares on a chess board using algebraic notation.
8.3. IMPLEMENTATION 153

A8 B8 C8 D8 E8 F8· G8 Il8 ,<"..,., •. .• ,'., ,,,.--=.-

>~fj B7 ,~~ D7:ftj; F7~1,H7


.. ..

A6 B6 C6f)6' E6 F:6 G6H6 ,/" . .0- ":' • .. • .., ;.,.

~~. BS .G~·:: DS ::E5 FS'<4JS: HS


A4 B4 C4D4 E4 F4 G4 H4
A~ B3 . .C~ D3 ··:::E3.. F3 .:w3 H3
. ... :.".:.;,::., . .,.',.
;:. . .

A2 B2 C2 D2 E2F2 G2 H2
Al B 1 Cl DI ';·;·EI
.:"
FI Gl HI
. ';.., . :.;.; .

FIGURE 8.1. Chess board squares labeled with algebraic notation.

Figure 8.2 shows the indexing scheme for the chess board that is
used by the example program in this chapter. The square Al on the
chess board (using algebraic notation as seen in Figure 8.1) is at vector
index 22 in Figure 8.2. Similarly, the square H8, in the opposite corner
of the chess board in Figure 8.1, is at board vector index 99 in Figure
8.2. In Figure 8.2 all squares that lie on the chyss board are enclosed in
a rectangle; all other squares in the board vector have a numeric value
of 7, which is a flag indicating that the square is off of the chess board.
Listing 8.1 shows the file CHESS.S. In Listing 8.1 the variable *piece*
contains relative move values for all pieces except pawns. The variable
*index* is used to convert a piece type into a move index pointer in the
vector stored in *piece*. Table 8.1 shows the piece type indices.
154 8. CHESS PLAYING PROGRAM

110 111 112 113 114 115 116 117 118 119
100 101 102 103 104 105 106 107 108 109
90 91 92 93 94 95 96 97 98 99
80 81 82 83 84 85 86 87 88 89
70 71 72 73 74 75 76 77 78 79
60 61 62 63 64 65 66 67 68 69
50 51 52 53 54 55 56 57 58 59
40 41 42 43 44 45 46 47 48 49
30 31 32 33 34 35 36 37 38 39
20 21 22 23 24 25 26 27 28 29
10 11 12 13 14 15 16 17 18 19
o 1 234 56789

FIGURE 8.2. Chess board represented by a vector of length 120; 64 elements represent the
board squares, while the remaining elements of the vector contain the value 7, indicating that
the square is off of the board.

Table 8.1
Piece type Piece value in board vector
Pawn 1
Knight 2
Bishop 3
Rook 4
Queen 5
King 9

For example, if the square at index 24 (CIon the chess board) con-
tains a Bishop, the value of (vector-ref *index* 3) is equal to 10. The
move entries for a Bishop start at index 10 in the vector stored in *piece*
and end with a zero entry. The move entries for a Bishop are: -9 -11 9
11. The move entries for a rook are -1 1 10 -10. The move entries for a
Queen are 1 -1 10 -10 -9 -11 9 11. For Bishops, Rooks, and Queens
possible moves are calculated by taking each move entry and repeti-
tively adding it to the current board index until the piece runs off of
the edge of the board or collides with another piece. As seen in Listing
8.1, the move calculations for these three types of pieces is simple to
implement using these move tables. The moves for Kings and Knights
are similar, except that each move value is applied to the current piece
position only once. The move calculations for Pawns are complex. As
seen in function Mover in Listing 8.1, there is more code for handling
pawn moves than for all of the other piece types combined.
8.3. IMPLEMENTATION 155

The piece type values in Table 8.1 are shown as positive integers. The
black pieces on the Chess board (always played by the computer) are
stored as negative integers in the global variable *board*. The absolute
value of the piece type is used as an index into the *plece* move table.
The function goto calculates all possible moves for a chess piece on
a specified square. This function is used to calculate control arrays that
specify how many times each square on the chess board is controlled by
both the black and white pieces. The function posib calculates all possi-
ble moves from all squares on the chess board. The function InitChess is
used to set up the data for a new game. The function Mover calls function
posib to calculate all possible moves, then for each move calls function
value to rate the moves. Function Mover uses the global flag variables
*wking-moved*, *wrook1-moved*, *wrook2-moved*, *bklng-moved*, *brook1-
moved*, *brook2-moved* to determine whether a castle move is legal (a
castle move is legal if the king is not in check, the king has never moved,
the rook on the side to castle has never moved, the squares between the
king and rook are empty, and these empty squares are not controlled
by the opponent). If the global flag variable *debug* is not equal to #F,
then all possible moves are printed with their values. Function Mover
uses the non-standard MIT Scheme function sort to sort the moves for
printout when the debug flag is set. If you are not using MIT Scheme,
you must either supply your own sort function, or remove the call to
sort, printing the moves in unsorted order for debug purposes. This
program also uses the MIT Scheme random function as a tie-breaker
for moves with the same value. Most Scheme functions have a random
number generating function.

Listing 8.1

Chess Program by Mark Watson

Bug list: (does not handle en passant capture)

Copyright 1990 by Mark Watson

; Define global variables:

(define *wking-moved* #F)


(define *wrook1-moved* #F)
(define *wrook2-moved* #F)
(define *bking-moved* #F)
(define *brook1-moved* #F)
(define *brook2-moved* #F)
156 8. CHESS PLAYING PROGRAM

(define *move-num* #F)


(define *board* #F)
(define *human-square-control* #F)
(define *computer-square-control* #F)
(define *index* #F)
(define *piece* #F)
(define *value* #F)
(define *debug* #F)
(define *old-board* #F)
(define *old-moves* #F)
(define *moves-for-current-piece* #F)

(set! *board* (make-vector 120 0)) ;; board


(set! *computer-square-control* (make-vector 120 0))
(set! *human-square-control* (make-vector 120 0))
(set! *moves-for-current-piece* If)
;; piece index into move table:
(set! *index* '(0 12 15 10 1 6 0 006))
;; piece move table:
(set! *piece* '(0 -1 1 10 -10 0 1 -1 10 -10 -9
-11 9 11 0 8 -8 12 -12
19 -19 21 -21 0 10 20 0 0 0 0 0))

; Set value of the pieces: pawn, knight, bishop,


; rook, queen, and king:

(set! *value* '(0 1 3 3 5 9 0 0 0 25))

Turn on debug output:

(set! *debug* It)

; Remember last board state to allow replaying the


; last move when modifying the program:

(set! *old-board* (make-vector 120 0))


(set! *old-moves* 0)

Use constants to define piece values:

(define PAWN 1)
(define KNIGHT 2)
(define BISHOP 3)
(define ROOK 4)
(define QUEEN 5)
8.3. IMPLEMENTATION 157

(define KING 9)

Format of the board (e.g., square A1 is square 22 and


; square H8 is square 99 in the indexing scheme used
; for the board squares):

Square index: Board with pieces:


------------------- -------------------------

92 93 94 95 96 97 98 99 BR BN BB BQ BK BB BN BR
82 83 84 85 86 87 88 89 BP BP BP BP BP BP BP BP
72 73 74 75 76 77 78 79 X X X X
62 63 64 65 66 67 68 69 X X X X
52 53 54 55 56 57 58 59 X X X X
42 43 45 45 46 47 48 49 X X X X
32 33 34 35 36 37 38 39 WP WP WP WP WP WP WP WP
22 23 24 25 26 27 28 29 WR WN WB WQ WK WB WN WR

Note: Human (white) pieces are positive numbers and


computer (black pieces) are negative numbers. For
0' example, if (vector-ref *board* 55) equals -2, then square
number 55 (04 or Queen 4 in chess notation) has a
black knight. If it equals 2, then the piece on square
0' 55 is a white knight.

; Initialize the board:

(define initChess
(lambda 0
(set! *wking-moved* #f)
(set! *wrook1-moved* #f)
(set! *wrook2-moved* #f)
(set! *bking-moved* #f)
(set! *brook1-moved* #f)
(set! *brook2-moved* #f)
(set! *move-num* 0)
(do «i 0 (+ i 1)))
«equal? i 120))
(vector-set!
*board* i
(vector-ref
(vector
7 7 7 7 7 7 7 7 777 ; empty squares around outside of board
7 7 7 7 7 7 7 7 7 7 7 ; empty squares around outside of board
423 5 9 3 247 7 white pieces
158 8. CHESS PLAYING PROGRAM

1 1 1 1 1 1 1 1 7 7 white pawns
o0 0 0 0 0 0 0 7 7 8 blank squares and 2 empty squares
o0 0 0 0 0 0 0 7 7 8 blank squares and 2 empty squares
o0 0 0 0 0 0 0 7 7 8 blank squares and 2 empty squares
o0 0 0 0 0 0 0 7 7 8 blank squares and 2 empty squares
-1 -1 -1 -1 -1 -1 -1 -1 7 7 ; black pawns
-4 -2 -3 -5 -9 -3 -2 -4 7 7 ; black pieces
7 7 7 7 7 7 7 7 7 7 7 7 7 7 empty squares
7 7 7 7 7 7 7 7 7 7 7) empty squares
i)))))

(define neq
(lambda (a b)
(not (equal? a b))))

Print a piece. If the piece happens to be an


empty square then print the representation for
a black or white square on the board:

(define printPiece
(lambda (piece blackSquare?)
(if (zero? piece)
(if blackSquare? (display II X II) (display II • II))
(begin
(if « piece 0)
(display II B")
(display II W"))
(display (list-ref
'(llplt tlNlt "B" IIRII "Q'I ttll '111 "'1 I'K'I)
(- (abs piece) 1)))))))

Print out the entire board:

(define printBoard
(lambda 0
(let «startColumnList '(92 82 72 62 52 42 32 22)))
(do «i 0 (+ i 1)))
«equal? i 8))
(newline)
(do «j 0 (+ j 1)))
«equal? j 8))
(let* «boardPos (+ (list-ref startColumnList i) j))
(blackSquare?
(member boardPos
'(22 24 26 28 33 35 37 39 42 44 46 48
8.3. 1M PLEM ENTATION 159

53 55 57 59 62 64 66 68 73 75 77 79
82 84 86 88 93 95 97 99))))
(printPiece
(vector-ref *board* boardPos)
blackSquare?))))
(newline))))

; Calculate all possible moves from all squares:

(define posib
(lambda 0
(let «returnedMoveList 'C)))
(do «ii 0 (+ ii 1)))
«equal? ii 78))
(let* «i (+ ii 22))
(boardVal (vector-ref *board* i)))
(if « boardVal 0) ;; valid piece to move?
;; collect all squares to which piece on
;; square i can move to:
(let «move-list (goto i #t))
(aMove #f))
(do «m move-list (cdr m)))
«null? m))
(set! aMove (car m))
(if (and
;; check for either an empty space
;; or opponent piece:
(>= (vector-ref *board* (cadr aMove)) 0)
(neq 7 (vector-ref *board* (cadr aMove))))
(set! returnedMoveList
(cons aMove returnedMoveList))))))))
(if (and
(not *bking-moved*)
(not *brook2-moved*)
(equal? (vector-ref *board* 97) 0)
(equal? (vector-ref *board* 98) 0)
« (vector-ref *human-square-control* 96) 1)
« (vector-ref *human-square-control* 97) 1)
« (vector-ref *human-square-control* 98) 1))
(set! returnedMoveList (cons '00 returnedMoveList)))
(if (and
(not *bking-moved*)
(not *brook1-moved*)
(equal? (vector-ref *board* 95) 0)
(equal? (vector-ref *board* 94) 0)
(equal? (vector-ref *board* 93) 0)
« (vector-ref *human-square-control* 96) 1)
160 8. CHESS PLAYING PROGRAM

« (vector-ref *human-square-control* 95) 1)


« (vector-ref *human-square-control* 94) 1»
(set! returnedMoveList (cons '000 returnedMoveList»)
returnedMoveList»)

; For a given square, return a list of all moves the


; piece on that square can move to:

(define goto
(lambda (squareNum captureFlag)
(let* ((piece (vector-ref *board* squareNum»
(retList '0)
(ivaI '(8 03»
(pieceType (abs piece»
(piece Index 0)
(pieceMovementIndex 0»
(set! pieceIndex (list-ref *index* pieceType»
(set! pieceMovementIndex (list-ref *piece* pieceIndex»
(if
(not (equal? piece 0» make sure that there is
; a piece on square
(case pieceType
((1) ; PAWN
(let ((sideIndex (if « piece 0) -1 +1»)
(do ((cd '(11 9) (cdr cd»)
((null? cd»
;; check for diagonal captures:
(let ((captureDelta (car cd»)
(let*
((movementOffsetInBoard
(+ squareNum (* sideIndex captureDelta»)
(targetPiece
(vector-ref *board* movementOffsetInBoard»)
(if (or
(and
«= targetPiece -1) ; enemy piece --> legal capture
(neq targetPiece 7) ; not off of board
(> piece 0» computer piece moving
(and
(>= targetPiece 1) ; computer piece
(neq targetPiece 7) ; not off of board
« piece 0») player piece moving
(set! retList (cons
(list
squareNum
(+
squareNum
8.3. IMPLEMENTATION 161

(* sideIndex captureDelta)))
retList))))))
;; Check for initial pawn move of two squares forward:
(let* «movementOffsetlnBoard
(+ squareNum (* sideIndex 20))))
(if (and
captureFlag
; move-to sq empty?:
(equal? (vector-ref *board* movementOffsetInBoard) 0)
(equal? (truncate (/ squareNum 10))
(if « piece 0) 8 3))
(if « piece 0)
(equal? (vector-ref *board* (- squareNum 10)) 0)
(equal? (vector-ref *board* (+ squareNum 10)) 0)))
(set!
ret List
(cons (list squareNum
(+ squareNum (* sideIndex 20)))
retList))))
(let*
«movementOffsetInBoard
(+ squareNum (* sideIndex 10))))
(if (and
captureFlag
(equal?
; move-to sq empty?
(vector-ref *board* movementOffsetInBoard) 0))
(set!
retList
(cons (list squareNum
(+ squareNum (* sideIndex 10)))
retList))))))

«2 3 4 5 9) KNIGHT BISHOP ROOK QUEEN KING


(let* «pieceType (abs piece))
(movementTableIndex (list-ref *index* pieceType))
(nextToSquare (+ squareNum
(list-ref *piece* movementTableIndex))))
(do «keep-going-outer-loop #t)) ;; over movement indices
«not keep-going-outer-loop))
(do «keep-going #t))
;; break out of loop if OFF OF BOARD:
«or
(equal? keep-going #f)
(> nextToSquare 99)
« nextToSquare 22)
(equal? (vector-ref *board* nextToSquare) 7)))
(set! retList (cons (list squareNum nextToSquare ) retList))
(if (neq (vector-ref *board* nextToSquare) 0) ; last move
162 8. CHESS PLAYING PROGRAM

; was a capture,
(set! keep-going If»~ so break out of the inner loop.
(if (and
(equal? pieceType 1)
(equal? (truncate (/ squareNum 10» 3»
(set! keep-going If»~ ;; break out of inner loop
(if (or
(equal? pieceType KNIGHT)
(equal? pieceType KING»
(set! keep-going If»~ ;; break out of inner loop
(set! nextToSquare
(+ nextToSquare
(list-ref *piece* movementTableIndex»»
(set! movementTableIndex (+ movementTableIndex 1»
Lack of further move segments is indicated
;; by a zero in the next element of the move
;; index table:
(if (equal? (list-ref *piece* movementTableIndex) 0)
(set! keep-going-outer-loop If»~ ;; no more move segments
(set! nextToSquare
(+ squareNum
(list-ref *piece*,movementTableIndex»»»»
(if (equal? retList It) '0 retList»»

; Return the static evaluation value for a given board position:

(define value
(lambda (toSq)
(let ((retVal 0.0»
(do ((i 0 (+ i 1»)
((equal? i 120»
(vector-set! *computer-square-control* i 0)
(vector-set! *human-square-control* i 0»
;; Calculate the number of times the computer's
;; pieces control each board square:
(do ((ii 0 (+ ii 1»)
((equal? ii 78»
(let ((i (+ ii 22»)
(if « (vector-ref *board* i) 0) computer piece
(let ((moveList (goto i If»~
(pawnFudge 0)
(move '0»
(do ((m moveList (cdr m»)
((null? m»
(set! move (car m»
(if (equal? (abs (vector-ref *board* (car move») 1)
(set! pawnFudge 1.15)
8.3. IMPLEMENTATION 163

(set! pawnFudge 1»
(vector-set!
*computer-square-control*
(cadr move)
(+
(vector-ref
*computer-square-control*
(cadr move»
pawnFudge»»»)

Calculate the number of times the player's


pieces control each square:
(do «ii 0 (+ ii 1»)
«equal? i i 78»
(let «i (+ ii 22»)
(if (> (vector-ref *board* i) 0) ;; computer piece
(let «moveList (goto i If»~ ;; generate moves from square # i
(pawnFudge 0)
(move '0»
(do «m moveList (cdr m»)
«null? m»
(set! move (car m» ;;?? 3/25/95: this was cdr
(if (equal? (abs (vector-ref *board* (car move») 1)
(set! pawnFudge 1.25)
(set! pawnFudge 1»
(vector-set! *human-square-control* (cadr move)
(+
(vector-ref *human-square-control* (cadr move»
pawnFudge»»»)

;; Subtract 1 from the control array


;; for the square being moved to:
(vector-set! *computer-square-control* toSq
(max
o
(- (vector-ref *computer-square-control* toSq) 1»)
;; Set initial value based on board control:
(do «ii 0 (+ ii 1»)
«equal? ii 78»
(let «i (+ ii 22»)
(set! retVal
(+
retVal
(*
0.1
(- (vector-ref *computer-square-control* i)
(vector-ref *human-square-control* i»»»)
164 8. CHESS PLAYING PROGRAM

;; Modify the value based on material advantage,


;; square control, and center control:
(do ((ii 0 (+ ii 1)))
((equal? ii 79))
(let ((i (+ ii 22)))
(if (and
;; not off of the board:
(neq 7 (vector-ref *board* i))
;; not a blank square:
(neq 0 (vector-ref *board* i)))
(let ((control
(- (vector-ref *computer-square-control* i)
(vector-ref *human-square-control* i))))
Piece value:
(if « (vector-ref *board* i) 0)
(set!
retVal computer piece
(+ retVal
(*
(list-ref
*value*
(abs (vector-ref *board* i)))
8)))
(set!
retVal human piece
(- retVal
(*
(list-ref
*value*
(abs (vector-ref *board* i)))
8))))
Check for black piece on white
controlled square:
(if (and
« control 0)
« (vector-ref *board* i) 0))
(set!
retVal
(- retVal
(* 14
(list-ref
*value*
(abs (vector-ref *board* i)))))))
Check for white piece on black
controlled square:
(if (and
(> control 0)
(> (vector-ref *board* i) 0))
(set!
8.3. IMPLEMENTATION 165

ret Val
(+ retVal
(* 2
(min ; limit value of attacked piece to 3
3 ; points since this side to move next
(list-ref
*value*
(abs (vector-ref *board* i»»»»
;; King attacked:
(if (and
(> (vector-ref *human-square-control* i) 0)
(equal? (vector-ref *board* i) -9»
(set! retVal (- retVal 5000»)
Queen attacked:
(if (and
(> (vector-ref *human-square-control* i) 0)
(equal? (vector-ref *board* i) -5»
(set! ret Val (- retVal 50»»»)

Pawn placement heuristics:


(loop over central four squares)
(do ((sq '(55 56 65 66) (cdr sq»)
((null? sq»
(if (equal? (vector-ref *board* (car sq» -1)
(set! retVal (+ retVal 2») ; black pawn
(if (equal? (vector-ref *board* (car sq» 1)
(set! retVal (- retVal 2»» ; white pawn
; Loop over central 16 squares:

(do ((sq
'(44 45 46 47 54 55 56 57 64 65 66 67 74 75 76 77)
(cdr sq»)
( (null? sq»
(if (equal? (vector-ref *board* (car sq» -1)
(set! retVal (+ retVal 1») ; black pawn
(if (equal? (vector-ref *board* (car sq» 1)
(set! retVal (- retVal 1»» ; white pawn
;; Decrease value of moving queen in the first five moves:
(if (and « *move-num* 5) (equal? (vector-ref *board* toSq) -5»
(set! ret Val (- ret Val 10»)
;; Decrease value of moving king in the first 15 moves:
(if (and « *move-num* 15) (equal? (vector-ref *board* toSq) -9»
(set! ret Val (- ret Val 20»)
(+ retVal (random 2»»)

; Convert internal square number to algrabraic notation:


166 8. CHESS PLAYING PROGRAM

(define board-pr
(lambda (sq)
(let* «rank (truncate (/ sq 10)))
(file (- sq (* rank 10))))
(set! file (- file 2))
(set! rank (- rank 1))
(display (list-ref '("A" "B" "C" "D" "E" "F" "G" "H") file))
(display rank))))

; Return a list containing the algrabraic notation for a square.


For example, square number 22 would be converted to (A 1).

(define board-sq
(lambda (sq)
(let* «rank (truncate (/ sq 10)))
(file (- sq (* rank 10))))
(set! file (- file 2))
(set! rank (- rank 1))
(list (list-ref '("A" "B" "C" "D" "E" "F" "G" "H") file)
rank))))

; Find the "best" move:

(define sort-func
(lambda (x y)
(> (cadr x) (cadr y))))

(define Mover
(lambda 0
(set! *move-num* (+ *move-num* 1))
(let «possibleMoves (posib))
(bestMove 'C))
(bestValue -100000)
(to' 0)
(moveValues 'C)) for debug output only
(tosave '0)
(fromsave '0)
(pm' 0)
(newVal 0))
(do «pm-list possibleMoves (cdr pm-list)))
«null? pm-list))
(set! pm (car pm-list))
(set! tosave 0)
(if (equal? pm '00)
(begin
8.3. IMPLEMENTATION 167

(vector-set! *board* 96 0)
(vector-set! *board* 97 -4)
(vector-set! *board* 98 -9)
(vector-set! *board* 99 0)
(set! to 10» ; off of board
(if (equal? pm '000)
(begin
(vector-set! *board* 96 0)
(vector-set! *board* 95 -4)
(vector-set! *board* 94 -9)
(set! to 10) ; off of board
(vector-set! *board* 92 0»
(begin
(set! fromsave (vector-ref *board* (car pm»)
(set! tosave (vector-ref *board* (cadr pm»)
(vector-set!
*board*
(cadr pm)
(vector-ref *board* (car pm»)
(vector-set! *board* (car pm) 0)
(set! to (cadr pm»»)

;; Call value to calculate a numeric score for


;; how "good" this board position is for the computer:
(set! newVal (value to»

;; increase the score slightly for captures:


(if (> tosave 0)
(set! newVal
(+ newVal 12 (* 11 (list-ref *value* tosave»»)
(if (member pm '(00 000» (set! newVal (+ newVal 10»)
(if *debug*
(if (member pm '(00 000»
(set! moveValues
(cons
(list (list pm) newVal)
moveValues»
(set! moveValues
(cons
(list
(append
(board-sq (car pm» '(" to ")
(board-sq (cadr pm»)
newVal)
moveValues»»
(if (> newVal bestValue)
(begin
(set! bestValue newVal)
168 8. CHESS PLAYING PROGRAM

(set! bestMove pm)))


(if (equal? pm '00)
(begin
(vector-set! *board* 96 -9)
(vector-set! *board* 97 0)
(vector-set! *board* 98 0)
(vector-set! *board* 99 -4))
(if (equal? pm '000)
(begin
(vector-set! *board* 96 -9)
(vector-set! *board* 95 0)
(vector-set! *board* 94 0)
(vector-set! *board* 92 -4))
(begin
(vector-set! *board* (car pm) fromsave)
(vector-set! *board* (cadr pm) tosave)))))
(if *debug*
(let «m-values (sort moveValues sort-func)))
(do «x m-values (cdr x)))
«null? x))
(newline)
(do «y (caar x) (cdr y)))
«null? y))
(display (car y)))
(display " : II)
(display (truncate (cadar x))))
(newline)) )
(if « bestValue -1000)
'checkmate
bestMove))))

; Main driver program:

(define (chess . restart)


(if (null? restart) (initChess))
(if (equal? restart '(backup))
;; debug option to back up one move on restart
(begin
(display "Backing up the game by one move")
(newline)
(dotimes (i 120)
(vector-set! *board* i
(vector-ref *old-board* i)))
(set! *moves-for-current-piece* *old-moves*)))
(printBoard)
(let «keep-going #t))
(do 0
«not keep-going))
8.3. IMPLEMENTATION 169

(display "Enter your move (e.g., d2-d4) : ")


(let* ((response (symbol->string (read»)
(moved? #f»
(if (or (equal? response " 00 ") (equal? response "00"»
(if
(and ;; let us be sure that casting king side is legal
(not *wking-moved*)
(not *wrook2-moved*)
(equal? (vector-ref *board* 27) 0)
(equal? (vector-ref *board* 28) 0)
« (vector-ref *computer-square-control* 26) 1)
« (vector-ref *computer-square-control* 27) 1)
« (vector-ref *computer-square-control* 27) 1»
(begin
(set! moved? #t)
(display "Castle King side") (newline)
(vector-set! *board* 26 0) blank old king square
(vector-set! *board* 29 0) ; blank old rook square
(vector-set! *board* 27 4) ; rook
(vector-set! *board* 28 9») ; king
(if (or (equal? response " 000 ") (equal? response "000"»
(if
(and ;; be sure that a queen side castle is legal
(not *wking-moved*)
(not *wrook1-moved*)
(equal? (vector-ref *board* 23) 0)
(equal? (vector-ref *board* 24) 0)
(equal? (vector-ref *board* 25) 0)
« (vector-ref *computer-square-control* 26) 1)
« (vector-ref *computer-square-control* 25) 1)
« (vector-ref *computer-square-control* 24) 1»
(begin
(set! moved? #t)
(display "Castle Queen side") (newline)
(vector-set! *board* 26 0) blank old king square
(vector-set! *board* 22 0) ; blank old rook square
(vector-set! *board* 25 4) ; rook
(vector-set! *board* 24 9»») ; king
(if (equal? (string-length response) 5)
(let*
((fromCol (cadr (assoc
(string (string-ref response 0»
'((" a " 0) ("b" 1) (" c " 2) ("d" 3)
(" e " 4) ("f" 5) (" g " 6) ("h" 7»»)
(fromRow (+
1
(string->number (string (string-ref response 1»»)
(toCol (cadr (assoc
(string (string-ref response 3»
170 8. CHESS PLAYING PROGRAM

'«"a" 0) ("b" 1) ("c" 2) ("d" 3)


("e" 4) ("f" 5) ("g" 6) ("h" 7»»)
(toRow (+ 1 (string->number (string (string-ref response 4»»)
(from (+ (* fromRow 10) fromCol 2»
(to (+ (* toRow 10) toCol 2»)
(set! moved? It)
(vector-set! *board* to (vector-ref *board* from»
(vector-set! *board* from 0»)
(if (not moved?)
(display "What???")
(begin
(printBoard)
;; Remember last state of the board to allow backing up
;; one move for debug by running: (chess 'backup):
(do «i 0 (+ i 1»)
«equal? i 120»
(vector-set! *old-board* i (vector-ref *board* i»)
(set! *old-moves* *moves-for-current-piece*)
;; Calculate the "best" computer move:
(let «bestMove (Mover»)
(if (equal? bestMove 'checkmate)
(begin
(display "Checkmate!! to)
(newline)
(set! keep-going If»~)
(if (equal? bestMove '00)
(begin
(vector-set! *board* 96 0)
(vector-set! *board* 97 -4)
(vector-set! *board* 98 -9)
(vector-set! *board* 99 0)
(set! *bking-moved* It)
(set! *brook2-moved* It)
(newline) (display "00") (newline»
(if (equal? bestMove '000)
(begin
(vector-set! *board* 96 0)
(vector-set! *board* 95 -4)
(vector-set! *board* 94 -9)
(vector-set! *board* 92 0)
(set! *bking-moved* It)
(set! *brook1-moved* It)
(newline) (display "000") (newline»
(begin
(vector-set!
*board*
(cadr bestMove)
(vector-ref *board* (car bestMove»)
(vector-set! *board* (car bestMove) 0)
8.3. I MPLEMENTATIO N 171

(newline) (display "Computer move ")


(board-pr (car bestMove»
(display "_")
(board-pr (cadr bestMove»
(newline»»))
(printBoard»))))

Listing 8.2 shows the beginning of a sample game played against the
program in Listing 8.1.

Listing 8.2
(load "chess.s")
;Loading "chess.s" -- done
;Value: chess

(chess)
BR BN BB BQ BK BB BN BR
BP BP BP BP BP BP BP BP
X X X X
X X X X
X X X X
X X X X
WP WP WP WP WP WP WP WP
WR WN WB WQ WK WB WN WR
Enter your move (e.g., d2-d4)

d2-d4
BR BN BB BQ BK BB BN BR
BP BP BP BP BP BP BP BP
X X X X
X X X X
X WP. X X
X X X X
WPWPWPX WPWPWPWP
WR WN WE WQ WI< WB WN WR

07 to 05 O.
E7 to E6 O.
C7 to C6 -1.
F7 to F5 -1.
G8 to F6 -2.
07 to 06 -2.
B7 to B6 -2.
B7 to B5 -2.
H7 to H5 -2.
A7 to A5 -2.
A7 to A6 -2.
F7 to F6 -2.
172 8. CHESS PLAYING PROGRAM

B8 to C6 -3.
B8 to A6 -3.
G7 to G6 -3.
H7 to H6 -3.
E7 to E5 -10.
C7 to C5 -14.
G7 to G5 -17.
G8 to H6 -45.

Computer move : 07-05

BR BN BB BQ BK BB BN BR
BP BP BP. BP BP BP BP
X X X X
X X BP X X
X WP. X X
X X X X
WP WP WP X WP WP WP WP
WR WN WB WQ WK WB WN WR
Enter your move (e.g., d2-d4)

gl-f3
BR BN BB BQ BK BB BN BR
BP BP BP. BP BP BP BP
X X X X
X X BP X X
X WP. X X
X X X WN X
WPWPWPX WPWPWPWP
WR WN WE WQ WK WB X WR

C8 to G4 6.
E7 to E6 1.
F7 to F5 1.
G8 to F6 1.
B8 to A6 O.
B7 to B6 O.
C7 to C6 O.
A7 to A5 O.
H7 to H5 O.
C8 to 07 O.
B7 to B5 O.
F7 to F6 O.
B8 to C6 O.
C8 to F5 O.
C8 to E6 O.
G7 to G6 O.
H7 to H6 O.
A7 to A6 O.
8.3. IMPLEMENTATION 173

B8 to D7 O.
D8 to D6 -8.
D8 to D7 -9.
E7 to E5 -10.
C7 to C5 -13.
G7 to G5 -14.
E8 to D7 -19.
G8 to H6 -41.
C8 to H3 -41.

Computer move : C8-G4

BR BN. BQ BK BB BN BR
BP BP BP. BP BP BP BP
X X X X
X X BP X X
X WP. X BB X
X X X WNX
WP WP WP X WP WP WP WP
WR WN WB WQ WK WB X WR
Enter your move (e.g., d2-d4)

e2-e3
BR BN BQ BK BB BN BR
BP BP BP. BP BP BP BP
X X X X
X X BP X X
X WP. X BB X
X X WPWNX
WP WP WP X WP WP WP
WR WN WB WQ WK WE X WR

G4 to F3 27.
E7 to E6 O.
C7 to C6 O.
F7 to FS O.
G8 to F6 O.
B8 to D7 O.
G8 to H6 O.
A7 to AS O.
A7 to A6 O.
G4 to E6 O.
H7 to H6 O.
F7 to F6 O.
G4 to 07 O.
B8 to C6 O.
G4 to C8 O.
G4 to F5 -1.
H7 to HS -1.
174 8. CHESS PLAYING PROGRAM

G7 to G6 -1.
B7 to B6 -1.
G4 to H5 -1.
08 to 07 -10.
08 to 06 -10.
E7 to E5 -10.
08 to C8 -11.
C7 to C5 -14.
B7 to B5 -14.
G7 to G5 -14.
E8 to 07 -21.
B8 to A6 -42.
G4 to H3 -43.

Computer move : G4-F3

BR BN. BQ BK BB BN BR
BP BP BP . BP BP BP BP
X X X X
X X BP X X
X WP. X X
X X WP BB X
WPWPWPX WPWPWP
WR WN WB WQ WI< WB X WR
Enter your move (e.g., d2-d4)

dl-f3
BR BN BQ BK BB BN BR
BP BP BP . BP BP BP BP
X X X X
X X BP X X
X WP. X X
X X WP WQ X
WP WP WP X WP WP WP
WR WN WB . WK WB X WR

B8 to C6 O.
E7 to E6 O.
C7 to C6 O.
G8 to H6 -1.
G7 to G6 -1.
B7 to B6 -1.
A7 to A6 -1.
G7 to G5 -2.
A7 to A5 -2.
H7 to H6 -2.
E7 to E5 -10.
08 to 07 -10.
08 to 06 -11.
8.3. IMPLEMENTATION 175

C7 to C5 -12.
F7 to F5 -14.
B8 to 07 -14.
H7 to H5 -15.
B7 to B5 -15.
F7 to F6 -15.
08 to C8 -26.
G8 to F6 -42.
B8 to A6 -42.
E8 to 07 -49.

Computer move : B8-C6

BR X . BQ BK BB BN BR
BP BP BP. BP BP BP BP
X BN X X X
X X BP X . X
X WP. X . X
X X WP WQ X
WP WP WP X . WP WP WP
WRWNWB. WKWBX WR
Enter your move (e.g., d2-d4)
It is interesting to observe how the following seven heuristics guide
move selection to playa very reasonable game of chess:
1. Maximize the amount of material advantage
2. Try to guard your own pieces
3. Try to maximize the mobility of your own pieces while minimizing
the mobility of the enemy pieces
4. Try to control the center squares with pawns
5. Try to control the center squares with pieces
6. Try to control the squares adjacent to both Kings
7. Try to attack enemy pieces with your own pieces of lesser value

Listing 8.3 shows a complete game in which the chess program is


checkmated in 28 moves. The program does not correctly use "x-ray"
analysis: in the game in Listing 8.3 the computer has its queen and
bishop lined up on the same diagonal, attacking a white piece that
is guarded only by the white queen. The computer incorrectly calcu-
lates that it cannot safely capture the white bishop. I have edited the
game shown in Listing 8.3 to remove most of the board displays, and
to remove blank lines to make the listing shorter.

Listing 8,3
(load "chess.s")
;Loading "chess.s" -- done
176 8. CHESS PLAYING PROGRAM

;Value: chess

(set! *debug* #f) turn off debug output


;Value: #t

(chess)
Enter your move (e.g., d2-d4) e2-e4
Computer move : E7-E5

BR BN BB BQ BK BB BN BR
BP BP BP BP X BP BP BP
X X X X
X X BP X
X X WPX X
X X X X
WP WP WP WP WP WP WP
WR WN WB WQ WK WB WN WR
Enter your move (e.g., d2-d4) gl-f3

BR BN BB BQ BK BB BN BR
BP BP BP BP X BP BP BP
X X X X
X X BP. X
X X WPX X
X X X WN X
WP WP WP WP. WP WP WP
WR WN WB WQ WK WB X WR

Computer move : B8-C6

BR X BB BQ BK BB BN BR
BP BP BP BP X BP BP BP
X BN X X X
X X BP X
X X WPX X
X X X WN X
WP WP WP WP WP WP WP
WR WN WB WQ WK WB X WR
Enter your move (e.g., d2-d4) fl-b5
Computer move : G8-E7
Enter your move (e.g., d2-d4) 00
Castle King side

BR X BB BQ BK BB BR
BP BP BP BP BN BP BP BP
X BN X X X
X WB X BP X
X X WPX X
X X X WN X
8.3. IMPLEMENTATION 177

WP WP WP WP WP WP WP
WR WN WB WQ X WR WK

Computer move : F7-F6


Enter your move (e.g., d2-d4) d2-d4

BR X BB BQ BK BB. BR
BP BP BP BP BN BP BP
X BN X BP X
X WB X BP X
X WP WP X X
X X X WN X
WP WP WP X WP WP WP
WR WN WB WQ X WR WK

Computer move : E5-D4


Enter your move (e .g. , d2-d4) f3-d4
Computer move : C6-D4
Enter your move (e.g. , d2-d4) dl-d4
Computer move : E7-G6
Enter your move (e.g. , d2-d4) bl-c3
Computer move : D8-E7
Enter your move (e .g. , d2-d4) cl-d2

BR X BB X BK BB BR
BP BP BP BP BQ BP BP
X X BP BN X
X WB X X X
X WQ WP X X
X WN X X
WP WP WP WB WP WP WP
WR . X X WR WK

Computer move : H7-H5


Enter your move (e.g., d2-d4) c3-d5

BR X BB X BK BB BR
BP BP BP BP BQ BP
X X BP BN X
X WB X WN X X BP
X WQ WP X X
X X X X
WP WP WP WB WP WP WP
WR. X X WR WK

Computer move : E7-D6

BR X BB X BK BB BR
BP BP BP BP X BP
178 8. CHESS PLAYING PROGRAM

X BQ. BP BN X
X WB X WN X X BP
X WQ WP X X
X X X X
WP WP WP WB. WP WP WP
WR. X X WR WK .
Enter your move (e.g., d2-d4) d2-b4 (mistake!)

BR X BB X BK BB. BR
BP BP BP BP X BP.
X BQ. BP BN X
X WB X WN X X BP
WB. WQ WP X X
X X X X
WP WP WP X WP WP WP
WR. X X WR WK

Computer move : C7-C5 (Ouch, I did not notice this move)

BR X BB X BK BB. BR
BP BP X BP X BP.
X BQ. BP BN X
X WB BP WN X X BP
WB. WQ WP X X
X X X X
WPWPWPX WPWPWP
WR. X X WR WK .
Enter your move (e.g., d2-d4) b4-c5

BR X BB X BK BB. BR
BP BP X BP X BP .
X BQ. BP BN X
X WB WB WN X X BP
X WQ WP X X
X X X X
WP WP WP X WP WP WP
WR. X X WR WK

Computer move : D6-E6 (blunder! see the note in text)

BR X BB X BK BB. BR
BP BP X BPX BP .
X X BQ BP BN X
X WB WB WNX X BP
X WQWP X X
X X X X
WP Wp·,JP X WP WP WP
WR. X X WR WK .
Enter your move (e.g., d2-d4) d5-c7
8.3. IMPLEMENTATION 179

Computer move : E8-F7


Enter your move (e.g., d2-d4) b5-c4 (black queen trapped)

BR X BB X BB. BR
BP BP WN BP X BK BP .
X X BQ BP BN X
X WB. X X BP
X WE WQ WP X X
X X X X
WP WP WP X WP WP WP
WR. X X WR WK

Computer move : G6-F4

BR X BB X BB. BR
BP BP WN BP X BK BP .
X X BQ BP. X
X WB. X X BP
X WB WQ WP BN. X
X X X X
WP WP WP X WP WP WP
WR. X X WR WK .
Enter your move (e.g., d2-d4) c4-e6
Computer move : D7-E6
Enter your move (e.g., d2-d4) d4-d8

BR X BB WQ. BB. BR
BP BP WN. X BK BP .
X X BP BP. X
X WB. X X BP
X X WP BN. X
X X X X
WP WP WP X WP WP WP
WR. X X WR WK

Computer move : A8-B8


Enter your move (e.g., d2-d4) d8-e8
Computer move : F7-G8

BR BB X WQ BB BK BR
BP BP WN . X BP .
X X BP BP . X
X WB . X X BP
X X WP BN . X
X X X X
WPWPWPX WP WP WP
WR . X X WR WK .
Enter your move (e.g .• d2-d4) e8-f8
180 8. CHESS PLAYING PROGRAM

BR BB X WQ BK BR
BP BP WN X BP
X X BP BP X
X WB X X BP
X X WP BN X
X X X X
WP WP WP X WP WP WP
WR . X X WR WK

Computer move : G8-H7

BR BB X WQ BR
BP BP WN . X BP BK
X X BP BP X
X WB X X BP
X X WP BN X
X X X X
WP WP WP X WP WP WP
VIR. X X WR WK .
Enter your move (e.g., d2-d4) £8-£7
Computer move : A7-A5

BR BB X X BR
X BP WN X WQ BP BK
X X BP BP X
BP WB X X BP
X X WP BN X
X X X X
WP WP WP X WP WP WP
VIR. X X WR WK .
Enter your move (e.g., d2-d4) c7-e8 (threatens mate!)

BR BB X WN X BR
X BP X X WQ BP BK
X X BP BP X
BP WB X X BP
X X WP BN X
X X X X
WP WP WP X WP WP WP
WR . X X WR WK

Computer move : H8-E8


Enter your move (e .g., d2-d4) £7-e8
Computer move : H7-H6
Enter your move (e.g. , d2-d4) g2-g3
Computer move : F4-E2
Enter your move (e.g. , d2-d4) gl-g2
Computer move : B8-A8
8.3. IMPLEMENTATION 181

BR X BB X WQ X X
X BP X X BP
X X BP BP BK
BP WB X X BP
X X WP X X
X X X WP .
WP WP WP X BN WP WK WP
WR. X X WR X
Enter your move (e.g., d2-d4) al-el (knight trapped)
Computer move : E2-G3
Enter your move (e.g., d2-d4) f2-g3

BR X BB X WQ X X
X BP X X BP
X X BP BP BK
BP WB X X BP
X X WP X X
X X X WP .
WP WP WP X X WK WP
X X WR WR X

Computer move : H6-G5

BR X BB X WQ X X
X BP X X BP
X X BP BP . X
BP WB X BK BP
X X WPX X
X X X WP
WP WP WP X X WK WP
X X WR WR X
Enter your move (e.g., d2-d4) c5-e3 (check)

BR X BB X WQ X X
X BP X X BP
X X BP BP X
BP X X BK BP
X X WP X X
X X WE WP
WP WP WP X X WK WP
X X WR WR X

Computer move : G5-G4

BR X BB X WQ X X
X BP X X BP
X X BP BP X
BP X X BP
X
X X WP X BK X
182 8. CHESS PLAYING PROGRAM

x . X WE. WP.
WP WP WP X X WK WP
X . X WR WR X
Enter your move (e.g., d2-d4)

h2-h3
BR X BB X WQ X X
X BP X X . BP.
. X X BP BP. X
BP. X X X BP
X X WP X BK X
X . X WE. WPWP
WP WP WP X X WK X
X X WR WR X
Checkmate! !

The first move that the white pawn at H2 makes is to checkmate the
black king!
CHAPTER 9
Go PLAYING PROGRAM
Go is an ancient game of strategy that is purported to be over 4,000
years old. My older brother Ron taught me to play Go when I was
about eight years old. The rules of Go are simple, but it is very difficult
to play the game well. As an indication of how difficult it is to write a Go
program, a Taiwanese businessman, Mr. lng, has offered (seriously!) a
cash prize of about one million dollars to the first programmer who can
write a Go playing program that can win a match against a Taiwanese
professional Go player.
Go is an incredibly beautiful game. I have had the privilege of playing
casual games against the women's world champion and the national
champion of South Korea (I was crushed in both games!). Go played
by strong human players has a magical quality not seen in the play
of any computer Go program (yet!). I particularly enjoy playing over
the moves of ancient recorded Go games (see Appendix B for pointers
to Internet Go sites that contain ancient Go games, modem recorded
Go games, and programs for playing Go and for playing recorded Go
games).
The Go playing program developed in this chapter is very simple. It
plays at the level of a human player who is just learning how to play.
People learn to play Go by making mistakes and avoiding making sim-
ilar mistakes in the future. Go programs are very knowledge-intensive;
Go programs become stronger players by noting weak points in the
program's play and adding specialized code to handle special situa-
tions. The Go program developed in this chapter was designed and
implemented to be
• Easy to understand
• Flexible in its module and software architecture so that the pro-
gram can easily be modified and its playing abilities extended to
understand more game situations
• Able to make move selections quickly

I started writing a Go program in 1978 for an Apple II computer


(which I later marketed as "Honinbo Warrior"). However, I have never
184 9. Go PLAYING PROGRAM

written a program which plays a good game of Go by human standards.


My Go program for the Apple II evolved over many years by cataloging
mistakes that the program made and adding specialized code for han-
dling those situations. My fascination with Go programs is shared by
many programmers around the world for the following reasons:

• Go cannot be programmed using brute-force search methods like


those used successfully in chess playing programs. The heuristic
knowledge used by Go programs must be slowly and methodically
developed by studying the program's playing ability in real game
situations
• The rules of Go are simple
• Mr. Ing has offered a generous prize fund to stimulate computer
Go competition

The problem of writing a Go playing program is an excellent way to


end this book. In this chapter we will put together all of the elements
of program construction that we have covered so far in this book:

• The importance of preliminary requirements analysis


• Rapid prototyping to improve understanding of the data structures
that will be required for a Go playing program
• A design process influenced by early experimentation with Go data
structures and the low-level Scheme functions that manipulate
those data structures
• An iterative approach of refining analysis, design, and staged
implementations

We will develop a flexible file module architecture in Section 9.2


for organizing the components of the Go playing program. Part of the
software development plan for building the Go program is to deter-
mine our strategy for incrementally analyzing the requirements for
designing and building a series of staged implementations that can be
easily tested. I developed the program in this chapter in one weekend
by following these steps:

1. Analyzed the game of Go


2. Described in plain language the data structures required to store
the state of current position on a Go board, adding data structures
to store results that could be reused
3. Designed a file module architecture based on six physical files
4. Interactively prototyped the data structures in a Scheme listener
window, including writing small pieces of code to create and access
data
9. Go PLAYING PROGRAM 185

5. Wrote the first file module GO_INITS, which contained the Scheme
functions required to initialize data structures for a Go game and
to access these data structures in other Scheme functions
6. Unit tested (i.e., "debugged") file module GO_INIT.S
7. Designed algorithms for utility calculations (e.g., checking for con-
ditions where stones on the Go board are touching, changing and
coalescing group IDs, updating the liberty counts of groups, adding
new stones to the board and updating all data structures, removing
groups of stones, and printing the current Go board)
8. Implemented the Go data utility functions and placed them in the
file module GO_DATA.S
9. Unit tested (i.e., "debugged") file module GO_DATA.S
10. Designed a minimal user interface for playing Go, including hooks
for future functions to refine computer move selection by using the
tactics and strategy file modules
11. Implemented the minimal user interface for playing Go and placed
these functions in file module GO_PLAYS
12. Unit tested (i.e., "debugged") file module GO_PLAYS
13. Designed and implemented simple graphics utilities for plotting
a Go board position, and placed these Scheme functions in file
module GO_PLOTS
14. Unit tested file module GO_PLOTS
15. Analyzed basic Go tactics that would be relatively simple to
implement
16. Designed algorithms for simple Go tactics
17. Implemented the tactics functions and placed them in file module
GO_TACTS
18. Unit tested file module GO_TACTS
19. Analyzed basic Go strategies that would be relatively simple to
implement
20. Designed algorithms for simple Go strategies
21. Implemented the strategy functions and placed them in file module
GO_STRATS
22. Unit tested file module GO_STRATS
23. Tested the entire game, returning to previous steps as required to
refine my analysis, design, and staged implementations

Individually, these steps were all fairly easy to accomplish. Hav-


ing a development plan helped me to focus on individual tasks, while
not losing sight of the entire development effort. The most impor-
tant requirement that I had in developing the example program for
this chapter was to provide a good, complete example of writing a
non-trivial program. I also wanted to make the architecture for the
Go playing program extensible, both for my own future programming
186 9. Go PLAYING PROGRAM

enjoyment, and to provide interested readers with a good foundation


for creating their own Go programs.

9. 1 REDUIREMENTS AND ANALYSIS


We wrote a chess playing program in Chapter 8. I assumed that most
readers would be familiar with the rules of chess. I do not make that
assumption for the game of Go. Appendix B lists several Internet sites
with interesting material on the game of Go, including tutorials, famous
games, and Go programs for several types of computers.

9.1.1. THE RULES OF Go

The object of the game of Go is to secure territory on the Go board. Go


is usually played on a board with 19x19 grid points formed by 19 verti-
cal lines and 19 horizontal lines. Go is often played on smaller boards
for teaching beginners, and some Go programs support smaller board
sizes to allow games to be played more rapidly. It is also easier for a
computer program to deal with a smaller board, since the number of
legal moves to evaluate is dramatically reduced. The program devel-
oped in this chapter supports board sizes of 4x4, sxs, 6x6, 7x7, 8x8,
9x9, llxll, 13x13, and 19x19. Play on the smaller board sizes is not
meaningful except for writing and debugging the low-level functions to
manipulate the Go data structures.
Figures 9.1 through 9.5 illustrate playing Go on a tiny, SxS board. In
practice, we would never use such a small board except for learning the
rules and for working on our Go program. The complete Go program
developed in this chapter plays a reasonable game on a 9x9 board.
In Figure 9.1 the black stones numbered 1 and 3 are touching. The
white stone numbered 2 is touching black stone 3 but is not touching
black stone 1. Stones of the same color that are touching belong to the
same group. A stone played on an empty Go board (assuming that it
is not placed on the edge of the board) has four lines from the stone's
position to other empty positions on the Go board; we say that this stone
has four liberties. Members of the same group share liberties. Any stone
or group that has no liberties dies, and is removed from the board. After
black plays stone 3 in Figure 9.1, the single black group (stones 1 and
3) has 5 liberties and the single white stone 2 (forming a group of only
one stone) has three liberties. After the control of territory on the Go
board, the capture (and the threat of capture!) of enemy stones is the
most important aspect of the game of Go. Strategically, it is often very
9.1. REQUIREMENTS AND ANALYSIS 187

1
A B c D E
FIGURE 9.1. Three moves played on a tiny, 5x5 Go board. Black moves first. Once a stone is
placed on the board, it is never moved unless it is captured and removed from the board.

important to link different groups into a single group in order to share


liberties.
Figure 9.2 shows additional Go stones played; the white stones in the
lower left corner are crowded and subject to attack by being surrounded
on the outside. Go is a game of territorial expansion: by crowding the
white stones into the corner, black lays claim to the outer (larger) ter-
ritory. As an added benefit to black, the white stones in the corner will
eventually be captured by reducing their liberty count to zero.
When the liberty count of a group is reduced to zero, the group is
dead, and the stones in the group are removed from the board. Fig-
ures 9.4 and 9.5 demonstrate the capturing of three white stones. The
situation in Figures 9.4 and 9.5 is not realistic.
A stone placed at the edge of the board can have at most three liber-
ties, and is therefore potentially easier to capture. In actual game play,
188 9. Go PLAYING PROGRAM

1
A B c E
FIGURE 9.2. This is the same game as shown in Figure 9.1 with five additional stones played.

stones are only placed on the edge for tactical purposes, like reducing
the liberty count of an enemy group of stones that is under attack.

9. 1.2. DATA STRUCTURES FOR MAINTAINING INFORMATION REOUIREO TO PLAY Go


We will require the following data for specifying the state of a Go game
and to provide data to efficiently calculate new moves on the Go board:
1. The size of the Go board being used
2. A two-dimensional array to represent the grid points on the Go
board
3. A two-dimensional array to indicate the group number, if any, for
a stone at each possible grid point on the Go board
4. A two-dimensional array to indicate the liberty count for each
possible stone position on the Go board
5. A two-dimensional array indicating scores for possible moves at
each possible grid point on the Go board. This information is saved
9.1. REQUIREMENTS AND ANALYSIS 189

A B c E
FIGURE 9.3. This is the same game as shown in Figure 9.2 with three additional stones played.
The white stones can be captured. Black continues the encirclement of the white stones.

from previous game turns and updated to make the program more
efficient
6. A counter used for numbering new groups as they are formed

The reader should note that we are not designing these data struc-
tures yet. We are, based on our current understanding of the game of
Go, noting the types of information that we will need to maintain in our
Go playing program. The two-dimensional array that maintains scores
for moves on the Go board is especially important: these calculated
scores will be reused when possible over multiple moves. Also, differ-
ent components of the program (i.e., tactics and strategy) can write
data to this array independently, separating tactics, strategy, and final
move selection implementations.
190 9. Go PLAYING PROGRAM

1
A B c
FIGURE 9.4. It is Black's turn to play. The three white stones at locations C-1, 0-1, and E-1 form
a single group with only one liberty. Black to play can capture the three white stones. In an
actual game White would never play along the edge of the board.

9.2 MODULE ARCHITECTURE


As we iteratively analyze the requirements of a Go program, design the
software, and write staged implementations of the program, we will
need to organize the following information:
1. Text and drawings for requirements analysis
2. Text and drawings for software design
3. Software module architecture specifying which Scheme functions
in our program belong in the different source files that we use
The reader will also want to organize her software projects in a simi-
lar way. Like analysis and design, a module architecture will also change
over the life of a software project. The following list shows the six file
9.2. MODULE ARCHITECTURE 191

1
A c D E
FIGURE 9.5. This is a continuation of the position found in Figure 9.4. Black has just played at
location B-1 to capture the three white stones seen in Figure 9.4.

modules that we will use to hold the Go playing program. The optional
fourth file module is used for plotting a Go board using the portable
graphics library developed in Section 4.3.

1.GO_INIT.S contains functions to initialize Go data structures at the


beginning of a game
2. GO_DATA.S contains functions to maintain the Go data structures
while playing the game
3. GO_PLAYS contains functions for playing the game, using the utility
functions in file module GO_DATA.S
4. GO_PLOT.S contains functions for plotting any size of Go board and
the stones played (optional)
5. GO_TACT.S contains functions to suggest good moves based on
short-term tactics
192 9. Go PLAYING PROGRAM

6. GO_STRAT.S contains functions to suggest good moves based on


long-term strategies

If the reader is interested in writing a Go program, the file modules


GO_INITS, GO_DATA.S, GO_PLAYS, and GO_PLOTS can be reused as is.
The reader can replace or extend the game logic in file modules GO_
TACTS and GO_STRATS to create his own Go playing program.

9.3 INTERACTIVELY PROTOTYPING DATA STRUCTURES


Before settling down to a detailed design, I usually like to prototype
the data structures for the program that I am working on. Even though
the data structures may change and the initial Scheme code to create
and modify the data structures may be discarded I find that what is
most important in the early stages of writing a program is to really
understand the problem being solved. I view initial prototyping as an
exercise to understand both the problem that we are solving and the
low-level tools that we will develop to build the program.
We listed in Section 9.1.2 the types of data that we want to use to
maintain the state of a Go game, and to make the program efficient.
Listing 9.1 shows file GO_INIT.S found on the example program disk
included with this book.

Listing 9.1

;; File: go_init.s

Description: Building a Go library interactively in Scheme.


This file contains Scheme functions to initialize
the data structures for a new Go game.

Copyright 1995, Mark Watson. All source code rights reserved.


The contents of this file can be used in compiled form
without restriction.

(define COMPUTER 1)
(define HUMAN 2)

(define make-board
(lambda (size)
9.3. INTERACTIVELY PROTOTYPING OATA STRUCTURES 193

(let «v (make-vector size)))


(do «i 0 (+ i 1)))
«> i (- size 1)))
(vector-set! v i (make-vector size 0)))
;; we must loop (see text) v)))

(define board-ref
(lambda (brd row column)
(vector-ref (vector-ref brd row) column)))

(define board-set!
(lambda (brd row column value)
(vector-set! (vector-ref brd row) column value)))

make a new Go game object:

(define (make-Go-game size . number-of-handicap-stones)


(let «g (vector
size
;; this is the actual board:
(make-board size)
;; this 20 array holds the group 10 indices
;; at each board location:
(make-board size)
;; this 20 array holds the current
;; liberty count at each board location:
(make-board size)
this 20 array holds move values ranging
;; from zero for "do not move here", to
;; large positive integer values for good moves:
(make-board size)
;; counter for creating new groups:
0)))
(set-Joseki g) ;; set the good move table
g))

(define copy-Go-object
(lambda (old-Go-game)
(let* «size (game-size old-Go-game))
(new-Go-game (make-Go-Game size))
(old-board (game-board old-Go-game))
(new-board (game-board new-Go-game))
(old-groups (game-groups old-Go-game))
(new-groups (game-groups new-Go-game))
(old-liberties (game-liberties old-Go-game))
(new-liberties (game-liberties new-Go-game))
(old-move-values (game-move-values old-Go-game))
(new-move-values (game-move-values new-Go-game)))
(game-group-counter-set!
194 9. Go PLAYING PROGRAM

new-Go-game
(game-group-counter old-Go-Game»
(do ((row 0 (+ row 1»)
((>= row size»
(do ((colO (+ col 1»)
((>= col size»
(board-set! new-board row col (board-ref old-board row col»
(board-set! new-groups row col (board-ref old-groups row col»
(board-set! new-liberties row col (board-ref old-liberties row col»
(board-set! new-move-values row col
(board-ref old-move-values row col»»
new-Go-game»)

Utilities for accessing the data structures in a game object:

(define (game-size game) (vector-ref game 0»


(define (game-board game) (vector-ref game 1»
(define (game-groups game) (vector-ref game 2»
(define (game-liberties game) (vector-ref game 3»
(define (game-move-values game) (vector-ref game 4»
(define (game-group-counter game) (vector-ref game 5»
(define (game-group-counter-set! game value) (vector-set! game 5 value»

(define set-Joseki
(lambda (game)
(let ((size (vector-ref game 0»)
(if (equal? size 5)
(vector-set!
game
4
(vector
(vector 0 0 0 0 0)
(vector 0 1 1 1 0)
(vector 0 1 4 1 0)
(vector 0 1 1 1 0)
(vector 0 0 0 0 0»»
(if (equal? size 6)
(vector-set!
game
4
(vector
(vector 0 0 0 0 0 0)
(vector 0 1 1 1 1 0)
(vector 0 1 4 4 1 0)
(vector 0 1 4 4 1 0)
(vector 0 1 1 1 1 0)
(vector 0 0 0 0 0 0»»
(if (equal? size 7)
(vector-set!
9.3. INTERACTIVELY PROTOTYPING DATA STRUCTURES 195

game
4
(vector
(vector 0 0 0 0 0 0 0)
(vector 0 1 1 0 1 1 0)
(vector 0 1 3 4 3 1 0)
(vector 0 0 4 3 4 0 0)
(vector 0 1 3 4 3 1 0)
(vector 0 1 1 0 1 1 0)
(vector 0 0 0 0 0 0 0))))
(if (equal? size 8)
(vector-set!
game
4
(vector
(vector 0 0 0 0 0 0 0 0)
(vector 0 1 0 0 0 0 1 0)
(vector 0 0 3 4 4 3 0 0)
(vector 0 2 4 4 4 4 2 0)
(vector 0 2 4 4 4 4 2 0)
(vector 0 0 3 4 4 3 0 0)
(vector 0 1 0 0 0 0 1 0)
(vector 0 0 0 0 0 0 0 0))))
(if (equal? size 9)
(vector-set!
game
4
(vector
(vector o 0 0 0 0 0 0 0 0)
(vector o1 0 0 0 0 0 1 0)
(vector o0 3 4 2 4 3 0 0)
(vector o0 4 4 3 4 4 0 0)
(vector o0 3 2 2 2 3 0 0)
(vector o0 4 4 3 4 4 0 0)
(vector o0 342 430 0)
(vector o 1 0 0 0 001 0)
(vector o0 0 0 0 0 0 0 0))))

(if (equal? size 11)


(vector-set!
game
4
(vector
(vector 0 0 0 0 0 0 0 0 0 0 0)
(vector 0 0 0 0 0 1 0 0 0 0 0)
(vector 0 0 3 3 0 0 0 0 3 0 0)
(vector 0 0 0 0 0 0 0 0 0 0 0)
(vector 0 0 0 0 0 0 0 0 0 0 0)
(vector 0 0 2 0 0 0 0 0 2 0 0)
196 9. Go PLAYING PROGRAM

(vector 0 0 0 0 0 0 0 0 0 0 0)
(vector 0 0 0 0 0 0 0 0 0 0 0)
(vector 0 0 0 2 0 0 0 0 3 0 0)
(vector 0 0 2 0 0 0 0 0 0 0 0)
(vector 0 0 0 0 0 0 0 0 0 0 0))))

(if (equal? size 13)


(vector-set!
game
4
(vector
(vector 0 0 0 0 0 0 0 0 0 0 0 0 0)
(vector 0 0 0 0 0 0 0 0 0 0 0 0 0)
(vector 0 0 0 0 0 0 0 0 0 0 0 0 0)
(vector 0 0 0 3 0 2 0 0 0 3 0 0 0)
(vector 0 0 0 0 0 0 0 0 0 0 0 0 0)
(vector 0 0 0 0 0 0 0 0 0 0 0 0 0)
(vector 0 0 0 2 0 0 0 0 0 2 0 0 0)
(vector 0 0 0 0 0 0 0 0 0 0 0 0 0)
(vector 0 0 0 0 0 0 0 0 0 0 0 0 0)
(vector 0 0 0 3 0 0 2 0 0 3 0 0 0)
(vector 0 0 0 0 0 0 2 0 0 0 0 0 0)
(vector 0 0 0 0 0 0 0 0 0 0 0 0 0)
(vector 0 0 0 0 0 0 0 0 0 0 0 0 0))))

(if (equal? size 15)


(vector-set!
game
4
(vector
(vector 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
(vector 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
(vector 0 0 0 0 0 0 0 2 0 0 0 0 0 0 0)
(vector 0 0 0 3 0 0 0 0 0 0 0 3 0 0 0)
(vector 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
(vector 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
(vector 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
(vector 0 0 2 0 0 0 0 0 0 0 0 0 2 0 0)
(vector 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
(vector 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
(vector 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
(vector 0 0 0 3 0 0 0 0 0 0 0 3 0 0 0)
(vector 0 0 0 0 0 0 0 2 0 0 0 0 0 0 0)
(vector 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
(vector 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0))))

(if (equal? size 19)


(vector-set!
game
9.3. INTERACTIVELY PROTOTYPING DATA STRUCTURES 197

4
(vector
(vector 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
(vector 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
(vector 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0)
(vector 0 0 0 3 0 0 0 0 0 2 0 0 0 0 0 3 0 0 0)
(vector 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
(vector 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0)
(vector 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
(vector 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
(vector 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
(vector 0 0 0 2 0 0 0 0 0 0 0 0 0 0 0 2 0 0 0)
(vector 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
(vector 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
(vector 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
(vector 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0)
(vector 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
(vector 0 0 0 3 0 0 0 0 0 2 0 0 0 0 0 3 0 0 0)
(vector 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
(vector 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
(vector 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0»»»)

The function make-board creates a two-dimensional array with each


element initialized to the value zero. The built-in Scheme function
make-vector takes an optional second argument that is used as the ini-
tial value for each element in the vector. The board data structure
is a one-dimensional vector, where each element of the vector is an-
other one-dimensional vector. You might think that you could write
make-board like this:

(define make-board (lambda (size)


(make-vector size (make-vector size»»

This, however, will not work, because the second, inner call to make-
vector is made only once; in other words, each element of the first vector
has the same value (points to the same vector). The definition of make-
board in Listing 9.1 works correctly.
The functions board-ref and board-set! in Listing 9.1 are similar to
the built-in Scheme functions vector-ref and vector-set! that operate on
vectors. The only difference is an extra calling argument to access a
specified row and column in the array. I refer to row and column indices
since these terms map nicely onto a Go board, with row indices labeled
1, 2, 3, ... and column indices labeled A, B, C, ... as seen in Figures
9.1 through 9.5. However, as with Scheme vectors, the indexing of a
two-dimensional board array is "zero indexed:' that is, array indices
start counting at zero rather than one.
I refer to board data structures whenever I want to use a two-
dimensional array of data that maps, one for one, onto a Go board.
198 9. Go PLAYING PROGRAM

We will use two-dimensional board arrays created with the function


make-board in Listing 9.1, to store Go stone locations on the Go board,
stone group indices, liberty count values, and for scratch storage for
calculating liberty counts, etc.
The function make-Go-game in Listing 9.1 was written after prototyp-
ing this function in a Scheme listener window. This function creates a
new data object that is a vector containing the following data:
• Size of the Go board (a Go board is always square, so a single
number suffices for the board size)
• Storage for the Go board to hold stone positions; this storage is
created with function make-board
• Storage for group indices; this storage is created with function
make-board
• Storage for group liberty counts; this storage is created with
function make-board
• Storage for possible move values; this storage is created with
function make-board

The function set·Joseki simply fills in some reasonable early game


moves into the possible move value array. The term Joseki refers to the
opening moves of a Go game.

9.4 Low LEVEL SCHEME FUNCTIONS TO MANIPULATE


Go DATA STRUCTURES
Before considering in detail how to design and implement a Go play-
ing program, I wanted to experiment with data structures and Scheme
code for performing low-level "bookkeeping" functions, like counting
the number of liberties of a specified group of stones. I also wanted to
collect all utility functions in one File Module, so they could be imple-
mented and tested, then reused in the tactical and strategic modules
of the Go playing program. Some of the functions in Listing 9.2 (file
GO_DATA.S) were written later, when I was writing the first implemen-
tation of the tactical or strategic modules, and moved back into the file
module GO_DATA.S.

When you develop large Scheme programs, it is very important oc-


casionally to revisit your decisions as to what functions to place in
which file modules (part ofthe Module Architecture), and also to look
for duplicated code that can be generalized and moved into more
general-purpose library file modules. This process is very similar to
9.4. Low LEVEL SCHEME FUNCTIONS TO MANIPULATE Go OATA STRUCTURES 199

modifying the class hierarchy in object-oriented languages like c++:


similar class definitions may have common code removed and placed
in a new class which is a superclass of the two original classes (the
two original classes are called derived classes, and they inherit both
the data structures and functional behavior of the superclass). There
are several popular object-oriented extensions to Scheme: SCOOPS,
BaS, Tiny CLOS, YASOS, and Meroon; these extensions can be found
on the Internet (see Appendix B).

The following is a list of utilities that are contained in the file module
GO_DATA.S in Listing 9.2:
• Count the number of enemy stones touching a specified grid point
on the Go board
• Count the number of friendly stones touching a specified grid point
on the Go board
• Change all stones with a specified group ID to a new group ID
• Update the liberty count of a specified group of connected stones
• General utility to perform all "bookkeeping" for adding a stone to
a Go board
• Remove a stone from the Go board and update all Go program data
• Return a list of all stones connected to a specified Go board grid
point
• Print out the stone positions on the Go board (see Listing 9.7)
Listing 9.2 shows the final version of the file module GO_DATA.S.

Listing 9.2
;; File: GO_DATA.S

Description: Building a Go library for maintaining


Go data structures

Copyright 1995, Mark Watson. All source code rights reserved.


The contents of this file can be used in compiled form
without restriction.

File module dependencies:

File GO INIT.S must be loaded before this file.

(define touched-enemy-stones
(lambda (game side-to-play row column)
(touched-friend-stones game (- 3 side-to-play) row column)))
200 9. Go PLAYING PROGRAM

(define touched-friend-stones
(lambda (game side-to-play row column)
(let ((ret '0)
(size (game-size game»
(board (game-board game»)
(if (> row 1)
(if (equal? side-to-play (board-ref board (- row 1) column»
(set!
ret
(cons (list (- row 1) column) ret»»
(if « row (- size 1»
(if (equal? side-to-play (board-ref board (+ row 1) column»
(set!
ret
(cons (list (+ row 1) column) ret»»
(if (> column 1)
(if (equal? side-to-play (board-ref board row (- column 1»)
(set!
ret
(cons (list row (- column 1» ret»»
(if « column (- size 1»
(if (equal? side-to-play (board-ref board row (+ column 1»)
(set!
ret
(cons (list row (+ column 1» ret»»
ret»)

(define change-group-ID
(lambda (game old-ID new-ID)
(let ((groups (game-groups game»
(size (game-size game»)
(do ((row 0 (+ row 1»)
((> row (- size 1»)
(do ((colO (+ col 1»)
((> col (- size 1»)
(if (equal?
(board-ref groups row col)
old-ID)
(board-set! groups row col new-ID»»
(update-liberties game new-ID»»

(define update-liberties
(lambda (game group-num)
(let* ((size (game-size game»
(scratch (make-board size»
(groups (game-groups game»
(board (game-board game»
(group-liberties (game-liberties game»
(num-lib 0»
9.4. Low LEVEL SCHEME FUNCTIONS TO MANIPULATE Go OATA STRUCTURES 201

(do ((i 0 (+ i 1»)


((>= i size»
(do ((j 0 (+ j 1»)
((>= j size»
(if (equal?
group-num
(board-ref groups i j»
(begin
(do ((ii 0 (+ ii 1»)
((>= ii2»
(let ((iii (+ i 1 (* -2 ii»»
(if (and
(> iii -0
« iii size»
(if (and
(equal?
(board-ref board iii j)
0)
(equal?
(board-ref scratch iii j)

(begin
(board-set! scratch iii j 1)
(set! num-lib (+ num-lib 1»»»
(let ((jjj (+ j 1 (* -2 ii»»
(if (and
(> jjj -1)
« jjj size»
(if (and
(equal?
(board-ref board i jjj)
0)
(equal?
(board-ref scratch i jjj)

(begin
(board-set! scratch i jjj 1)
(set! num-lib (+ num-lib 1»»»»»)
(if (> num-lib 0)
(do ((i 0 (+ i 1»)
((>= i size»
(do ((j 0 (+ j 1»)
((>= j size»
(if
(equal?
group-num
(board-ref groups i j»
(board-set! group-liberties i j num-lib»»
(begin
202 9. Go PLAYING PROGRAM

(display "Group ")


(display group-num)
(display" is dead. ")
(newline)
(remove-group game group-num))))))

(define add-stone
(lambda (game side-to-play row column)
(let «touched-friends
(touched-friend-stones game side-to-play row column))
(board (game-board game))
(groups (game-groups game))
(group-num (+ (game-group-counter game) 1)))
(game-group-counter-set! game group-num)
(board-set! (game-groups game) row column group-num)
(board-set! (game-board game) row column side-to-play)
;; loop over all friendly touched stones, changing all
;; group IDs of touched stones to 'group-num':
(do «grp touched-friends (cdr grp)))
«null? grp))
(let «a-row (caar grp))
(a-col (cadar grp)))
(let «a-grp-num (board-ref groups a-row a-col)))
(change-group-ID game a-grp-num group-num))))
(let «touched-stones
(append
(touched-friend-stones game COMPUTER row column)
(touched-enemy-stones game COMPUTER row column)))
(touched-groups 'C)))
(set! touched-groups
(map
(lambda (grid)
(let «row (car grid))
(col (cadr grid)))
(board-ref groups row col)))
touched-stones))
(display "touched groups: ")
(display touched-groups)
(newline)
;; note: should remove duplicates in list here
(do «group touched-groups (cdr group)))
«null? group))
(update-liberties game (car group))))
(update-liberties game (board-ref groups row column)))))

(define remove-group
(lambda (game group-num)
(let «size (game-size game))
(groups (game-groups game))
9.4. Low LEVEL SCHEME FUNCTIONS TO MANIPULATE Go DATA STRUCTURES 203

(board (game-board game))


(group-liberties (game-liberties game)))
(do «i 0 (+ i 1)))
«>= i size))
(do «j 0 (+ j 1)))
«>= j size))
(if
(equal?
group-num
(board-ref groups i j))
(begin
(board-set! group-liberties i j 0)
(board-set! groups i j 0)
(board-set! board i j 0))))))))

Return a list of all empty grid points on the Go board that are
touching a specified group:

(define group-attachment
(lambda (game group-num)
(let* «size (game-size game))
(scratch (make-board size))
(groups (game-groups game))
(board (game-board game))
(ret-list '0))
(do «i 0 (+ i 1)))
«>= i size))
(do «j 0 (+ j 1)))
«>= j size))
(if (equal?
group-num
(board-ref groups i j))
(begin
(do «ii 0 (+ ii 1)))
«>= ii 2))
(let «iii (+ i 1 (* -2 ii))))
(if (and
(> iii -1)
« iii size))
(if (and
(equal?
(board-ref board iii j)
0)
(equal?
(board-ref scratch iii j)
0))
(begin
(board-set! scratch iii j 1)))))
(let «jjj (+ j 1 (* -2 ii))))
204 9. Go PLAYING PROGRAM

(if (and
(> jjj -1)
« jjj size))
(if (and
(equal?
(board-ref board i jjj)
0)
(equal?
(board-ref scratch i jjj)
0))
(begin
(board-set! scratch i jjj 1))))))))))

(do «i 0 (+ i 1)))
«>= i size))
(do «j 0 (+ j 1)))
«>= j size))
(if
(not
(equal?
o
(board-ref scratch i j)))
(set! ret-list (cons (list i j) ret-list)))))
ret-list)))

Return the number of stones in a specified group:

(define group-count
(lambda (game group-num)
(let «size (game-size game))
(groups (game-groups game))
(ret-count 0))
(do «row 0 (+ row 1)))
«>= row size))
(do «colO (+ col 1)))
«>= col size))
(if (equal?
group-num
(board-ref groups row col))
(set! ret-count (+ ret-count 1)))))
ret-count)))

(define print-move
(lambda (row col)
(let «col-names '(IIA" liB lie" "D IIE "Fit "Gil "HII "Itt "J
II
II II II

"K" IIL" I'M'I "N" "D" Ilpl' IIQt' f'R" "S11 t'T"»
(row-names '("1 11 "2" "3" "4" "5" "6" "7" "8"
"9" "10" "11" "12" "13" "14" "15" "16"
9.4. Low LEVEL SCHEME FUNCTIONS TO MANIPULATE Go DATA STRUCTURES 205

"17" "18" "19" "20"»)


(display (list-ref col-names col»
(display (list-ref row-names row»»)

(define print-board
(lambda (game)
(let ((col-names' ("A" "B" "CD "D" "E" "F" "Gil "H" "I" "l"
11K" IlL" '1M" IIN'I "Oil Ilplt IIQtl "R'f IISlt IIT II ) )
(row-names ,(n 1 11 II 2 11 II 3 11 II 4" " 5 1t II 6 1t II 7" II 8 1t
" 9" "10" "11" "12" "13" "14" "15" "16"
"17" "18" "19" "20"»
(size (game-size game»
(board (game-board game»)
(newline)
(do ((row 0 (+ row 1»)
((>= row size»
(newline)
(display (list-ref row-names (- size row 1»)
(display ": II)
(do ((colO (+ coIl»)
((>= col size»
(if (equal?
(board-ref board (- size row 1) col)
0)
(display "-1-"»
(if (equal?
(board-ref board (- size row 1) col)
COMPtITER)
(display "_C_"»
(if (equal?
(board-ref board (- size row 1) col)
HUMAN)
(display "-H-"»)
(newline»
(newline)
(display II II)
(do ((colO (+ coIl»)
((>= col size»
(begin
(display II II)
(display (list-ref col-names col»
(display II II»)
(newline)
(newline»»

Listing 9.2 shows the final version of the file module GO_DATA.S. Sev-
eral of the functions were added after the initial implementation; for
example, the utility function group-attachments was originally written
206 9. Go PLAYING PROGRAM

as part of the tactical module GO_TACT.S, and then moved to the data
structures module GO_DATA.S.
The function touched-friend-stones simply returns a list of coordinates
(i.e., grid positions on the Go board) of any friendly stones by checking
the neighboring points. This function is reused in function touched-
enemy-stones by changing the side-to-play indicator (second argument
to both functions).
The function change-group-ID simply changes all occurrences of the
old group 10 value to the new group 10 value in the group 10 array in
a Go game data structure.
The function update-liberties looks complicated, but the idea behind
the function is simple: we create a scratch board array (with all values
automatically initialized to zero). We then loop over every point on the
Go board looking for connected stones belonging to the group that we
are updating. For each stone in the group we look for empty connected
(adjacent) grid positions on the Go board and set the corresponding
point in the scratch array to one if its value is zero. We keep a running
count of the number of entries in the scratch array that we set to one,
and this will be the liberty count for the entire group. This process
allows us to count liberties without double-counting empty grid points
that are adjacent to two separate stones in the same group. Function
update-liberties also removes the specified group if it finds that the group
has zero liberties.
The function add-stone is a general-purpose utility that manages the
process of adding a stone to the Go board and updating all relevant data
structures in a Go game object. function add-stone uses other functions
in modules GO_DATA.S and GO_INIT.S:
touched-friend-stones
touched-enemy-stones
game-board
game-group-counter
game-group-counter-set!
board-set!
board-ref
change-group-ID
update-liberties

The function remove-group looks for all entries in the groups array
in a Go game object, and sets the corresponding entries in the liber-
ties, groups, and board arrays in the same Go game object to zero. The
function print-board simply prints out a Go board position (see Listing
9.7).
In order to unit test the functions in file module GO_DATA.S, the mod-
ule GO_PLAYS was written to provide a simple user interface for making
moves on the Go board for both the human and computer players.
9.4. Low LEVEL SCHEME FUNCTIONS TO MANIPULATE Go DATA STRUCTURES 207

Module GO_PLAV.s contains "hooks" to call functions in modules GO_


TACT.S and GO_START.S when those modules are implemented. Listing
9.3 shows the final version of file module GO_PLAYS.

Listing 9.3
;; File: GO_PLAY.S

Description: This file contains the logic to play


the game of Go.

File Module Dependencies: load GO_INIT.S, GO_DATA.S before


loading this file.

(define make-player-move
(lambda (game)
(display "Enter move (e.g., D2 or fS, or a
single character for debug) II)
(newline)
(let* «response (string-capitalize (symbol->string (read»»
(col (- (char-code (string-ref response 0» (char-code # \A»)
(row 0»
(if « (string-length response) 2)
(begin
(Go-debug game)
(make-player-move game»
(begin
(set! row (- (char->digit (string-ref response 1» 1»
(if (>
(string-length response)
2)
(set!
row
(+
(* 10 (+ row 1»
(- (char->digit (string-ref response 2» 1»»
(display "Move: column=")
(display col)
(display ", row=")
(display row)
(newline)
(add-stone game HUMAN row col)
(board-set! (game-move-values game) row colO»»»

(define best-computer-move
(lambda (game)
(let «board (game-board game»
(size (game-size game»
208 9. Go PLAYING PROGRAM

(groups (game-groups game»


(move-values (game-move-values game»
(best-move-val 0)
(best-roy -1)
(best-colO»

;; Update move values data before selecting a move:


(tactical-update game)
(strategic-update game)

;; remove illegal moves from the move values array:

(do «roy 0 (+ roy 1»)


«>= roy size»
(do «colO (+ coli»)
«>= col size»
(if
(> 0 (board-ref move-values roy col»
(if (not (equal? 0 (board-ref board roy col»)
(begin
(display "Removing illegal move from
move values array at ")
(print-move roy col)
(newline)
(board-set! move-values roy col -5»»»

Choose the move '.lith the highest value:

(do «roy 0 (+ roy 1»)


«>= roy size»
(do «colO (+ coli»)
«>= col size»
(if
«
best-move-val
(board-ref move-values row col»
(begin
(set! best-roy roy)
(set! best-col col)
(set! best-move-val (board-ref move-values roy col»»»
(if « best-row 0)
(begin
(display "Computer passes")
(neyline) )
(begin
(add-stone game COMPUTER best-roy best-col)
(display "Computer move: ")
(display (list-ref
'(IIAII IIB'I tlCl 1 110 11 liE" "FI 1 I'GII I'H III"
I1 11JII "K"
9.4. Low LEVEL SCHEME FUNCTIONS TO MANIPULATE Go OATA STRUCTURES 209

"LI I "Mil t'N'1 "Oil Ilpll IIQII)


best-col»
(display (+ best-row 1»
(newline)
(board-set! move-values best-row best-colO»»»

The following function plots the Go board in a graphics window.


This version does nothing. This function can be re-defined in
a plotting File Module.

(define (plot-board game)


#f)

The following function provides debug printout. This version


does nothing. This function can be re-defined in another
File Module to provide specific debug output for testing
new software added to the Go program.

(define Go-debug
(lambda (game)
(display "Entered dummy Go-debug function.")
(newline» )

The following two functions are stubbed out so that the


; file modules GD_INIT.S, GD_DATA.S, and GO_PLAY.S can be
; unit tested without the tactical and strategic modules:

(define (tactical-update game)


#f)
(define (strategic-update game)
#£)

(define (go . size)


(let ((game #f»
(if (null? size)
(set! game (make-go-game 9»
(set! game (make-go-game (car size»»
(do «(count 0 (+ count 1»)
(> count 1000»
(best-computer-move game)
(print-board game)
(plot-board game)
(make-player-move game»»
210 9. Go PLAYING PROGRAM

The function make-player-move prompts the player for a move, and


then uses the function add-stone to update the Go game data object.
Moves are made by typing in two characters, a column letter and a row
number, followed by the Enter key on the keyboard. If the player types
in only one letter, followed by the Enter key, then the debug function
Go-debug is called and the player is prompted for another move.
The function best-computer-move calls the two functions

tactical-update
strategic-update

to update the move values array in the specified Go game object. These
two functions are "stubbed out" (i.e., the functions are defined, but
these dummy function definitions do nothing) in file module GO_PLAY
listed in Listing 9.3. All of the code in file modules GO_INIT.S, GO_
DATA.S, and GO_PLAY.s can be tested with the stubbed-out definitions
for tactical-update and strategic-update. The file module GO_TACT.S will
eventually contain the working definition of function tactical-update, and
file module GO_STRAT.S will contain the working definition of function
strategic-update. The function best-computer-move calls these two update
functions, then selects the highest value move from the move values
array to make a move.
File module GO_PLAY.S also contains a stubbed-out definition for
function plot-board, which is defined in file module GO_PLOT.S. If you
load file GO_PLOT.S, then you will see a graphics display of the Go board.
File module GO_PLAYS also contains a stubbed-out version of the func-
tion Go-debug, which can be overridden to print out any data necessary
for debugging new code that you add to file modules GO_TACT.S and GO_
STRAT.S. The function go is the main test function for playing a game
of Go.
The reader will note that all of the functions in the Go playing pro-
gram require a first argument that is a Go data object created by the
function make-Go-game. By passing the current state of the Go game
through the argument lists of these functions (and not relying on any
global data!), we make it possible for the reader to use any of these
functions recursively to implement tactical look-ahead in his or her
own Go programs. This would require copying the Go data object be-
fore passing it to recursive function calls, because many of the functions
in this program modify the contents of the Go data object. The destruc-
tive modification of data passed to a function is sometimes done to
increase runtime performance (as is done in this program), but it is
important to understand that this modification is occurring. File mod-
ule GOJNIT.S contains an unused utility function copy-Go-object which
makes a new complete copy of a Go data object that can be used for
look-ahead search.
9.4. Low LEVEL SCHEME FUNCTIONS TO MANIPULATE Go DATA STRUCTURES 211

The text-based user interface for the Go program in Listing 9.3 is


adequate for writing and testing the Go program. Listing 9.4 shows
a simple graphics interface for displaying Go board positions. Move
selection is still performed in text mode in a Scheme listener window.

Listing 9.4
File: GO_PLOT.S

Description: This file module contains optional plotting routines


for displaying the current Go board.

File Module Dependencies: load GO_INIT.S, GO_DATA.S, and GO_PLAY.S


before loading this file.

The plotting library GRAPH.S

(load "GRAPH.S") ;; just in case ...

The following global variable is used to see if we need to


initialize the plotting window. Usually, we try to avoid using
global variables.

(define init-Go-plot
(lambda 0
(set! GO_PLOT_NEED_TO_INITIALIZE_GRAPHICS_RIGHT_NOW If)
(open-gr»)

(define plot-board
(lambda (game)

(define plot-solid-ellipse
(lambda (left top right bottom color)
(plot-ellipse left top right bottom color)
(if (and
(>= right left)
(>= top bottom»
(plot-solid-ellipse
(+ left 2)
(- top 2)
(- right 2)
(+ bottom 2)
color»)
212 9. Go PLAYING PROGRAM

(init-Go-plot))

(let* «size (game-size game))


(del-x (inexact->exact (/ 800 size)))
(del-y (inexact->exact (/ 900 size)))
(x-start 150)
(y-start 150)
(board (game-board game)))

(clear-plot)

(do «i 0 (+ i 1)))
«>= i size))
(plot-line (+ x-start (* i del-x))
y-start
(+ x-start (* i del-x))
(+ y-start (* (- size 1) del-y)))
(plot-line x-start
(+ y-start (* i del-y))
(+ x-start (* (- size 1) del-x))
(+ y-start (* i del-y)))
(plot-string
(+ x-start -4 (* i del-x))
(- y-start 55)
(list-ref '(ltA tt IIC't lID" IIEI IIFlt I'G'I I'Hlt 111 11
IIBII I

IIJII "K" ItLl 1 "M I' IIN't 110 1' Ilptt ItRtl IISI' IIT'I)

i))
(plot-string
(- x-start 80)
(+ y-start -5 (* i del-y))
(list-ref '("1" "2" "3" "4" "5" "6" "7" "8" "9" "10"
"11" "12" "13" "14" "15" "16" "17" "18" "19")
i))

(do «row 0 (+ row 1)))


«>= row size))
(do «colO (+ coIl)))
«>= col size))
(if (equal?
(board-ref board row col)
HUMAN)
(plot-solid-ellipse
(- (+ x-start (* col del-x)) (/ del-x 2))
(+ (+ y-start (* row del-y)) (/ del-x 2))
(+ (+ x-start (* col del-x)) (/ del-x 2))
(- (+ y-start (* row del-y)) (/ del-x 2))
"gray"))
(if (equal?
(board-ref board row col)
9.4. Low LEVEL SCHEME FUNCTIONS TO MANIPULATE Go DATA STRUCTURES 213

COMPUTER)
(plot-solid-ellipse
(- (+ x-start (* col del-x)) (/ del-x 2))
(+ (+ y-start (* row del-y)) (/ del-x 2))
(+ (+ x-start (* col del-x)) (/ del-x 2))
(- (+ y-start (* row del-y)) (/ del-x 2))
"black"))))))))

The file GO_PLOT.S shown in Listing 9.4 can optionally be loaded to


enable a graphics Go board display. The function plot-board overrIdes
the stubbed-out function with the same name in file GO_PLAY.S. The
function plot-board uses the lexically-scoped function plot-solid-ellipse
to plot the Go stones on the board. The function plot-solid-ellipse uses
recursion to draw a series of concentric circles of a specified color; the
recursive function calls are terminated when a circle with radius zero
is plotted. The function plot-ellipse is defined in the portable graphics
library written in Chapter 4. The first time that function plot-board is
called, it calls the function init-Go-plot to open a graphics window.
Figure 9.6 shows a Go board display from a game between the author
(the human always plays white) and the Go program (the computer
always plays black, and therefore gets to move first).

'9
18 I
.1
1..1 !
Ij.i ~.
"
..
15

IJ
f-f-..

'2
11
.0
9
r
B I
I
,7

i
I
ABC 0 E F G H I J K L ~ HOP R S T

FIGURE 9.6. A 19x19 Go board display. The computer, playing the black stones, has just played
at E16 to avoid having the stone at 016 captured.
214 9. Go PLAYING PROGRAM

A B o

FIGURE 9.7. A 5x5 Go board display.

Figures 9.7 and 9.8 show part of a game played on a tiny, SxS Go
board.

9.5 Go PROGRAM DESIGN


In the first four sections of this chapter we have analyzed the require-
ments for a Go playing program and designed and implemented the-low
level data structures and utility functions for performing the "book-
keeping"; that is, we can now enter moves and update data structures
like group numberings and group liberty counts. Now that we have the
low-level details under control, we can view our Go playing program
more abstractly: we no longer need to worry about the mechanics of
placing and capturing stones on the Go board. Since we now have a
better understanding of the problem because of the work implement-
ing the low-level Go data structure utilities it is now a good time to
design the Go playing program.
Go is an interesting game to play because the player must continually
switch between short-term tactical thinking and long-term strategy. Go
9.5. Go PROGRAM DESIGN 215

FIGURE 9.8. The same game sequence as shown in Figure 9.7, six moves later. The computer
has just played at C5, capturing the white stone at C4.

would be a very difficult game to play if we forgot all of our analysis


after making each move! It is common to analyze in detail the tactics for
a fight in one area of the board and use this analysis many moves later
when your opponent plays in the area that you have already thought
about (while your opponent was pondering her move elsewhere on the
board). Between human opponents Go is usually played on a 19x19
board; the possibilities of play are for all practical purposes infinite.
The human brain seems very well adapted to playing Go for at least
two reasons:
1. Visual pattern recognition
2. The ability to organize local game analysis, updating it only when
something significant changes

To write a decent Go program we must somehow emulate both of


these abilities. We will crudely mimic human visual pattern recogni-
tion by using simple templates of stone patterns; this will often result
in moves that are much less than optimal. We will mimic the ability
to organize previous analysis by using the move values data structure.
Specifically, good moves have a high positive score in this data struc-
ture. Moves that are not evaluated as not being good moves have a zero
216 9. Go PLAYING PROGRAM

score. The tactical and strategic modules, working independently, can


fill in positive scores. In order to "age" our analysis all move scores will
be slightly reduced towards a zero value after each computer move on
the Go board. For move selection, the "best" move (i.e., the move with
the highest score in the move values array) is tested for legality, and then
made on the board if it is a legal move.

9.6 Go PROGRAM IMPLEMENTATION


We have already implemented the low-level data manipulation func-
tions for our Go playing program, and have written simple user
interface functions that allow us to playa game against the program.
These utility functions allow us to program Go without concern for
maintaining the Go game data structures.
The enjoyable part of writing our Go program will be designing and
implementing the tactical and strategic update functions that exam-
ine the current state of a Go game object and suggest moves based on
heuristic rules. If the interested reader wants to extend the Go pro-
gram's playing capabilities, all of the modifications will be made to the
file modules GO_TACT.S and GO_STRAT.S listed in this section. The tacti-
cal update function in file GO_TACT.S is fairly simple, basically knowing
how to temporarily avoid stone capture, and threatening or capturing
enemy stones in a few set circumstances.
Listing 9.5 shows the implementation of the tactical module of the
Go playing program.

Listing 9.5
;; File: Go_TACT.S

Description: This file module contains functions that


perform the tactical analysis of a given
game situation, and add suggested tactical
moves in the move values array in the game
data structure.

Copyright 1995, Mark Watson. All source code rights reserved.


The contents of this file can be used in compiled form
without restriction.

File module dependencies:

Files Go_INIT.S, Go_DATA.S, and GO PLAY.S must be loaded


before this file.
9.6. Go PROGRAM IMPLEMENTATION 217

Override the GO-debug function defined in file GO PLAY.S. This


function can be commented out after ye have unit tested the
functions in this file module.

(define Go-debug
(lambda (game)
(pp game)
(neyline»)

Over ide the dummied out tactical analysis function defined


in file GO_PLAY.S:

(define tactical-update
(lambda (game)
(check-for-capture game)
(check-for-atari-human game)
(escape-from-atari game»)

(define check-for-capture
(lambda (game)
(let «size (game-size game»
(groups (game-groups game»
(board (game-board game»
(move-values (game-move-values game»
(group-liberties (game-liberties game»
(groups-processed 'C»~)
(do «roy 0 (+ roy 1»)
«>= roy size»
(do «colO (+ coli»)
«>= col size»
(if (and
(equal? HUMAN (board-ref board roy col»
(equal? 1 (board-ref group-liberties roy col»)
(if (not (member (board-ref groups roy col) groups-processed»
(let
We have found a group Yith only one liberty
that has not already been processed this turn:
«atari-list
(group-attachment
game
(board-ref groups roy col»»
(if (> (length atari-list) 0)
(let* «attack-roy (caar atari-list»
(attack-col (cadar atari-list»
(current-score
(board-ref move-values attack-roy attack-col»)
218 9. Go PLAYING PROGRAM

(display "possible to capture HUMAN group #")


(display (board-ref groups row col»
(newline)
(board-set!
move-values
attack-row attack-col
(+ current-score
(*
2
(group-count game (board-ref groups row col»»)
(set! groups-processed
(cons
(board-ref groups row col)
groups-processed»»»»»»

(define check-for-atari-human
(lambda (game)
(let «size (game-size game»
(groups (game-groups game»
(board (game-board game»
(move-values (game-move-values game»
(group-liberties (game-liberties game»
(groups-processed 'C»~)
(do «row 0 (+ row 1»)
«>= row size»
(do «colO (+ coli»)
«>= col size»
(if (and
(equal? HUMAN (board-ref board row col»
(equal? 2 (board-ref group-liberties row col»)
(if (not (member (board-ref groups row col) groups-processed»
(let
" We have found a group with only two liberties
that has not already been processed this turn:
«atari-list
(group-attachment
game
(board-ref groups row col»»
(if (> (length atari-list) 0)
(begin
(do «atari atari-list (cdr atari»)
«null? atari»
(let* «attack-row (caar atari»
(attack-col (cadar atari»
(current-score
(board-ref move-values attack-row attack-col»)
(display "possible to atari HUMAN group #")
(display (board-ref groups row col»
(newline)
9.6. Go PROGRAM IMPLEMENTATION 219

(board-set!
move-values
attack-row attack-col
(+ current-score
(*
0.7
(group-count
game
(board-ref groups row col)))))))
(set! groups-processed
(cons
(board-ref groups row col)
groups-processed))))))))))))

(define escape-from-atari
(lambda (game)
(let ((size (game-size game))
(groups (game-groups game))
(board (game-board game))
(move-values (game-move-values game))
(group-liberties (game-liberties game))
(groups-processed 'C)))
(do ((row 0 (+ row 1)))
((>= row size))
(do ((colO (+ col 1)))
((>= col size))
(if (and
(equal? COMPUTER (board-ref board row col))
(equal? 1 (board-ref group-liberties row col)))
(if (not (member (board-ref groups row col) groups-processed))
(let
We have found a group with only one liberty
that has not already been processed this turn:
((atari-list
(group-attachment
game
(board-ref groups row col))))
(if (> (length atari-list) 0)
(let* ((defend-row (caar atari-list))
(defend-col (cadar atari-list))
(current-score
(board-ref move-values defend-row defend-col)))
(display "computer group subject to capture: #")
(display (board-ref groups row col»
(newline)
(board-set!
move-values
defend-row defend-col
(+ current-score
220 9. Go PLAYING PROGRAM

4
(group-count game (board-ref groups row col)))))
(set! groups-processed
(cons
(board-ref groups row col)
groups-processed))))))))))))

The strategic update function in file GO_STRAT.S suggests moves that


• Are located in empty areas of the board
• Link groups of computer stones together
• Approach (crowd) groups of opponent stones

Two temporary two-dimensional arrays are used to store influence


values for both the computer's and the player's stones on the board.
For a specified grid point on the Go board the program can access the
corresponding influence values in these two arrays to get a reasonably
accurate estimate of the density of stones in that area of the board.
Listing 9.6 shows the implementation of the strategic module of the
Go playing program.

Listing 9.6
;; File: GO_STRAT.S

Description: This file module contains functions that


perform the strategical analysis of a given
game situation, and add suggested strategic
moves in the move values array in the game
data structure.

Copyright 1995, Mark Watson. All source code rights reserved.


The contents of this file can be used in compiled form
without restriction.

File module dependencies:

Files GO_INIT.S, GO_DATA.S, and GO_PLAY.S must be loaded


before this file.

Override the GO-debug function defined in file GO_PLAY.S. This


function can be commented out after we have unit tested the
functions in this file module.

(define Go-debug
(lambda (game)
9.6. Go PROGRAM IMPLEMENTATION 221

(pp game)
(newline)))

(define strategic-update
(lambda (game)
(let* ((size (game-size game))
(human-influence (make-board size))
(computer-influence (make-board size))
(board (game-board game)))
Fill in both computer and human "influence" arrays:
(do ((row 2 (+ row 1)))
((>= row (- size 2)))
(do ((col 2 (+ coli)))
((>= col (- size 2)))

(if (equal?
(board-ref board row col)
HUMAN)
(begin
(board-set!
human-influence (- row 2) col
(+ 1 (board-ref human-influence (- row 2) col)))
(board-set!
human-influence (+ row 2) col
(+ 1 (board-ref human-influence (+ row 2) col)))
(board-set!
human-influence row (- col 2)
(+ 1 (board-ref human-influence row (- col 2))))
(board-set!
human-influence row (+ col 2)
(+ 1 (board-ref human-influence row (+ col 2))))
(board-set!
human-influence (- row 1) col
(+ 2 (board-ref human-influence (- row 1) col)))
(board-set!
human-influence (+ row 1) col
(+ 2 (board-ref human-influence (+ row 1) col)))
(board-set!
human-influence row (- coli)
(+ 2 (board-ref human-influence row (- col 1))))
(board-set!
human-influence row (+ col 1)
(+ 2 (board-ref human-influence row (+ col 1))))))

(if (equal?
(board-ref board row col)
COMPUTER)
(begin
(board-set!
222 9. Go PLAYING PROGRAM

computer-influence (- row 2) col


(+ 1 (board-ref computer-influence (- row 2) col)))
(board-set!
computer-influence (+ row 2) col
(+ 1 (board-ref computer-influence (+ row 2) col)))
(board-set!
computer-influence row (- col 2)
(+ 1 (board-ref computer-influence row (- col 2))))
(board-set!
computer-influence row (+ col 2)
(+ 1 (board-ref computer-influence row (+ col 2))))
(board-set!
computer-influence (- row 1) col
(+ 2 (board-ref computer-influence (- row 1) col)))
(board-set!
computer-influence (+ row 1) col
(+ 2 (board-ref computer-influence (+ row 1) col)))
(board-set!
computer-influence row (- coli)
(+ 2 (board-ref computer-influence row (- coli))))
(board-set!
computer-influence row (+ coli)
(+ 2 (board-ref computer-influence row (+ coli))))))))

(look-for-empty-territory game computer-influence human-influence)


(connect-groups game)
(approach-enemy-stones game computer-influence human-influence))))

(define look-for-empty-territory
(lambda (game computer-influence human-influence)
(let «size (game-size game))
(move-values (game-move-values game))
(sum 0))
(do «row 1 (+ row 1)))
«>= row (- size 1)))
(do «coli (+ coli)))
«>= col (- size 1)))
(set!
sum
(+
(board-ref computer-influence row col)
(board-ref human-influence row col)))
(board-set! move-values row col
(+
(board-ref move-values row col)
(* 0.2 (- 3 sum)))))))))
(define connect-groups
(lambda (game)
If))
9.6. Go PROGRAM IMPLEMENTATION 223

(define approach-enemy-stones
(lambda (game computer-influence human-influence)
(let «size (game-size game»
(board (game-board game»
(move-values (game-move-values game»)
(do «row 1 (+ row 1»)
«>= row (- size 1»)
(do «coli (+ coli»)
«>= col (- size 1»)
(if (and
(equal? 0 (board-ref board row col»
(equal?
(board-ref computer-influence row col)
1)
(equal?
(board-ref human-influence row col)
1)
(begin
(board-set!
move-values row col
(+
(board-ref move-values row col)
1. 5»
(display "Approach move considered at ")
(print-move row col)
(newline»»»»
Listing 9.7 shows the text output from playing a 20-move sequence
against the Go program.

Listing 9.7
(load "go_init.s")
;Loading "go_init.s" -- done
;Value: set-joseki
(load "go_data.s")
;Loading "go_data.s" -- done
;Value: print-board
(load "go_play.s")
;Loading "go_play.s" -- done
;Value: go
(load "go_tact.s")
;Loading "go_tact.s" -- done
;Value: escape-from-atari
(load "go_strat.s")
;Loading "go_strat.s" -- done
;Value: approach-enemy-stones
224 9. Go PLAYING PROGRAM

(go)touched groups: ()
Computer move: D3

9: -1--1--1--1--1--1--1--1--1-
8: -1--1--1--1--1--1--1--1--1-
7: -1--1--1--1--1--1--1--1--1-
6: -1--1--1--1--1--1--1--1--1-
5: -1--1--1--1--1--1--1--1--1-
4: -1--1--1--1--1--1--1--1--1-
3: -I--I--j--C--I--I--I--I--I-
2: -1--1--1--1--1--1--1--1--1-
1: -1--1--1--1--1--1--1--1--1-
ABC D E F G H I

Enter move (e.g., D2 or f5, or a single character for debug)


f7
Move: column=5, row=6
touched groups: ()
touched groups: ()
Computer move: C4

9: -1--1--1--1--1--1--1--1--1-
8: -1--1--1--1--1--1--1--1--1-
7: -I--I--I--I--I--H--I--I--I-
6: -1--1--1--1--1--1--1--1--1-
5: -1--1--1--1--1--1--1--1--1-
4: -I--I--C--I--I--I--I--I--I-
3: -I--I--I--C--I--I--I--I--I-
2: -1--1--1--1--1--1--1--1--1-
1: -1--1--1--1--1--1--1--1--1-
ABC D E F G H I

Enter move (e.g., D2 or f5, or a single character for debug)


f3
Move: column=5, row=2
touched groups: ()
touched groups: ()
Computer move: G4

9: -1--1--1--1--1--1--1--1--1-
8: -1--1--1--1--1--1--1--1--1-
7: -I--I--I--I--I--H--I--I--I-
6: -1--1--1--1--1--1--1--1--1-
5: -1--1--1--1--1--1--1--1--1-
4: -I--I--C--I--I--I--C--I--I-
3: -I--I--I--C--:--H--I--I--I-
2: -1--1--1--1--: - ·1--1--1--1-
1: -1--1--1--1--1--1--1--1--1-
ABC D E F G H I
9.6. Go PROGRAM IMPLEMENTATION 225

Enter move (e.g., D2 or f5, or a single character for debug)

c6
Move: column=2, row=5
touched groups: ()
touched groups: ()
Computer move: G6

9: -1--1--1--1--1--1--1--1--1-
8: -1--1--1--1--1--1--1--1--1-
7: -I--I--I--I--I--H--I--I--I-
6: -I--I--H--I--I--I--C--I--I-
5: -1--1--1--1--1--1--1--1--1-
4: -I--I--C--I--I--I--C--I--I-
3: -I--I--I--C--I--H--I--I--I-
2: -1--1--1--1--1--1--1--1--1-
1: -1--1--1--1--1--1--1--1--1-
ABC D E F G H I

Enter move (e.g., D2 or f5, or a single character for debug)

f6
Move: column=5, row=5
touched groups: (7 8)
touched groups: ()
Computer move: D7

9: -1--1--1--1--1--1--1--1--1-
8: -1--1--1--1--1--1--1--1--1-
7: -I--I--I--C--I--H--I--I--I-
6: -I--I--H--I--I--H--C--I--I-
5: -1--1--1--1--1--1--1--1--1-
4: -I--I--C--I--I--I--C--I--I-
3: -I--I--I--C--I--H--I--I--I-
2: -1--1--1--1--1--1--1--1--1-
1: -1--1--1--1--1--1--1--1--1-
ABC D E F G H I

Enter move (e.g., D2 or f5, or a single character for debug)

g7
Move: column=6, row=6
touched groups: (7 10)
touched groups: ()
Computer move: E5
226 9. Go PLAYING PROGRAM

9: -1--1--1--1--1--1--1--1--1-
8: -1--1--1--1--1--1--1--1--1-
7: -I--I--I--C--I--H--H--I--I-
6: -I--I--H--I--I--H--C--I--I-
5: -I--I--I--I--C--I--I--I--I-
4: -I--I--C--I--I--I--C--I--I-
3: -I--I--I--C--I--H--I--I--I-
2: -1--1--1--1--1--1--1--1--1-
1: -1--1--1--1--1--1--1--1--1-
ABC D E F G H I

Enter move (e.g., D2 or f5, or a single character for debug)

f5
Move: column=5, row=4
touched groups: (11 12)
touched groups: ()
Computer move: B2

9: -1--1--1--1--1--1--1--1--1-
8: -1--1--1--1--1--1--1--1--1-
7: -I--I--I--C--I--H--H--I-~I­
6: -I--I--H--I--I--H--C--I--I-
5: -I--I--I--I--C--H--I--I--I-
4: -I--I--C--I--I--I--C--I--I-
3: -I--I--I--C--I--H--I--I--I-
2: -I--C--I--I--I--I--I--I--I-
1: -1--1--1--1--1--1--1--1--1-
ABC D E F G H I

Enter move (e.g., D2 or f5, or a single character for debug)

e2
Move: column=4, row=l
touched groups: ()
touched groups: ()
Computer move: H2

9: -1--1--1--1--1--1--1--1--1-
8: -1--1--1--1--1--1--1--1--1-
7: -I--I--I--C--I--H--H--I--I-
6: -I--I--H--I--I--H--C--I--I-
5: -I--I--I--I--C--H--I--I--I-
4: -I--I--C--I--I--I--C--I--I-
3: -I--I--I--C--I--H--I--I--I-
2: -I--C--I--I--H--I--I--C--I-
1: -1--1--1--1--1--1--1--1--1-
ABC D E F G H I
9.6. Go PROGRAM IMPLEMENTATION 227

Enter move (e.g., D2 or f5, or a single character for debug)

g2
Move: column=6, row=l
touched groups: (15)
touched groups: ()
Computer move: B8

9: -1--1--1--1--1--1--1--1--1-
8: -I--C--I--I--I--I--I--I--I-
7: -I--I--I--C--I--H--H--I--I-
6: -I--I--H--I--I--H--C--I--I-
5: -I--I--I--I--C--H--I--I--I-
4: -I--I--C--I--I--I--C--I--I-
3: -I--I--I--C--I--H--I--I--I-
2: -I--C--I--I--H--I--H--C--I-
1: -1--1--1--1--1--1--1--1--1-
ABC D E F G H I

Enter move (e.g., D2 or f5, or a single character for debug)

b7
Move: column=l, row=6
touched groups: (17)
touched groups: ()
Computer move: H8

9: -1--1--1--1--1--1--1--1--1-
8: -I--C--I--I--I--I--I--C--I-
7: -I--H--I--C--I--H--H--I--I-
6: -I--I--H--I--I--H--C--I--I-
5: -I--I--I--I--C--H--I--I--I-
4: -I--I--C--I--I--I--C--I--I-
3: -I--I--I--C--I--H--I--I--I-
2: -I--C--I--I--H--I--H--C--I-
1: -1--1--1--1--1--1--1--1--1-
ABC D E F G H I

Enter move (e.g., D2 or f5, or a single character for debug)

d5
Move: column=3, row=4
touched groups: (11)
touched groups: ()
Computer move: E8
228 9. Go PLAYING PROGRAM

9: -1--1--1--1--1--1--1--1--1-
8: -I--C--I--I--C--I--I--C--I-
7: -I--H--I--C--I--H--H--I--I-
6: -I--I--H--I--I--H--C--I--I-
5: -I--I--I--H--C--H--I--I--I-
4: -I--I--C--I--I--I--C--I--I-
3: -I--I--I--C--I--H--I--I--I-
2: -I--C--I--I--H--I--H--C--I-
1: -1--1--1--1--1--1--1--1--1-
ABC D E F G H I

Enter move (e.g., D2 or f5, or a single character for debug)


e4
Move: column=4, row=3
touched groups: (11)
computer group subject to capture: #11
touched groups: ()
Computer move: B5

9: -1--1--1--1--1--1--1--1--1-
8: -I--C--I--I--C--I--I--C--I-
7: -I--H--I--C--I--H--H--I--I-
6: -I--I--H--I--I--H--C--I--I-
5: -I--C--I--H--C--H--I--I--I-
4: -I--I--C--I--H--I--C--I--I-
3: -I--I--I--C--I--H--I--I--I-
2: -I--C--I--I--H--I--H--C--I-
1: -1--1--1--1--1--1--1--1--1-
ABC D E F G H I

Enter move (e.g., D2 or f5, or a single character for debug)


e6
Move: column=4, row=5
touched groups: (11 24)
Group 11 is dead.
touched groups: ()
Computer move: H5

9: -1--1--1--1--1--1--1--1--1-
8: -I--C--I--I--C--I--I--C--I-
7: -I--H--I--C--I--H--H--I--I-
6: -I--I--H--I--H--H--C--I--I-
5: -I--C--I--H--I--H--I--C--I-
4: -I--I--C--I--H--I--C--I--I-
3: -I--I--I--C--I--H--I--I--I-
2: -I--C--I--I--H--I--H--C--I-
1: -1--1--1--1--1--1--1--1--1-
ABC D E F G H I
Enter move (e.g., D2 or f5, or a single character for debug)
9.7. IDEAS FOR IMPROVING THE Go PLAYING PROGRAM 229

As seen in Listing 9.7, the simple Go program developed in this chap-


ter plays a weak game. The interested reader is encouraged to explore
the history, lessons on play, and other available Go playing programs on
the Internet (see Appendix B). The program developed in this chapter
will provide a good foundation for building your own Go program if
you are unfortunate enough to have your imagination captured by Go
programming.

9.7 IOEAS FOR IMPROVING THE Go PLAYING PROGRAM


Although the file modules GO_INIT.S, GO_DATA.S, GO_PLAYS, and GO_
PLOT.S are fully implemented, the tactical and strategic update file mod-
ules GO_TACT.S and GO_STRAT.S are very simple. There are many ways
to improve the tactical update file module; for example:
1. Recognize common patterns for capturing stones at the edge of the
board
2. Learn to make two eyes for groups that might be captured. An eye
is an empty grid point that is surrounded by a group of stones (and
possibly also the edge of the Go board). If a group has two eyes, it
is impossible to capture
3. Learn to recognize enemy groups that cannot form two eyes, and
attack them by surrounding the group to reduce its liberty count
4. Add a limited look-ahead capability. Since there are so many
possible moves on a Go board, look-ahead must be used sparingly

There are many ways to improve the strategic update function; for
example:
1. Recognize friendly groups (i.e., groups made up of the same color
stones) that can be attacked, and try to connect weak groups to
other friendly groups
2. Recognize large areas controlled by the opponent, and determine
either a safe place to play inside this area (an invasion), or approach
the area from the outside to reduce its territory
3. Calculate a safety attribute for each group. Groups with two eyes
are absolutely safe. When calculating influence around stones, take
into account the safety of the stones
APPENDIX A
INSTALLING AND RUNNING
THE MIT SCHEME
SYSTEM

There are two disks included with this book. These disks contain all of
the example programs in this book as well as the runtime portion of
the MIT Scheme development system (see Appendix B for locations on
the Internet where the full MIT Scheme system can be found, which
includes a native mode compiler and an Emacs-like editor).
It is easiest to set up MIT Scheme if it is installed in the directory
C:\SCHEME. Type the following commands in a DOS window:
C:
cd\
mkdir SCHEME
cd SCHEME
[PLACE THE FIRST FLOPPY IN YOUR DISK DRIVE NOW]
xcopy a:\*.* . /s
[PLACE THE SECOND FLOPPY IN YOUR DISK DRIVE NOW]
xcopy a:\*.* . /s
There are four ZIP format files that you need to decompress. You
will use the program PKUNZIP.EXE contained on the included disks
to decompress these files.
Type the following commands:
pkunzip -d bin. zip
pkunzip -d lib.zip
pkunzip -d runnoflo.zip
pkunzip -d help. zip
You will require WIN32S support to run MIT Scheme. Microsoft
Windows NT and Windows 95 have WIN32S support built-in. Microsoft
freely distributes a WIN32S kit which can be loaded on top of either
232 A. INSTALLING AND RUNNING THE MIT SCHEME SYSTEM

Windows 3.1 or Windows for Workgroups to provide WIN32S support.


This WIN32S package is available at the same Internet sites as MIT
Scheme; see Appendix B.
There is an INSTALL file in C:\SCHEME written by the developers
of MIT Scheme.
If you are only going to run MIT Scheme under Windows, type:
copy c:\scheme\bin\WIN31\*.* c:\scheme\bin
If you are going to run MIT Scheme only under Windows NT or
Windows 95, type:
copy c:\scheme\bin\NT\*.* c:\scheme\bin
If you would like to run MIT Scheme under both Windows NT
(or Windows 95) and Windows 3.1, then read the file C:\SCHEME
\INSTALL for instructions.
After you have UNZIPed the four ZIP files, start Windows if you have
not already done so and run the following command from the Program
Manager:
C:\SCHEME\BIN\SCHEME -load C:\SCHEME\ETC\PMGRP
APPENDIX B
MORE INFORMATION
AVA/LABLE ON THE
INTERNET
There is a wealth of information available on the Internet. This ap-
pendix contains references for several FTP and World Wide Web sites
where you can find additional information on topics covered in this
book.

SCHEME
http://18.23.0.16/scheme-home.html
http://18.23.0.16/ftpdir/scheme-7.3/
http://www.cs.indiana.edu/scheme-repository/SRhome.html

FTP: ftp.cs.cmu.edu and ftp.cs.indiana.edu

CHESS
http://caissa.onenet.net/chess/

Go
ftp://bsdserver.ucsf.edu/Go
http://www.cwi.nl/jansteen/go/events/japan_news.html
http://www.mth.kcl.ac.uk/ mreiss/compgo.html
BIBLIOGRAPHY

Gleick, James: Chaos, Making a New Science. Harrisonburg (VA):


Penguin Books Ltd 1987.
Schank, Roger C. and Riesbeck, Christopher K.: Inside Computer Un-
derstanding. Hillsdale (NJ): Lawrence Erlbaum Associates, Inc.
1981.
Watson, Mark L.: Common LISP Modules. Artificial Intelligence in
the Era of Neural Networks and Chaos Theory. New York (NY):
Springer-Verlag 1991.
Watson, Mark L.: C++ Power Paradigms. New York (NY): McGraw-Hill,
Inc. 1994.
INDEX

#1,10 display function, 8


#t,1O do special form, 13,20-21
> function, 11 dynamic memory allocation, 3
>= function, 11
< function, 11 eq? function, 10
<= function, 11 equal? function, 10

Ada, 1 FORTRAN,l
allele, 33
and function, 11 garbage collection, 3
append, function, 8 gene, 28
genetic algorithms, 25, 28-43
begin special form, 13 Go, 183-190
breadth first search, 80-81 graph layout, 52-53

C++, ix if statement, 11-12


car function, 8 interface, 25
case statement, 23
cdr function, 8 joseki, 198
char->digit function, 23
char-code function, 23 lambda expression, 1
character recognition, 128-139 let special form, 13
chaos theory, 65-69 let* special form, 14
chess, 151-155 lexical scoping, 19
chromosome, 28 LISP, 1
Cobol, 1 list function, 3-7, 8
cohesion, 25 local variables, 12-13
comments, 12
complex arithmetic, 62-63 Macintosh Gambit Scheme, 47
conceptual dependency theory, make-vector function, 9
142-145 Mandelbrot sets, 62-65
cons function, 8 May, Robert, 65
crossover, 28, 32-33 modularity, 25
momentum (in neural networks),
define special form, 7 90
depth first search, 71-73 mutation, 28
238 INDEX

neural networks, 89-94 software reuse, 45


not function, 11 string->number function, 22
string-length function, 22
or function, 11 string-ref function, 22
supervised learning (neural
networks),90
pp (pretty print function), 6,52
symbol->string function, 22
read function, 22 2D arrays, 92, 95-98
recursion, 17-20 truncate function, 19
resource allocation, 41
reverse function, 22 vector function, 9-10
rules of Go, 186-188 vector-length function, 9
vector-ref function, 9
set! function, 7, 8 vector-set! Function, 9
Shannon, Claude, 151
Sigmoid function, 90, 124 write function, 8
Programming in SCHEME

MIT SCHEME © 1996 MIT


Source code © 1996 Mark Watson, Solana Beach

This electronic component package is protected by federal copyright


law and international treaty. If you wish to return this book and the
diskette to Springer-Verlag, do riot open the diskette envelope or reo
move it from the back cover. Springer-Verlag will not accept any retums
if the envelope has been opened and/or separated from the back cover.
The copyright holders retain title to and ownership of the package.
Without the written permission of Mark Watson and MIT, the customer
may not copy the electronic component except for backup purposes and
for the installation of the software on the customer's own computer.
Springer-Verlag or its designee has the right to audit your computer
and electronic component usage to determine whether any unautho-
rized copies of this package have been made.

Springer-Verlag or the author makes no warranty or representation,


either expressed or implied, with respect to this electronic component,
the disks, or documentation, including their quality, merchantability, or
fitness for a particular purpose. In no event will Springer-Verlag or the
author be liable for direct, indirect, special, incidental, or consequential
damages arising out of the use or inability to use the electronic compo-
nent or documentation, even if Springer-Verlag or the author has been
advised of the possibility of such damages.

You might also like