GETTING STARTED IN *LISP Version 6.1, June 1991 Copyright (c) 1991 by Thinking Machines Corporation. "Wilt thou show the whole wealth of Chapter 1: Instant *Lisp thy wit in an instant?" ************************ William Shakespeare *Lisp (pronounced "star Lisp") is a data parallel extension of the Common Lisp programming language, designed for the Connection Machine (R) (CM) system. *Lisp is a direct extension of Common Lisp, so if you've pro- grammed in Lisp before, *Lisp programs should look very familiar to you. The data parallel extensions of *Lisp include a large number of functions and macros, and one important abstraction, the parallel variable, or pvar ("p-var"). As we'll see shortly, pvars are as important to *Lisp programmers as lists are to Lisp programmers. 1.1 Getting Started: A Simple Application So that we have something specific to talk about, I'm going to pick a particular type of application and show you how to develop a *Lisp program for it. While the application I've chosen may not be identical to the projects you may have in mind, the techniques I use in developing it and the rules of thumb that I use in choosing which features of *Lisp to use should carry over well to your own work. IMPORTANT: Interspersed throughout this chapter are sample *Lisp sessions that display both the *Lisp forms that you need to type, and the value(s) that *Lisp returns. In all these ses- sions, the lines that you'll want to type are preceded by the prompt character ">" and printed in bold type. Lines printed in normal type are either results displayed by *Lisp or examples of *Lisp code that you don't need to type in. 1.1.1 Our Project: A Cellular Automata System Depending on your background and interests, you may or may not have heard of the Game of Life. Invented by John H. Conway of the University of Cambridge, the Game is played on a very large board, marked off into squares like a checkerboard. Each square can be in one of two states, traditionally called "live" and "dead." Before each game, some of the squares are made "live," usually forming a pattern of some kind. The Game itself is played by changing the state of each square according to the following rules: o A cell's neighbors are the eight squares that surround it on the board. o If a live square has fewer than two, or more than three live neighbors, it dies. o If a dead square has exactly three live neighbors, it be- comes live again. All cells change their values together, in a single "step." The Game consists of a series of such steps, one after another. There are no winners and losers in the Game--the purpose of the Game is to see the kinds of patterns that arise on the board from follow- ing these three simple rules. The rules of the Game of Life cause the automaton's cells to resemble colonies of living creatures, producing patterns of "life" and "death" that ripple across the automaton's grid. Conway's Game of Life is a simple example of a cellular automa- ton. A cellular automaton consists of a grid, or array, of ele- ments called cells, each of which contains a value of some kind that determines that cell's state. Along with the array of cells, there is a set of rules that determine how the automaton's cells change over time. The cells change their states in a regular, step-by-step fashion, and the current state of each cell typical- ly depends on the state values of its neighbors, the cells that are closest to it. But the Game of Life is only one of a spectrum of different pos- sible automata. Whereas the Game of Life uses two states per cell, other kinds of automata use tens, hundreds, or even thousands of states. Such automata are used to simulate the flow of liquids or gases of varying densities, the absorption and release of molecules in chemical reactions, and even the spread of infectious diseases. Other kinds of automata use grids of different shapes and sizes; there are even automata that run on one-dimensional and three- dimensional grids. What we'd like to produce is a simple *Lisp program that allows us to try out a number of different automata on the CM, simply by specifying the rules that describe how they work. What we need is a set of tools that will let us define and display a cellular au- tomaton in action. For simplicity's sake, let's assume that we'll always be working with automata that use two-dimensional grids of cells, and that the states of the cells will be represented by integers from 0 to n-1, with n being the total number of possible states for a cell. Thus, we need tools for setting the total number of states that each cell can have, and for defining how the value of each cell depends on the values of its neighbors. Our set of tools should also allow us to run the automaton through any required number of steps, and to display the current state of the automaton in a readable format. Okay, now that we've got a reasonably clear idea of what we want to do, we can start up a *Lisp session and begin programming. 1.1.2 *ting Up *Lisp At this point you have a choice to make, because *Lisp comes in two main forms. There's the standard version of *Lisp that runs on the CM hardware, and there is also a *Lisp simulator. The simulator is a Common Lisp program that runs entirely on your front-end computer, simulating the operations of an attached CM through software. Depending on the front-end computer you are using, and where your system administrator has stored the *Lisp software, there is gen- erally a specific command you can type that loads the software for either the CM hardware or the simulator version of *Lisp. For example, on UNIX front ends the command will typically be % starlisp or % starlisp-simulator Once you have the *Lisp software loaded, type: > (*lisp) ;;; To select the *Lisp package > (*cold-boot) ;;; To initialize *Lisp and the CM IMPORTANT: The *cold-boot command initializes *Lisp and the CM so that you can begin typing commands. If you don't *cold-boot, you won't be able to execute any *Lisp code! FOR SIMULATOR USERS: Among other things, the *cold-boot command sets the number of simulated processors that you have available. The default number of processors (32) is rather small for our purposes, so you'll want to *cold-boot the simulator like this: > (*cold-boot :initial-dimensions '(16 16)) This starts you off with 256 processors, arranged in a square pattern 16 by 16 in size. FOR CM USERS: The *cold-boot command automatically attaches you to a CM, if one is available. If you get an error message such as "the FEBI is currently in use" or "no sequencers are avail- able", it probably means that the CM is currently busy; you may have to try again later. If you're using *Lisp on real CM hardware, you can use the command (cm:finger) to find out what CM resources are available; see the CM System User's Guide for more information. If you find that the CM is busy, you can use the *Lisp simulator instead; the simulator is always available, regardless of the state, busy or not, of the CM. 1.2 Using *Lisp 1.2.1 Defining an Automata Grid First, we're going to need some way of representing the grid of cells for the automaton. In a program written for a serial com- puter, the grid would be a two-dimensional array of integers. On the CM, however, we can take advantage of the parallel nature of the machine by storing the state value for each cell in the memory of a different CM processor. To do this, we'll define a parallel variable, or pvar, to keep track of the grid: > (*defvar *automata-grid* 0) *AUTOMATA-GRID* By default, when you *cold-boot *Lisp the CM processors are au- tomatically arranged in a two-dimensional grid that is as close to being square as the number of available processors permits. Let's print out a portion of this grid: > (ppp *automata-grid* :mode :grid :end '(8 5)) 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 0 0 0 0 0 0 0 0 0 0 The ppp function (also known as pretty-print-pvar) prints out the values of a pvar in whatever format you specify. Here we're using it to display the state values of our automata-grid as a grid of numbers. We'll want to do this quite often, so let's define a function to display the grid: > (defun view (&optional (width 8) (height 5)) (ppp *automata-grid* :mode :grid :end (list width height))) Each value displayed by view comes from the memory of a single CM processor. We can read and write each value individually, much as we can read and write the individual elements of an array. For example: > (defun read-cell (x y) (pref *automata-grid* (grid x ))) > (read-cell 5 1) 0 The *Lisp operation pref reads a value from a pvar, given its lo- cation within the CM. To specify the location I've used grid, a *Lisp helper function that lets you describe the location of a particular value by its grid (x and y) coordinates. Applying *setf to pref (by analogy with Common Lisp's setf) changes the value of the pvar at the specified location: > (defun set-cell (x y newvalue) (*setf (pref *automata-grid* (grid x y)) newvalue)) > (set-cell 5 1 7) > (read-cell 5 1) 7 > (view 8 3) 0 0 0 0 0 0 0 0 0 0 0 0 0 7 0 0 0 0 0 0 0 0 0 0 It would be a nuisance to have to set the state value of each cell in the grid individually, so let's define a function that will let us set the state of all the cells at once: > (defun set-grid (newvalue) (*set *automata-grid* newvalue)) > (set-grid 7) ;;; Set cells to same value > (view 8 3) 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 > (set-grid (random!! 10)) ;;; Set cells to random values > (view 8 3) 1 3 0 5 3 1 9 3 8 5 6 6 0 0 4 6 4 4 5 9 6 7 5 6 Notice that the random!! function calculates a different random value for each cell, rather than choosing one random value for all of the cells. This is an important feature of *Lisp: data parallelism. *Lisp operations such as random!! cause every pro- cessor to perform the same operation on potentially different data; each processor can produce a different result. 1.2.2 Defining a Simple Automaton Now we can start thinking about how to define the rules for an automaton. The simplest type of automaton is one in which each cell's state depends only on its previous state--that is, where neighboring cells have no effect on one another. As an example, let's define an automaton that obeys the following rules: o Each cell can have any state value from 0 to 9. o If a cell's state is even, divide its state value by two. o If a cell's state is odd, add 1 to its state value and mul- tiply by 2. I'm not just picking these rules out of thin air. We'll see the effect they have in a moment. In *Lisp, we can easily write a function that implements these rules. For example, here's a function that does the calculations for a single step of the automaton: > (defun one-step () (if!! (evenp!! *automata-grid*) (floor!! *automata-grid* 2) (*!! (1+!! *automata-grid*) 2))) Here, I'm using the *Lisp operator if!!, which works much like if in Common Lisp; it evaluates a test expression for all the CM processors, and then performs one *Lisp operation for all proces- sors where the test was true (not nil), and another operation for all processors where it was false (nil). In the one-step function we use evenp!! as the test to find those processors whose cell values are even. For these processors, the value of *automata-grid* is divided by 2. For all the remaining processors, the value of *automata-grid* is incremented by 1, and then multiplied by 2, using the parallel operators 1+!! and *!! respectively. FOR THE CURIOUS: Why am I using floor!! rather than /!!? In *Lisp /!! is de- fined to always return a floating-point result, because *Lisp doesn't include "rational number" pvars. To get an integer result instead, I use floor!!. Each calculation used by one-step takes place simultaneously in all processors. Even more important, the value of *automata-grid* is not changed by the one-step function. if!! returns a pvar whose values depend on the results obtained from the two if!! branches. The value of this pvar for each processor is the value of the branch that processor executed. This means that all we need to do is type (set-grid (one-step)) to calculate the new value for each cell and update the entire grid. But there's an important question that we need to answer first: What happens if a cell's value exceeds the total number of states allowed? Cells are only permitted values between 0 and 9, but one-step can return a value for a cell that is greater than 9. This suggests a simple answer: We can use the *Lisp function mod!! to redefine set-grid so that it will automatically wrap values greater than 9 back into the range 0 through 9: > (defvar *total-number-of-states* 10) > (defun set-grid (newvalue) (*set *automata-grid* (mod!! newvalue *total-number-of-states*))) > (set-grid 27) > (view 8 3) 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 > (set-grid (random!! 27)) > (view 8 3) 7 2 0 7 3 8 6 4 1 0 8 3 2 4 5 2 1 9 7 0 2 3 1 3 This last example is a useful tool. We'll want the ability to set every cell in the grid to a random state. Let's write a function that does this, and write it so that it automatically chooses random values in the range from 0 up to one less than the current number of states: > (defun random-grid () (set-grid (random!! *total-number-of-states*))) And while we're defining things, let's create a function that will run the automaton through any specified number of steps: > (defun run (&optional (n 1)) (dotimes (i n) (set-grid (one-step)))) Now we can run the automata and see how it works. Let's randomize the grid and run the automaton through 1, 5, and 50 cumulative steps: > (random-grid) > (view 8 3) 5 3 2 4 6 7 6 3 0 9 5 2 1 2 9 7 7 5 3 8 6 8 7 8 > (run) > (view 8 3) 2 8 1 2 3 6 3 8 0 0 2 1 4 1 0 6 6 2 8 4 3 4 6 4 > (run 4) > (view 8 3) 1 4 4 1 1 2 1 4 0 0 1 4 2 4 0 2 2 1 4 2 1 2 2 2 > (run 45) > (view 8 3) 1 4 4 1 1 2 1 4 0 0 1 4 2 4 0 2 2 1 4 2 1 2 2 2 We don't have to run this automaton for many more steps to real- ize that it's in a steady loop; all 0 cells will remain 0, and all other cells will run through the series 4-2-1 over and over. I chose the original rules with this result in mind; it's one of the most common ending conditions of a cellular automaton. After a certain number of loops, a cellular automaton typically settles down into one of a few basic patterns of activity: o It can become moribund, with no cells ever changing. o It can become trapped in a steady, repeating pattern. o It can become chaotic, displaying no readily apparent pat- tern. o It can alternate irregularly between steady patterns and chaotic activity. Let's define another automaton, this time one that's a little more interesting (that is, one of the latter two varieties). 1.2.3 Defining a More Complex Automaton This time we'll create an automaton in which neighboring cells do influence one another. The automaton I have in mind is a varia- tion on Conway's Game of Life, called "9 Life." As before, each cell can have any one of ten states, 0 through 9, but the rules are somewhat more detailed: o For each cell in the automaton, get the state values of the cell's neighbors, and count the number of neighbors that have non-zero values. o If the number of non-zero neighbors is either less than 1, or greater than 3, subtract 1 from the cell's value (unless it is already zero, in which case it remains zero). o If the number of non-zero neighbors is either 2 or 3, add 1 to the cell's value. o Otherwise leave the cell unchanged. Rest assured, there is method to this madness, as we'll see when we run this automaton. Of course, I also have to define what I mean by "neighbors". There are two well-known types of neighborhoods that are used in two-dimensional cellular automata, the Von Neumann neighborhood and the Moore neighborhood. The Von Neumann neighborhood is the four cells immediately ad- joining a given cell; in other words, its neighbors to the north, east, south, and west. The Moore neighborhood adds the four diag- onal neighbors as well, for a total of eight cells. The Game of Life happens to use the Moore neighborhood, but we'll design our tools so that we can choose either neighborhood when we run the 9 Life automaton--that way we can see the differences between the two. No matter which neighborhood we choose, however, we'll need a function that will cause each cell to check the values of its neighbors, make a count of the ones that are currently positive, and update its own value. If we were to write such a function for an ordinary serial com- puter, it would most likely consist of a loop that steps through the automaton cell by cell, doing the count for each cell indivi- dually. A serial one-step function would look something like: (defun serial-one-step (array-x-size array-y-size) (dotimes (x array-x-size) (dotimes (y array-y-size) (let ((neighbors (list (get-cell (- x 1) y) (get-cell (+ x 1) y) (get-cell x (- y 1)) (get-cell x (+ y 1)) ... ))) (store-new-cell-value x y (new-value neighbors))))) (update-grid)) Here, get-cell gets the value of a cell from the automata grid, new-value calculates the new value for a cell based on the values of its neighbors, store-new-cell-value stores the new value of the cell in a temporary array, and update-grid updates the grid by storing the new value for each cell into the automata grid. There are three important things to notice about this function: o The function must explicitly refer to the actual shape of the grid, as described by the arguments array-x-size and array-y-size. o The function includes a double dotimes loop to step through all the cells. o The function must temporarily store the new value for each cell, and then update the values for all the cells after the loop has been completed, so that the new values of cells reached earlier in the loop don't influence the calculations for cells reached later in the loop. Things are much simpler on the CM. Remember that each cell of the automaton is stored in the memory of a different CM processor. We can simply have the processor associated with each cell find the state values of the cell's neighbors, count the ones that are po- sitive, and then update the cell accordingly. All the cells will update simultaneously, eliminating the need for any looping as well as the need to temporarily store the new value for each cell outside the grid. Before we can write the function that does this, however, we'll need to look at how we can use *Lisp opera- tions to cause the CM processors to exchange values. There are a number of *Lisp operators that cause the CM proces- sors to exchange values with each other. What we want is a opera- tor that will instruct each cell in the automaton to get the value of a single neighboring cell (for example, one to the north, one to the east, etc.). In *Lisp, the tool we want is news!!. For example: > (random-grid) > (view 8 3) 2 3 1 0 2 7 2 4 ;;; Dimension 0 runs across 4 5 6 5 4 6 8 0 ;;; Dimension 1 runs down 6 8 4 3 8 1 7 9 > (set-grid (news!! *automata-grid* -1 0)) > (view 8 3) 0 2 3 1 0 2 7 2 1 4 5 6 5 4 6 8 0 6 8 4 3 8 1 7 In this example, I've used news!! to shift the entire contents of the *automata-grid* by one cell. The grid coordinates given to news!!, (-1, 0), are interpreted by each processor as being rela- tive to that processor's position in the grid. So the coordinates (-1, 0) cause each processor to get a value from the processor that is one step to the "west" across the grid. Each processor gets the cell value of its west neighbor, and stores that value in its own cell. FOR THE CURIOUS: You may be wondering where the numbers in the left-most column came from. A useful property of news!! is that it "wraps" automatically at the edges of the grid. The cells on each edge automatically wrap to their counterparts on the opposite edge, so there's no need to do anything special at the edges of the grid. This also means that we don't have to worry about the actual size of the grid. Our code will run unchanged on CMs of any processor size. Who Are the People in YOUR Neighborhood? Let's use news!! to define a set of functions that will cause each cell to count the number of its non-zero neighbors according to the current neighborhood we've chosen. We'll want to be able to switch neighborhoods, so let's define a global variable that will determine the neighborhood that we want these functions to use. > (defvar *neighborhood* :neumann) Now for the counting functions themselves. There are many ways to write them, of course, but what we want is something simple and readable. Here's a function that will get the sum of neighbors in the Von Neumann neighborhood: > (defun neumann-count (grid) (+!! (news!! grid 0 -1) ;; north (news!! grid 0 1) ;; south (news!! grid -1 0) ;; west (news!! grid 1 0) ;; east )) and here's one that does the same thing for the Moore neighbor- hood. > (defun moore-count (grid) (+!! (news!! grid 0 -1) ;; north (news!! grid 0 1) ;; south (news!! grid -1 0) ;; west (news!! grid 1 0) ;; east (news!! grid -1 -1) ;; northwest (news!! grid -1 1) ;; southwest (news!! grid 1 -1) ;; northeast (news!! grid 1 1) ;; southeast )) It will be really convenient if we can call a single function and have it choose the right helper function for our current needs. Here's the function we'll use: > (defun neighbor-count () (*let ((grid (signum!! *automata-grid*))) (ecase *neighborhood* (:moore (moore-count grid)) (:neumann (neumann-count grid))))) Here we've used *let, the *Lisp version of let, to define a local pvar whose value is a copy of the *automata-grid* with all non- zero cells set to 1. When we pass this grid to the neighbor- counting functions, we obtain just a count of the number of non- zero neighbors. FOR THE CURIOUS: The *Lisp function signum!! is the parallel version of Com- mon Lisp's signum; the signum!! function takes a numeric pvar argument, and returns a pvar that contains 1, 0, or -1 for each processor, depending on whether the argument pvar was positive, zero, or negative for that processor. One Small Step...for 9 Life Having these neighbor-counting functions, we can redefine the one-step function for the 9 Life automata as: > (defun one-step () (*let ((count (neighbor-count))) (cond!! ; When count is < 1 or > 3, subtract 1 if not zero. ((or!! (!! count 3)) (if!! (zerop!! *automata-grid*) *automata-grid* (1-!! *automata-grid*))) ; When count is 2 or 3, add 1 ((<=!! 2 count 3) (1+!! *automata-grid*)) ; Otherwise, leave cells unchanged (t *automata-grid*)))) In this function we're using another *Lisp conditional operator, cond!!. Similar to Common Lisp's cond form, cond!! evaluates mul- tiple tests and executes branches of code in separate groups of processors. As with if!!, cond!! returns a pvar whose value depends on the execution of its branches. The value of a cond!! form for each processor is simply the value of the cond!! clause that processor executed. This is handy, because we want to set the value of *automata-grid* based on the results of a number of parallel tests, but we don't want to have to write a separate (set-grid (if!! ... )) expression for each test. Now in order to see the effects of this automaton we'll want to set up a more orderly test case than a random grid. This pair of functions will initialize the grid with a simple pattern: > (defun set-cells (cell-list value) (dolist (cell cell-list) (set-cell (car cell) (cadr cell) value))) > (defun init () (set-grid 0) (set-cells '((2 2) (3 1) (3 2) (3 3) (4 1)) 1) (view)) And let's add a function to our toolbox that will let us run the automaton through a number of steps, and then automatically display the current state of the automaton. This is easy--we'll just reuse the run and view functions we defined earlier: > (defun view-step (&optional (n 1)) (run n) (view)) Let's run this new automaton through 1, 5, and 50 cumulative steps: > (init) 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 1 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 > (view-step) 0 0 0 0 0 0 0 0 0 0 1 2 1 0 0 0 0 0 1 2 1 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 > (view-step 4) 0 0 0 0 0 0 0 0 0 0 5 6 5 0 0 0 0 0 5 0 5 0 0 0 0 0 5 5 4 0 0 0 0 0 0 0 0 0 0 0 > (view-step 45) 0 0 0 0 0 0 0 0 0 0 9 7 2 0 0 0 0 0 4 0 6 0 0 0 0 0 3 4 8 0 0 0 0 0 0 0 0 0 0 0 Strange, isn't it? The automaton seems trapped in a square pat- tern of numbers, determined by the shape of the original pattern of 1's. The cells within the square continue to change in a manner that is not readily predictable, but this pocket of non- zero values can't seem to spread beyond the edges of the square. We're running the automaton with a Von Neumann neighborhood, so that each cell has a limited number of neighbors. What happens if we use the Moore neighborhood? > (setq *neighborhood* :moore) :MOORE Here's the results for runs of 1, 5, and 50 steps: > (init) 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 1 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 > (view-step) 0 0 0 1 1 0 0 0 0 0 1 2 2 0 0 0 0 0 2 0 0 0 0 0 0 0 1 2 1 0 0 0 0 0 0 0 0 0 0 0 > (view-step 4) 0 1 0 0 0 0 1 0 1 0 2 2 4 0 1 0 1 0 0 0 0 2 1 2 1 0 3 4 0 0 0 1 1 0 3 4 0 0 0 1 > (view-step 45) 0 0 0 0 3 0 0 0 4 6 6 1 0 0 0 0 0 0 0 0 0 0 4 3 3 3 8 1 0 0 5 1 0 0 0 0 2 1 0 1 As you can see, the eight-fold pathways of the Moore neighborhood allow the original pattern to spread much further across the grid. FOR SPEED FREAKS: By the way, if the (view-step 45) calls in the above exam- ples seem slow to you, remember that you're running these examples in an interpreted form, which will by definition not run at the maximum speed of your machine. You can com- pile your *Lisp code for speed, turning it into efficient Lisp/Paris code--we'll see how to do this later on. 1.2.4 Mortal or Immortal? An interesting question to ask is: Does the pattern die out for either neighborhood? That is, does the automaton ever reach a state at which every cell is in the zero state, so that no furth- er change is possible? I'll leave that for you to answer, by exploring further the two versions of the automaton. To try either one, remember to choose a *neighborhood*, either :neumann or :moore, and to call the init function to restore the original pattern. Then view-step yourself into oblivion. There's one more tool that you'll need to use to check whether the grid is truly dead. As you know, view shows only a portion of the grid. But the state values produced by the Moore version of this automaton can ripple across the grid, into cells that we can't see using the default width and height arguments to view. Rather than print out the entire grid to make sure there are no live cells left, you can use a simple *Lisp function to do the test for you. It's called *sum: > (*sum (signum!! *automata-grid*)) 208 The *sum function returns the sum of all the values of a pvar. In this example, we've once again made a copy of the *automata-grid* in which every non-zero cell is replaced by 1, and then used *sum to take the sum of this grid's values. This gives a count of the number of live cells in *automata-grid*. When the count reaches zero, the automaton is well and truly dead. So a simple test for the dead state is: > (defun deadp () (zerop (*sum (signum!! *automata-grid*)))) > (deadp) NIL Notice that we're using the Common Lisp zerop test here, not a parallel test, because *sum returns a scalar value, not a pvar. When deadp returns t, you'll know that every cell in the grid-- including the ones that you can't see with view--has been zeroed out. Try running both versions of the 9 Life automaton for a few hun- dred steps, and see if either one dies out. Then try modifying the rules for the automaton, or the initial pattern, and see what happens as a result. See if you make the pattern die out faster, or cause it to fall into a fixed state forever. 1.2.5 Personalize Your System If you've been typing things in all along, and saving the defun and defvar forms into a file as you work, you now have a fairly sizable amount of *Lisp code that implements a limited but gener- ic cellular automata simulator. What do you do with it now? Per- sonalize it, of course. Play around with the code yourself, and modify things to suit your own taste. Enhance the system with your own ideas; there are many ways in which it can be improved. For example, the way things are right now you must redefine the one-step function every time you want to try a different automa- ton. A useful way to extend this automata simulator would be to write operations that let you define many different kinds of au- tomata by name, and to call up any named automaton for use au- tomatically. In an appendix to this document, I've outlined just such a system for your perusal. The appendix also includes a commented copy of all of the *Lisp code used in this chapter. There is also a copy of this code on-line in the *Lisp software directory, in the file /cm/starlisp/interpreter/f6100/cellular-automata-example.lisp Check with your system administrator or applications engineer if you need help locating this file. 1.3 Exiting From *Lisp At this point, let's assume you're ready to call it quits for now, and want to exit from *Lisp. If you're attached to a CM, type > (cm:detach) ;; Detach any attached CM to release it. You can then type the Lisp exit command that is appropriate for your system, as described in the CM User's Guide. For example, Lucid users should type either (lcl:quit) or (sys:quit). Symbol- ics users don't need to "exit" from Lisp, and can simply type (*lisp) to deselect the *Lisp software package. NOTE: If you detach, and then decide that you want to do some more work with *Lisp, simply type > (*cold-boot) to reattach and re-initialize *Lisp. Remember: until you call *cold-boot to attach a CM and ini- tialize *Lisp, you won't be able to execute any *Lisp code!