John Scholes' Conway's Game of Life: Difference between revisions
(Separated from main GoL page) |
(Expand on introduction) |
||
Line 5: | Line 5: | ||
Life←{↑1 ⍵∨.∧3 4=+/,¯1 0 1∘.⊖¯1 0 1∘.⌽⊂⍵} | Life←{↑1 ⍵∨.∧3 4=+/,¯1 0 1∘.⊖¯1 0 1∘.⌽⊂⍵} | ||
</source> | </source> | ||
In this page we will break down the function to see how each part operates. If you're completely new to APL, you probably won't understand all of it right away—consider also reading some [[Simple examples]] or APL [[introductions]]. However, you should be able to follow the general ideas. Bear in mind that APL evaluates expressions from [[Evaluation order|right to left]] unless you use parentheses. | |||
The code here is written for [[Dyalog APL]]. It also works in [[ngn/apl]], and other [[Nested array theory|nested]] array languages allow similar implementations: see [[#Translations]]. | |||
== The Glider == | == The Glider == | ||
Line 86: | Line 90: | ||
== How it works == | == How it works == | ||
To compute a new generation in the Game of Life we first need to find out how many neighbours each live cell has. To keep things simple let's consider the following 5-by-5 grid: | To compute a new generation in the Game of Life we first need to find out how many neighbours each live cell has. To keep things simple let's consider the following 5-by-5 grid: | ||
Revision as of 10:55, 12 April 2020
Perhaps the most famous APL video is John Scholes explaining his own implementation of Conway's Game of Life in Dyalog APL.[1] Scholes also published notes[2] on the Game of Life, including alternate implementations, in his dfns workspace.
Here is the full implementation:
Life←{↑1 ⍵∨.∧3 4=+/,¯1 0 1∘.⊖¯1 0 1∘.⌽⊂⍵}
In this page we will break down the function to see how each part operates. If you're completely new to APL, you probably won't understand all of it right away—consider also reading some Simple examples or APL introductions. However, you should be able to follow the general ideas. Bear in mind that APL evaluates expressions from right to left unless you use parentheses.
The code here is written for Dyalog APL. It also works in ngn/apl, and other nested array languages allow similar implementations: see #Translations.
The Glider
One interesting pattern that occurs in Life is a Glider:
glider←3 3⍴1 1 1 1 0 0 0 1 0 glider 1 1 1 1 0 0 0 1 0
To see how the Glider evolves over the generations, we need to give it some space to move around. The Game of Life takes place on an infinite two-dimensional grid, but that might be a bit large for our computer! Let's start the Glider off in the bottom right-hand corner of a 10 x 10 grid:
grid←¯10 ¯10↑glider grid 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 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 1 1 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1 0
After one generation the pattern becomes:
Life grid 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 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 1 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 0 0 0 0
The Glider has slightly changed shape and also moved up and to the left.
Visualising it
To make the pattern easier to see, we can use indexing to place a ⌺
symbol where there's a cell and a ·
otherwise, for example:
Show←{'·⌺'[⎕IO+⍵]} Show grid ·········· ·········· ·········· ·········· ·········· ·········· ·········· ·······⌺⌺⌺ ·······⌺·· ········⌺·
Here's how the Glider evolves over the first ten generations, computed using the Power operator for iteration:
Show¨{Life⍣⍵⊢grid}¨0,⍳9 ┌──────────┬──────────┬──────────┬──────────┬──────────┬──────────┬──────────┬──────────┬──────────┬──────────┐ │··········│··········│··········│··········│··········│··········│··········│··········│··········│··········│ │··········│··········│··········│··········│··········│··········│··········│··········│··········│··········│ │··········│··········│··········│··········│··········│··········│··········│··········│··········│··········│ │··········│··········│··········│··········│··········│··········│··········│··········│··········│··········│ │··········│··········│··········│··········│··········│··········│··········│··········│··········│······⌺···│ │··········│··········│··········│··········│··········│·······⌺··│······⌺⌺··│······⌺⌺··│·····⌺⌺⌺··│·····⌺⌺···│ │··········│········⌺·│·······⌺⌺·│·······⌺⌺·│······⌺⌺⌺·│······⌺⌺··│······⌺·⌺·│·····⌺⌺···│·····⌺····│·····⌺·⌺··│ │·······⌺⌺⌺│·······⌺⌺·│·······⌺·⌺│······⌺⌺··│······⌺···│······⌺·⌺·│······⌺···│·······⌺··│······⌺···│··········│ │·······⌺··│·······⌺·⌺│·······⌺··│········⌺·│·······⌺··│··········│··········│··········│··········│··········│ │········⌺·│··········│··········│··········│··········│··········│··········│··········│··········│··········│ └──────────┴──────────┴──────────┴──────────┴──────────┴──────────┴──────────┴──────────┴──────────┴──────────┘
How it works
To compute a new generation in the Game of Life we first need to find out how many neighbours each live cell has. To keep things simple let's consider the following 5-by-5 grid:
currentGeneration←5 5⍴0 0 0 0 0 0 0 1 1 0 0 1 1 0 0 0 0 1 0 0 currentGeneration 0 0 0 0 0 0 0 1 1 0 0 1 1 0 0 0 0 1 0 0 0 0 0 0 0
Rotation
If we rotate each row of the grid one place left by using APL's Rotate function ⌽
, each element is replaced by its right-hand neighbour. In other words, the new grid will have a 1 for all cells whose right-hand neighbour was 1:
1⌽currentGeneration 0 0 0 0 0 0 1 1 0 0 1 1 0 0 0 0 1 0 0 0 0 0 0 0 0
Similarly we can rotate right to find the left-hand neighbour by using ¯1⌽currentGeneration
. By using APL's Outer Product operator ∘.
together with the Rotate function we can get a set of three grids containing the left-hand neighbour, the original grid, and the right-hand neighbour:
¯1 0 1∘.⌽⊂currentGeneration ┌─────────┬─────────┬─────────┐ │0 0 0 0 0│0 0 0 0 0│0 0 0 0 0│ │0 0 0 1 1│0 0 1 1 0│0 1 1 0 0│ │0 0 1 1 0│0 1 1 0 0│1 1 0 0 0│ │0 0 0 1 0│0 0 1 0 0│0 1 0 0 0│ │0 0 0 0 0│0 0 0 0 0│0 0 0 0 0│ └─────────┴─────────┴─────────┘
Here currentGeneration
must be enclosed before applying the outer product so that it is treated as a single element rather than rotating each of its Boolean elements (which would have no effect).
Extending to two dimensions
Now we can use a similar trick but rotate up and down by using APL's First Axis Rotate function ⊖
. Used with an Outer Product this gives us a series of 9 grids:
¯1 0 1∘.⊖¯1 0 1∘.⌽⊂currentGeneration ┌─────────┬─────────┬─────────┐ │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 1 1│0 0 1 1 0│0 1 1 0 0│ │0 0 1 1 0│0 1 1 0 0│1 1 0 0 0│ │0 0 0 1 0│0 0 1 0 0│0 1 0 0 0│ ├─────────┼─────────┼─────────┤ │0 0 0 0 0│0 0 0 0 0│0 0 0 0 0│ │0 0 0 1 1│0 0 1 1 0│0 1 1 0 0│ │0 0 1 1 0│0 1 1 0 0│1 1 0 0 0│ │0 0 0 1 0│0 0 1 0 0│0 1 0 0 0│ │0 0 0 0 0│0 0 0 0 0│0 0 0 0 0│ ├─────────┼─────────┼─────────┤ │0 0 0 1 1│0 0 1 1 0│0 1 1 0 0│ │0 0 1 1 0│0 1 1 0 0│1 1 0 0 0│ │0 0 0 1 0│0 0 1 0 0│0 1 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 grid in the centre is our original grid, and the others give the neighbours above, below, left, right and diagonally.
Summing it up
Now we can find out how many neighbours each cell in the original grid has by summing (Plus Reduce) the corresponding elements in each of the 9 grids. We sum all the grids at once by ravelling the shape 3 3
matrix of grids to obtain a 9-element vector; otherwise Reduce would only allow us to sum rows or columns. Here APL's representation of Booleans as numbers is advantageous, since otherwise counting the number of true values would require converting them to numbers or using conditional logic. For a live cell the sum is 1 more than the number of neighbours—for example, a 4 in the result means a cell has three neighbours. For a dead cell the result is just the number of neighbours:
+/,¯1 0 1∘.⊖¯1 0 1∘.⌽⊂currentGeneration ┌─────────┐ │0 1 2 2 1│ │1 3 4 3 1│ │1 4 5 4 1│ │1 3 3 2 0│ │0 1 1 1 0│ └─────────┘
For a cell to exist in the next generation, one of two rules must be met:
- Any live cell with two neighbours lives, unchanged, to the next generation
- Any cell, live or dead, with exactly three neighbours is live in the next generation
This means that all cells which might be live in the next generation will have a sum of 3 (from the first rule) or 3 or 4 (from the second rule).
So we're interested in cells with a sum of 3 or 4. We can find these cells with a simple application of Equal to, relying on pervasion and scalar extension to distribute the data properly. The result of the last step was a scalar containing one grid; when paired with the vector 3 4
it is extended to give two grids. Within each grid 3 and 4 are compared with every element by scalar extension again. If the right argument were a flat array rather than being enclosed, we could obtain a similar, but flat, result using another Outer Product.
3 4=+/,¯1 0 1∘.⊖¯1 0 1∘.⌽⊂currentGeneration ┌─────────┬─────────┐ │0 0 0 0 0│0 0 0 0 0│ │0 1 0 1 0│0 0 1 0 0│ │0 0 0 0 0│0 1 0 1 0│ │0 1 1 0 0│0 0 0 0 0│ │0 0 0 0 0│0 0 0 0 0│ └─────────┴─────────┘
The next step is best understood by first considering the two scores 4 and 3 separately.
(a) A score of 4 corresponds either to a live cell with three neighbours or a dead cell with four neighbours. Only in the former case do we get a cell in the next generation - i.e. we get a cell if the score is 4 and the cell is currently live. Here we're using APL's And function, ∧
(⊂currentGeneration)∧4=+/,¯1 0 1∘.⊖¯1 0 1∘.⌽⊂currentGeneration ┌─────────┐ │0 0 0 0 0│ │0 0 1 0 0│ │0 1 0 0 0│ │0 0 0 0 0│ │0 0 0 0 0│ └─────────┘
(b) A score of three corresponds either to a live cell with two neighbours or a dead cell with three neighbours. In either case we get a cell in the next generation:
3=+/,¯1 0 1∘.⊖¯1 0 1∘.⌽⊂currentGeneration ┌─────────┐ │0 0 0 0 0│ │0 1 0 1 0│ │0 0 0 0 0│ │0 1 1 0 0│ │0 0 0 0 0│ └─────────┘
We can write this slightly redundantly by and-ing with 1 (meaning "and the cell is alive or dead") for reasons which will become apparent shortly:
1∧3=+/,¯1 0 1∘.⊖¯1 0 1∘.⌽⊂currentGeneration ┌─────────┐ │0 0 0 0 0│ │0 1 0 1 0│ │0 0 0 0 0│ │0 1 1 0 0│ │0 0 0 0 0│ └─────────┘
So the next step is to say that a cell is live if either test (a) or test (b) is met. We need to use APL's Or function, ∨
. We could write this rather long-windedly as:
(1∧3=+/,¯1 0 1∘.⊖¯1 0 1∘.⌽⊂currentGeneration)∨(⊂currentGeneration)∧4=+/,¯1 0 1∘.⊖¯1 0 1∘.⌽⊂currentGeneration ┌─────────┐ │0 0 0 0 0│ │0 1 1 1 0│ │0 1 0 0 0│ │0 1 1 0 0│ │0 0 0 0 0│ └─────────┘
Refactoring
But we can take advantage of APL's Inner Product operator to do the and operations of each test and then or the result - the ∨.∧
in the following:
1 currentGeneration∨.∧3 4=+/,¯1 0 1∘.⊖¯1 0 1∘.⌽⊂currentGeneration ┌─────────┐ │0 0 0 0 0│ │0 1 1 1 0│ │0 1 0 0 0│ │0 1 1 0 0│ │0 0 0 0 0│ └─────────┘
The final expression ends with a bit of housekeeping using Mix (↑
) to turn the result back into a simple array (it's currently nested).
References
- ↑ Scholes, John. "Conway's Game of Life in APL". 2009-01-26.
- ↑ Scholes, John. life in the dfns workspace.