Life: Nasty, brutish, and short
Authors: E. E. McDonnell
Venue: ACM SIGAPL APL Quote Quad (1987)

This paper describes a series of functions for performing Conway's game of Life [Ga70] in APL, beginning with versions that go back to the early 1970's. The paper doesn't deal with the game itself, but rather with the expressive power of various approaches, and particularly with the increased expressiveness found in some of the new operator extensions to APL. Given a state of the game, which we can think of as a Boolean matrix, the object, from the point of view of this paper, is to produce the next state, or generation, of the game. The rules for providing the next Boolean matrix are: consider the eight cells surrounding a given cell in the current matrix (ignoring border cells); the corresponding cell in the next matrix will have the value 1 if the current value is 0 and exactly three of the surrounding cells are 1, or if the current value is 1 and either two or three of the surrounding cells are 1. Expressed this way, it is easy to see why many APL versions of Life have appeared since the game was first discussed in 1970, since APL is admirably suited to dealing with matrices in general and Boolean matrices in particular. Several APL versions of the game of Life appeared in the magazine APL Quote-Quad. The first of these was given by Duby [Du71]. I have taken the liberty of changing the display form of the function, in order to allow it to be studied more easily. In its original form, this was a seven-line function. I have broken it into many more lines by putting on separate lines all uses of assignment, and removing all uses of locutions such as .op. The function uses index origin 0. The part of this function that we are interested in begins at label Next, and goes to the end of the function. This is the part where the next state is computed. Unlike most of the other versions we will study, this one attends to the matter of growing and shrinking the size of the matrix in accordance with the location of the living cells in each state. The assignment to &fnof; in Loopy is the actual development of the next state. Note that it treats the creation of the next state as a scalar operation, computing Loopy x times y times, where x is the number of rows in the matrix and y is the number of columns. For each cell, the values in the neighbor cells are computed, and used to determine the value of the corresponding cell in the next state. Thus, this first recorded attempt doesn't make much use of APL's ability to describe matrix operations &ldquo;all at once&rdquo;. The obvious inefficiencies of the approach of Algorithm 70 led to the appearance of two new versions in the following year. The first of these that I will discuss is due to Bonyun [BO72]. This function as it originally appeared had only four (very long) lines. I have performed the same untangling for it that I did for Algorithm 70. This function uses index origin 1. It also takes care of growing and shrinking the matrix mat according to the size of the living cell population. The five lines of the function that I have marked with a comment symbol determine the next state, using a strategy that will become familiar. In the line marked (*), eight terms enter into the formation of a sum; each of these terms gives one of the eight neighbors of each cell, and the sum gives the total number of living neighbors of each cell. Thus, the function makes good use of APL's matrix-handling abilities. The eight neighbors are found by appropriate rotations of the matrix, in various combinations. Thus, the neighbor cell to the upper left of the cell is found by -1&phgr;-1&thgr; (the term shown as (-1&phgr;h)), and so forth. The function uses a global variable mat, and I shall stigmatize a function which uses a global variable as nasty. In the same issue as Bonyun's algorithm appeared the next solution to the problem. It was provided by W.J. Jones [Jo72]. It is in index-origin 1. It assumes a fixed sized state matrix of 20 rows by 20 columns. Jones determines the next state in the two lines marked with the comment symbol. Essential to his solution is the matrix r, which gives the indices of the state matrix in staggered form: 2 3 4 5 6 7 8 9 10&hellip;18 19 20 1 1 2 3 4 5 6 7 8 9&hellip;17 18 19 20 20 1 2 3 4 5 6 7 8&hellip;16 17 19 19 This is used to give all possible vertical and horizontal alignments of an element with adjacent elements. A solution which uses such a brute-force approach to a solution I shall call brutish. The first line marked with a comment symbol develops t by using r as both a row and a column index to the state matrix a, thereby giving a four-dimensional result, and summing this along the first and third dimensions; this is used in the next commented line to determine the living cells in the next state via a simple pair of tests. Jones displays the successive states followed by a separator line which includes the state number and the number of living elements. The next version of Life appeared in Apl Quote-Quad in 1974 [Si74]. The authors were high school students, who describe their version as follows: [The function] has the syntax: Increment Life Matrix where Increment is how often the pattern is to be printed (1 = every generation, 2 = every other generation, etc.) and Matrix is a binary matrix which has 1's for occupied cells and 0's for unoccupied cells. The function operates in either origin and prints the character 0 for occupied cells. It also prints a set of statistics with each generation printed, showing: population, births, deaths, and survivals. The program will stop and print an explanatory message if the pattern dies out or becomes stable. The program operates on the principle of creating eight identical matrices of the pattern and offsetting each one in a different direction. When these matrices are summed together the resulting matrix represents how many neighboring occupied cells each cell has. The matrix (called sum in the program) is then used to compute the succeeding generations. This program uses little CPU time because the only primitive functions used to compute the sum matrix are + and &uarr; which are two of the fastest primitives. I've tried to make this function more readable by various means, but it does not seem to be a notational improvement over the ones we have already seen. Our next example comes from the year 1984. In that year one of the commercial PC publications contained an article [Wy84] which compared several programming languages in terms of their ability to describe the game of Life. One of the languages used was APL. The example in APL was nicely structured, and there were several functions, of which I shall show only two: the outermost function called LifeGame and the function which produces the next generation, called NextGen: The best comment on this comes from Donald McIntyre, who wrote: This is an excellent representation for the purpose of the beginning tutorial for which it was written. Its clarity is admirable. But it is not the final formulation that I would want my students to aim for. Long names for variables are no doubt necessary in programs such as Fortran and COBOL that commonly run to hundreds of lines, but just as long names are never used in formulas by mathematicians, physicists, or engineers, so they are unnecessary in short functions (often of a single line) defined in APL. Long names have a superficial appearance of good documentation, but in my opinion they tend to conceal rather than illuminate the operation of functions containing them. This quotation comes from an article submitted by McIntyre to the same journal, but which was never printed [McI84]. He shows how the logic of the NextGen function can be considerably simplified, by applying the rules of logic, and then presents a suite of functions of his own, embodying the principles he espouses. I found out about this unprinted article in a conversation with Don, and asked him if he would send me the article. He did that, as well as a floppy disk containing an STSC APL workspace for the IBM PC, that played the game using Don's functions. There are just six functions in the suite, each having just one line. The two of interest to us are those which compute the next state of the system: Next: (3 = n) v &ohgr; ^ 2 = n &larr; CN &ohgr; CN: (1&phgr;&ohgr;)+ (-1&phgr;&ohgr;)+(1&thgr;&ohgr;)+(-1&thgr;&ohgr;)+ (1&phgr;1&thgr;&ohgr;)+(1&phgr;-1&thgr;&ohgr;)+ (-1&phgr;1&thgr;&ohgr;)+(-1&phgr;- 1&thgr;&ohgr;) I trust you will agree that McIntyre's Next function is easy to read, given the knowledge that the function CN computes the number of neighbors for each cell. It says that a cell in the next state is alive if the cell in the current state has three neighbors, or if it is now living and has two neighbors. You may recognize that the CN function uses a technique almost exactly the same as that used by Bonyun in 1972: summing the results of all eight rotations of the state matrix. McIntyre uses the direct definition style popularized by Iverson and Orth in their mathematical textbooks [Iv76, Or76]. McIntyre's effort inspired me to see whether it might not be possible to abbreviate even further. Suppose, I asked, that instead of using eight separate rotation expressions, containing twelve separate instances of one or the other rotation function, one were to assume eight copies of the current state matrix, forming a three-dimensional array, and that this three dimensional array were to be rotated by two phrases, one to give horizontal rotations, and one to give vertical rotations. For example, one might write: &ngr;&thgr;h&phgr;(8,&rgr;&ohgr;)&rgr;&ohgr;, where &ngr; is the vertical rotations desired, 1 1 1 0 0 -1 -1 -1, and h is the horizontal rotations desired, 1 0 -1 1 -1 1 0 -1. You may have already perceived the difficulty. Ordinarily, the rank of the left argument to rotate must be one less than that of the right argument: thus a scalar is used to rotate a vector, a vector to rotate a matrix (one element for each row), and so forth. However, a scalar left argument may be used with a matrix, and it is extended in the usual manner: 1&phgr;M means to rotate each row one to the left. Unfortunately, there is no corresponding rule in the case of a three-dimensional array: it is not possible to supply a vector argument, and have the elements of the vector apply as scalars to the matrices of the right argument. This means that, instead of the vector 1 1 1 0 0 -1 -1 -1, I'd need to have the matrix 8 8&rgr;1 1 1 0 0 -1 -1 -1, and so forth. It seemed inefficient to produce a matrix in which all the rows were identical. It would be nice if I could use a vector, and have each scalar element of the vector apply to a corresponding matrix. The APL system I was using was SHARP APL, which had implemented the rank operator, as proposed by Arthur Whitney [Wh82] and discussed by Iverson [Iv83]. The rank operator allows one to specify the applicable ranks of the subarrays used as arguments to the function. In the case at hand, it suggested that to get scalars from a vector on the left to apply to matrices on the right, an expression of the form rotate rank 0 2 (that is, &phgr;@@@@0 2) would be appropriate, since I wanted to use rank 0 elements (that is, scalars) on the left, and rank 2 elements (that is, matrices) on the right. The function Nbr below, illustrates this usage. Since the major object of this paper is to show the importance of new operators in APL, I'm going to spend a fair amount of time discussing the Nbr function. The first phrase to be executed is h &phgr;@@@@0 2 &ohgr;. In words, this says rotate the rows of &ohgr; in accordance with h, using scalars from h to rotate the matrices of &ohgr;. The way to read a phrase is to look from the left for the first operator, in this case @@@@. Since @@@@ is dyadic, to find its left argument we look to its left for the longest operator expression. In our case, this is the dyadic function &phgr;. Its right argument is the vector 0 2. Thus we have as our derived function &phgr;@@@@0 2, which can be read as rotate rank 0 2, that is, use scalars from the left argument to rotate matrices from the right argument. This derived function is dyadic, with left argument h and right argument &ohgr;. The result of h is a vector of eight elements. If we assume &ohgr; to be a matrix of 20 rows and 20 columns, what is the shape of h &phgr;@@@@0 2 &ohgr;? Well, we want each scalar element of the left argument to be used with the sole matrix right argument, so we'd like to have a result with shape 8 20 20. This is exactly what happens! We call the elements of the derived function's arguments that have the ranks indicated by the rank argument (0 2 in our case) the cells. The remaining parts of the derived function's arguments are called the frames. For the left argument, the scalars (the rank zero elements) are the cells, and the vector is the frame. For the right argument, the matrix (the rank two element) is the cell, and the frame is empty. When the frame is empty, the cell is used with each cell of the other argument. You are familiar with the idea of scalar extension: the corresponding notion in connection with the rank operator is a generalization of scalar extension called cell extension; in the case at hand, the cell (that is, the matrix), is replicated eight times to conform to the frame of the left argument. So, now we have the result of the horizontal rotation phrase h &phgr;@@@@0 2 &ohgr;, with shape 8 20 20. This becomes the argument to the vertical rotation phrase &ngr; &thgr;@@@@0 2. This time, the left argument has shape 8 and the right argument shape 8 20 20, so there is no cell extension, because the frame shapes are both 8 and thus agree. The shape of the result of this phrase is also 8 20 20. The next phrase is +@@@@, which sums the matrices along the leading dimension, giving as the final result a 20 by 20 matrix containing in each cell the number of neighbors for the corresponding cell of the original matrix argument. The New function uses the Nbr function to obtain the count of neighbors for each cell, then tests to see which counts are equal to either 2 or 3, giving a 2 by 20 by 20 result, with the first matrix plane having a 1 where the neighbor count is 2, and the second matrix plane having a 1 where the neighbor count is 3. This provides the right argument to the ^ function. The left argument is also a 2 by 20 by 20 array, where the first plane is simply the original argument &ohgr; (formed by 0v&ohgr;) and the second plane all 1's (formed by 1v&ohgr;). And-ing the first plane of the left argument with the first plane of the right argument produces a 1 for each cell with a neighbor count of 2 which is presently 1; and-ing the second plane of the left argument with the second plane of the right argument simply produces a 1 wherever the neighbor count is 3. Finally, or-ing across the first dimension of this 2 by 20 by 20 result gives the desired new state as a 20 by 20 Boolean matrix. Those of you familiar with IBM's APL2 may at this point feel that a function to Nbr could be written using APL2's each operator [Gh73], and so it can. The function Nbre below is in APL2: Nbre: +@@@@ &sup; &ngr; &thgr;.. h &phgr;.. &sub; &ohgr; The number of tokens in Nbre is the same as in Nbr, but the approach reflects the difference between the rank and the each operators. The each operator, deriving a dyadic function, is used to pair each item of h with &ohgr;, since &ohgr; has been made into a scalar by enclosing it; the result has the shape of h, in this case, eight. This gives us a list of enclosed matrices, each rotated on the last axis by the corresponding element of h. This becomes the argument to &ngr; &thgr;.., which results in another eight-element list, again of enclosed matrices, with these rotated along the first axis, according to the corresponding element of &ngr;. This result is disclosed, giving an 8 by 20 by 20 result; summing this on the first axis gives the desired 20 by 20 result. The difference in the viewpoints fostered by SHARP APL and IBM's APL2 is worth noting. Although the number of tokens is the same in each function, there are only three functions in the SHARP APL expression (+@@@@, &thgr;@@@@0 2, and &phgr;@@@@0 2), but five functions in IBM's APL2 (+@@@@, &sup;, &thgr;.., &phgr;.., and &sub;). IBM's APL2 approach requires the enclose function to make the matrix a scalar, in order to get the benefit of scalar extension. It then needs the each operator to modify the rotates, in order to have them apply itemwise. After this, the disclose is required (it could go to the left of the +@@@@ as well as to the right) in order to give back the simple matrix result. In contrast, the SHARP APL approach doesn't require the Sharp equivalent of an enclosed array, and its subsequent disclosure, since it uses the generalization of scalar extension called cell extension, which is built-in to the rank operator. This was the state of things until early 1986. I was reasonably happy with what I had. Then I chanced to read (for reasons having nothing to do with the game of Life) Donald Knuth's book on his typefont designing system, Metafont [Kn86]. Among the data types in the METAFONT language is the picture. Looked at from the APL point of view, a METAFONT picture is a numeric matrix. The elements of a picture describe the way a plane surface is marked, that is, they describe a picture. Pictures can be added or subtracted, and can be shifted, reflected, and rotated by multiples of 90 degrees. In other words, METAFONT has stumbled through a back door into the same area where APL has held sway all alone for so many years. The book gives the following Exercise 13.24: In John Conway's &ldquo;Game of Life,&rdquo; pixels are said to be either alive or dead. Each pixel is in contact with eight neighbors. The live pixels in the (n+1)st generation are those who were dead and had exactly three live neighbors in the nth generation, or those who were alive and had exactly two or three live neighbors in the nth generation. Write a short METAFONT program that displays successive generations on your screen. Turning to the answers section of the book, I found Answer 13.24: 13.24 (We assume that currentpicture initially has some configuration in which all pixel values are zero or one; one means &ldquo;alive.&rdquo;) picture v; def c = currentpicture enddef; forever: v := c; showit; addto c also c shifted left + c shifted right; addto c also c shifted up + c shifted down; addto c also c - v; cull c keeping (5, 7); endfor. (It is wise not to waste too much computer time watching this program.) I was impressed by this algorithm, because it reduced the number of vertical and horizontal rotations required from the eight that I had used in Nbr to three. Racing to my APL machine I translated Knuth's METAFONT algorithm into APL. I also began counting tokens at this point. The APL version of Knuth's algorithm used 49 tokens. &nabla;z &larr; LifeKnuth &ohgr;&ngr; &ngr; &larr; &ohgr; &ohgr; &larr; &ohgr; + (1 &phgr; &ohgr;) + - 1 &phgr; &ohgr; &ohgr; &larr; &ohgr; + (1 &thgr; &ohgr;) + - 1 &thgr; &ohgr; &ohgr; &larr; &ohgr; + &ohgr; - &ngr; z &larr; &ohgr; &isin; 5 6 7 &dtri; This APL algorithm isn't fully equivalent to Knuth's solution, since his METAFONT shifted primitive shifts in zeros, whereas the APL rotate primitive uses nondestructive cyclic rotation. By ensuring that the argument is bordered by an edge of zeros all around, the APL and METAFONT programs will be completely equivalent. The workings of this algorithm may not be immediately evident. What is happening was expressed as follows [Hu86]: in the pattern x x x x @@@@ x x x x encode ones in the x cells by 2, and in the @@@@ cell by 1, then sum. &ldquo;Winning&rdquo; cells are those where sums are 5 6 7. Like McIntyre, I also prefer to use the direct definition style of programming. I further impose on myself the burden of using no assignment statements, and as few parentheses as possible. Using these criteria, I translated LifeKnuth into the function lf, which uses 23 tokens: lf: ((2x+@@@@-1 0 1&thgr;@@@@0 2+@@@@-1 0 1&phgr;@@@@0 2 &ohgr;)-&ohgr;)&isin;5 6 7 A similar program in APL2, also 23 tokens long, is lfe: lfe: ((2x+@@@@&sup;-1 0 1&thgr;..+@@@@-1 0 1&phgr;..&sub;&ohgr;)-&ohgr;)&isin;5 6 7 At the time, the commute operator had been defined [Iv87], but not yet implemented in SHARP APL, and I was intrigued by it as a way of avoiding parentheses, so I wrote the function l&ngr;, which uses 21 tokens. The commute operator applied to a dyadic function reverses the order of the arguments. For example 3-@@@@ 8 is 5. Using commute (21 tokens): l&ngr;:5 6 7&isin;@@@@&ohgr;-@@@@2x+@@@@-1 0 1&thgr;@@@@0 2+@@@@-1 0 1&phgr;@@@@0 2&ohgr; If APL2 had a commute operator, a similar function lve, also 21 tokens long, could be written: lve:5 6 7&isin;@@@@&ohgr;-@@@@2x+@@@@&sup;-1 0 1&thgr;..+@@@@-1 0 1&phgr;..&sub;&ohgr; I communicated the functions lf and l&ngr; to a group on the I.P. Sharp internal system mailbox whose members are especially interested in coding techniques. I should have known better. In less than half an hour I received a reply from one of the members of the group [Hu86], entitled &ldquo;shorter life&rdquo;: l&ohgr; 2 3 &isin;@@@@(,3) -3@@@@(+. &and;@@@@,.. (4 &ne; 19)) &ohgr; This function uses the tesselation operator -3@@@@&ngr; but I won't be describing this until a bit later. I won't describe the function l&ohgr; at all, since it was soon superseded. The author of this reply claimed that his l&ohgr; was shorter than my l&ngr;. In fact, it does have fewer characters, but I pointed out to him that I was counting tokens, not characters (-1 0 1 is one token). Before exploring the issues this led to, let me give you the contribution of another member of this mailbox group [Sc86], who claimed that + could serve at least as well as -: lh1:6 7 9&isin;@@@@&ohgr;+2x+@@@@-1 0 1&thgr;@@@@0 2+@@@@-1 0 1&phgr;@@@@0 2&ohgr; The explanation of lh1 is like that for LifeKnuth, where the encoding still gives a weight of two to x, but gives a weight of three to @@@@. By using + instead of - the need for one commute operator disappears, so that lh1 is only 20 tokens long, compared to the 21 needed by l&ngr;. The same author also contributed a highly contrived solution, just for fun, using the &ogr;&ohgr; and &agr;&ogr;&ohgr; functions, in just 22 tokens (I haven't verified that this works): lh2: (0 1 103 3 4) &isin;@@@@&ohgr;&order;+@@@@-1 0 1&thgr;@@@@0 2+@@@@ -1 0 1&phgr;@@@@0 2&ohgr; My first respondent retorted (in an hour and a half) to my rule about token counting with two new functions, called &ldquo;such is life&rdquo;, of 19 and 14 tokens, respectively [Hu86], which I will spare you because (as pointed out by my second respondent [Sc86]) they failed to conform to the rules of the game (they erroneously gave birth whenever the neighbor count was 2, regardless of whether the cell was currently alive or dead). In any event, the first respondent was undeterred, and in 12 hours sent the function lz [Hu86], calling it &ldquo;life is like that&rdquo; (15 tokens long): lz:5 6 7&isin;@@@@(&ohgr;-3@@@@,@@@@3 3)+.x2 2 2 2 1 2 2 2 2 His claim was that, once he had the 3 by 3 cells in hand, any computable rule is straightforward. This requires a bit of a detour while I explain to you about the tesselation operator. The tessellation operator was introduced in [Iv83], as a subcase of the cut operator, but has undergone minor changes since then. The current definition is as given in [Iv87]. There, you may read: The case 3@@@@&ngr; &hellip; has left rank 2, and &agr; 3@@@@&ngr; &ohgr; applies &ngr; to each element produced by a tessellation of &ohgr; using a size &agr; [1;], and beginning points that are multiples of the &ldquo;shift&rdquo; &agr; [0;]. For example: &agr; &ohgr; 3 2 abcdef 2 3 ghijkl mnopqr stuvwx yzABCD &hellip; The case -3@@@@&ngr; is equivalent to 3@@@@&ngr; except that shards of shape less than &agr;[1;] are omitted. (Note: The word &ldquo;tessellation&rdquo; means a covering by tiles. In the context of the cut operator, it means to produce subarrays of a given size, and with a given spacing, derived from an argument array.) With this explanation of the tessellation operator digested, we can attempt to understand lz. This function says something like this: tessellate &ohgr; into all possible three by three submatrices, ravel these, and form the inner product of each nine-element vector so formed with the vector 2 2 2 2 1 2 2 2 2, thus giving as many scalar elements as there are submatrices; produce a result in which, for each of these scalar elements having a value of 5, 6, or 7, a 1 appears, with 0's appearing elsewhere. Nothing daunted, I attempted to see if I could come up with something shorter still. I realized that there were 140 matrices of size three by three which correspond to &ldquo;winning&rdquo; arrangements; these consist of all those which have exactly two out of eight of the outer cells equal to one (2!8 of these, or 28), plus twice the number for which exactly three out of eight of the outer cells are equal to one (2 x 3!8 or 2 x 56 or 112). If one were to take the 2 &perp; of the ravel of each of these, one would get a vector of 140 elements; if this were upgraded, the first and last few elements would be 7 11 13 &hellip; 408 416 432. It would take a good bit of typing, but one could define a function l&szlig; as follows: l&szlig; 7 11 13&hellip;408 416 432 &egr;@@@@2@@@@ &ohgr; - 3@@@@,@@@@ 3 3 which requires 11 tokens. Also, because of the exhaustive list of integers, it qualifies as a brutish solution. What this does is to tessellate the argument into all possible three by three matrices, ravel these, and take the two base value of the ravels; it then looks to see which of the resulting items appear in the list of 140 elements (where &hellip; stands for the omitted elements), giving a 1 where the item is in the list, and 0 elsewhere. But wait: that's not the shortest yet. Suppose that instead of taking the two base value of the 140 matrices, one were to have instead a global (that is, nasty) 140-element vector of enclosed three by three matrices, where each matrix was a distinct one of the winners: let's call this array &Ogr;. The first and last few elements of the global vector &Ogr; of winning matrices look like this: 0 0 0 0 0 0 0 0 0 1 1 0 1 1 0 1 1 0 0 0 0 0 0 0 0 0 1&hellip;0 1 1 1 0 0 1 1 0 1 1 1 0 1 1 1 0 1 0 0 0 0 0 0 0 0 0 7 11 13 &hellip; 408 416 432 Then one could produce the function lnbs, which is nasty, brutish, and short (9 tokens): lnbs: &Ogr; &egr;@@@@&ohgr; -3@@@@<@@@@3 3