Conway's Game of Life
From the SETL Wiki
Conway's game of life happens to be a good example for illustrating the conciseness of SETL.
If you don't know what Conway's Game of Life is, see the Wikipedia article.
Conway's Game of Life in SETL
First, the code:
program life;
const
initialMatrix =
[".....",
"..#..",
"...#.",
".###.",
"....."];
loop
init
s := initialLiveSet();
do
output(s);
nm := {[[x+dx, y+dy], [x, y]]: [x, y] in s, dx in {-1..1}, dy in {-1..1}};
s := {c: t = nm{c} | 3 in {#t, #(t less c)}};
end;
proc output(s);
system("clear");
(for y in [0..24])
(for x in [0..78])
nprint(if [x, y] in s then "#" else " " end);
end;
print();
end;
select([], 250);
end proc;
proc initialLiveSet();
return {[x,y]: row = initialMatrix(y), c = row(x) | c = '#'};
end proc;
end program;
How it works
First, we choose an initial pattern, in this case a glider:
const
initialMatrix =
[".....",
"..#..",
"...#.",
".###.",
"....."];
...
s := initialLiveSet();
...
proc initialLiveSet();
return {[x,y]: row = initialMatrix(y), c = row(x) | c = '#'};
end proc;
The program could be improved, e.g. by reading the initial pattern from a file.
Now we enter the infinite loop, repeatedly displaying the current generation and rendering the next.
loop ... do output(s); ... end;
Obviously we would rather see the output in matrix form rather than a list of pairs of numbers. We assume the only I/O facility available is the console. A fixed region of the universe is displayed (between [0, 0] and [78, 24]), even though there may be live cells outside this region. Live cells are displayed as "#", dead cells as spaces.
There is no built-in function to clear the screen, so we rely on the unix "clear" command instead, invoking it through the shell.
Finally we introduce a short (0.25 second) delay, so the user has a chance to see the result.
proc output(s); system("clear"); (for y in [0..24]) (for x in [0..78]) nprint(if [x, y] in s then "#" else " " end); end; print(); end; select([], 250); end proc;
Now for the interesting bit - the code that computes successive generations.
For each existing live cell we examine a 3x3 region around it and generate a map (nm or the "neighbour map").
nm := {[[x+dx, y+dy], [x, y]]: [x, y] in s, dx in {-1..1}, dy in {-1..1}};
The domain of this map consists of all cells (live or dead) that are in the 3x3 region surrounding a live cell (this includes the live cell itself.) The range consists of the corresponding adjacent live cells.
All cells that could possibly be live in the next generation are in the domain of nm, so we can use this domain as the set of cells to examine to see whether they should be live in the following generation.
We take advantage of this by iterating over the domain of nm to find potentially live cells:
s := {c: t = nm{c} | ... };
Here c is the candidate cell (the one we are examining to test whether it should become live or remain live) and t is the set of live cells adjacent to c.
Now we apply the rules of Conway's Game of Life.
The rules are typically stated as follows:
- If a cell has 0 or 1 live neighbours in generation n, then it is dead in generation n + 1.
- If a cell has exactly 2 live neighbours in generation n, then it is in the same state in generation n + 1 as it was in generation n.
- If a cell has exactly 3 live neighbours in generation n, then it is alive in generation n + 1.
- If a cell has 4 or more live neighbours in generation n, then it is dead in generation n + 1.
This alternate definition is equivalent:
- A cell C is alive in generation n + 1 if one or both of the following are true in generation n:
- The number of live cells in the 3x3 block surrounding and including C is exactly 3 or
- the number of live cells adjacent to (but excluding) C is exactly 3.
So the guard condition could be #t = 3 or #(t less c) = 3
We can further shorten this by combining the two comparisons as a set membership test, by observing the following equivalence:
(a = x or b = x) <=> (x = a or x = b) <=> (x in {a, b})
The guard condition thus becomes:
3 in {#t, #(t less c)}}
as in the sample code.

