Why take this class? What do you want to explain? What annoys you?
First, we want to establish the idea that a computer language is not just a way of getting a computer to perform operations but rather that it is a novel formal medium for expressing ideas about methodology. Thus, programs must be written for people to read, and only incidentally for machines to execute.
(Harold Abelson and Gerald Jay Sussman, “Preface to the first edition”, Structure and Interpretation of Computer Programs, MIT Press, 1996)
Computational modeling is not just descriptive curve-fitting, but rather predictive explanation: people can inspect a computational model as a simulation and interact with it as a game. Therefore, it is worth your effort to build up a toolbox for inspecting and interacting with your models. In other words, we should build domain-specific debuggers.
The prologue of the text-adventure game Christminster is a good example of a toolbox for inspecting and interacting with processes, which are an important kind of models (or an important way to think about models). (If you are interested, you can find the source code of this prologue in prologue.inf.)
“I don’t really agree with the game on a political level,” said Mr. Frasca, “but it was a way to get people to discuss it.”
(Clive Thompson, “Saving the world, one video game at a time”, New York Times, 2006-07-23)
The key to making a process debuggable (that is, interruptible and inspectable and interactable-with) is to express it as transitions among states. For example, we can express the summing process
(define (sum n accumulator)
(if (> n 0)
(sum (- n 1) (+ accumulator n))
accumulator))
(sum 10 0)
as a transition function
(define (step state)
(let ([n (first state)]
[accumulator (second state)])
(if (> n 0)
(list (- n 1) (+ accumulator n))
accumulator)))
(step (list 10 0))
by taking a state to be a number n
and a
number accumulator
. We can also trace the execution of
this summing process by returning a list of snapshots.
(define (sum-trace n accumulator)
(if (> n 0)
(cons accumulator (sum-trace (- n 1) (+ accumulator n)))
(list accumulator)))
(sum-trace 10 0)
To take another example, we can express the counting process
(define (example2 n)
(if (> n 0)
(begin (write n)
(newline)
(example2 (- n 1)))
'done))
(example2 10)
by taking a state to be a number n
. To take another
example, we can express the trivial “magic trick”
(define (example1 dummy)
(begin (write "First step")
(newline)
(write "Second step")
(newline)
(write "Third step")
(newline)))
(example1 'dummy)
by taking a state to be one of four unit values—say “before first step”, “before second step”, “before third step”, or “done”. (A “unit” is a structure containing nothing.)
It is useful to have a concise language for describing the shape of a state (that is, the set of valid states), for the same reason it is useful to have a concise language for describing the shape of a type of data (that is, the set of valid data of that type): for example, be sure to cover all cases in the transition function. The two most important connectives in such a language are “and” (which specifies a structure, or equivalently, the Cartesian product of two sets of valid data) and “or” (which specifies a mixture, or equivalently, the disjoint union of two sets of valid data). Please see parts I–III of How to Design Programs for more information about these descriptions and how to design functions using them.
As a general alternative to these state spaces, we can always take a state to be a list of things left to do and define the transition function to keep expanding the first element of this list until the list is empty or the first element is a primitive action. This general recipe is useful for more complex processes, such as the following solution to the Tower of Hanoi.
(define (hanoi n from to using)
(if (> n 0)
(begin (hanoi (- n 1) from using to)
(write (list from to))
(newline)
(hanoi (- n 1) using to from))
'done))
(hanoi 3 'a 'b 'c)
Such a state is called a stack or a continuation.
By juggling multiple processes, each expressed as a state space and a transition function, we can essentially build an operating system that oversees the entire simulation and runs it on a single processor. Making the state explicit (rather than relying on the state-space machinery built in to a programming language) lets us manage the simulation and inspect it as we wish.
If it weren’t for communication among these processes, we would be able to run the simulation faster by using multiple processors in parallel. (For one example, see Section 4, “Parallelisation”, in this paper about computational chemistry simulations.) In general, a good way to understand an interacting system of processes is to pay attention to exactly what information is communicated among processes, in other words how they depend on each other.
If different actions take different amounts of time, then the state transition function should not just return a primitive action and the next state but also the duration of the action, that is, when to next wake up that process. The operation system aka simulation engine should then keep a queue of pending events aka wake-ups, and always process the earliest item in the queue. Thus the simulation can fast-forward past moments during which nothing happens. The queue can be thought of as a sorted deck of cards. This technique is called “discrete event simulation”.