a small application for running cellular automata from random inputs, using comonads
the program will default to the size of the window
-w
and -h
inputs can be given to determine the width and height, respectively
getOpt
ncurses
(for detecting term width/height)
builds using nix
from a local folder:
nix build .
./result/bin/cellularAutomata
from the repo directly:
nix run github:techieAgnostic/cellularAutomata -- -w 40 -g 25
it may also be included as a flake input, as one normally would, and added to the package list using the included overlay
this program was an exercise in using comonads to simulate cellular automata.
a cellular automaton is defined by a calculation on a local space, which is applied to a global space of cells.
in a one dimensional cellular automata, each cell has two immediate neighbors, to the left and right of it.
additionally, each cell may be in one of two state - Alive, or Dead.
this program has a hard-coded (for now) xor-based rule that works as follows:
regardless of the state of the current cell, it will become Alive if one of its neighbors is alive, but not both, otherwise it will Die.
this local behaviour, when viewed on a global scale, can provide emergent behaviour, in this case a nice triangular pattern (when viewed over time).
a comonad is the dual to a monad.
it is defined by two operations, extract
and duplicate
.
extract
takes a comonad (which wraps some base type) and returns a single item of that type, it can be seen in the semantics of a cellular automata space as extract
ing the focussed cell from the global space.
duplicate
takes a comonad space, and wraps it in an extra layer of comonad, essentially creating a comonad of comonads. in semantics of a cellular automata, we can duplicate
a space into that same space, but focussed on every possible cell in the space.
finally, a comonad is a functor, meaning it can be mapped over. we can use this to our advantage in the semantics of a cellular automata, by creating a function from a comonad space, to a cell state.
this function will be our transition function, and determines the new state of the focussed cell, using the non-focussed parts as its inputs.
by duplicate
ing the comonad space, and mapping the transition function over this new universe of spaces, we get back a single universe, which represents the next step in time.
due to haskell's laziness, the comonad space can (and should) be infinite in both directions, and as long as we remember to bound the space we want to print, our program will work properly.
as such, the functions to change our focus within the space assumes an infinite list, and i have defaulted to "clamping" our focus at the edges if a finite list is given on either direction. im not sure this is semantically the correct choice, but in regular usage (with infinite lists) it should not come up.
currently, as we are animating in a window, the program will turn the initially infinite grid into a grid the size of the window, therefore avoidinig memory leaks at the sides.
in the future if we add the ability to render animated gifs of a cellular automata, it may be of use to keep the infinite grid, in the knowledge that we will not be running it indefinitely, but instead collecting garbage at the end of the animation.
this is the difference between a truly infinite grid, and one that is simply larger than our viewspace (but still finite).