Microbial I
Microbial I is a simulation of the life of a fake micro-organism, Mono, which is lives in a very flat world — not Flatland, but thin, like the layer of water between a microscope slide and the coverslip. Mono has a diameter, an area, a lifespan, and a growth rate. When it grows to a sufficient size, it undergoes (at random) the process of cell-division, giving rise to two daughter cells. The daughters begin life anew, with the minimum area and age zero. As cells age, they change color. Babies are green, young cells are yellow, mature ones are blue, and the old are red. Cell death, like cell-division, is a random process. Once a cell reaches 95% of the nominal life-span of Mono, the grim-reaper comes to remove cells at random. This, of course, implies that cells can survive well beyond the nominal life-span, just as humans do. There are endless variations of such simulations. As an example, in Microbial I the rate of cell division is reduced when the population density near the cell that wishes to divide is too high.
Implementing Microbial I: Types
One can implement a simulation like Microbial I in any computer language that can display colors on a monitor. I chose Elm, a language that I have been using with increasing enthusiasm and love for the last three years. Why Elm? There are many good reasons — no runtime errors, a lightning-fast compiler that is a friendly, expert collaborator and mentor rather than a nag, the ability to refactor without fear, a sane, semantically-versioned package repository, to name four. But I will concentrate on just one: the type system.
A good type system is, first and foremost, a tool for thinking, as are paper, pencil, and eraser. If your model of types come from C (for example), with types for integers, floats, arrays, and a few other things, this may seem like an odd statement. So let’s look at an example. In Microbial I, we have the notions of Species and Organism. These can be modeled by types, e.g.,
type Species = Species Characteristics
type alias Characteristics =
{ name : SpeciesName
, minNumberOfCells : Int
, maxNumberOfCells : Int
, growthRate : Float
, minArea : Float
, maxArea : Float
, color : Color
, lifeSpan : Int
, moves : Motion
}
The name of a species is given by
type SpeciesName = Mono | Brio| Ferocious
where Mono is used in Microbial I and Brio and Ferocious are for future and more interesting versions. The moves
field, a so-called custom or algebraic data type, deserves mention:
type Motion = Immobile | Random Int | Hunter Int
In the case of Mono, moves takes the value Random 1
, which corresponds to the fact that at each tick of the discrete-time clock regulating the simulation, each Mono organism makes a small random movement in the x and y directions. My plan for Brio, which will be a multi-cellular yellow-brown algae, is for move = Immobile
. I haven’t decided yet what its relation to Mono will be. Perhaps Mono organisms like to colonize the branches of Brio, or to feed off them, or both. Or perhaps they avoid Brio, which exudes a short-range but toxic slime. As for Ferocious, it will use something likeHunter 5
. Ferocious will not simply move at random, but rather seek out prey with larger steps, stopping to eat and digest them when it finds them.
Individual members of a species are organisms, modeled by
type Organism =
Organism OrganismData
type alias OrganismData =
{
id : Int
, species : Species
, diameter : Float
, area : Float
, numberOfCells : Int
, position : Position
, age : Int
}
To use these types, both Species and Organisms must be instantiated. Here is Mono:
mono : Species
mono = Species {
name = Mono
, minNumberOfCells = 1
, maxNumberOfCells = 1
, maxArea = 3
, minArea = 1
, growthRate = 0.01
, color = Color.rgb 0 0.7 0.8
, lifeSpan = 600
, moves = Random 1
}
And here is an organism belonging to the species Mono:
monoSeed : Organism
monoSeed =
Organism {
id = 0
, species = mono
, area = 1
, diameter = 1
, numberOfCells = 1
, position = {row = 5, column = 5}
, age = 0
}
We can create others by transforming monoSeed
, e.g., by changing its id
and position
.
Dynamics
What makes a simulation interesting are its dynamics — the way the state of the simulation changes over time. The state is defined by
type alias State =
{ organisms : List Organism
, seed : Random.Seed
, nextId : Int
}
There is not much here—just a list of organisms, which could belong to various species, a seed, and an id. The seed is used to generate random numbers; whenever this is done, we update the seed for the next time a random number is needed. The integer nextId
is used when new organisms are created by cell division: it becomes the id
of the new daughter cell, after which it is incremented in state
so as to have a fresh value ready for the next cell-division.
Now comes the fun part. To transform the current state into the next one, we use
nextState : State -> State
nextState state =
state
|> tick
|> moveOrganisms
|> growOrganisms
|> cellDivision
|> cellDeath
All the components in the function pipeline have the same signature, namely, State -> State
. Their role is mostly guessable from the name. As for tick
it increments the age of each organism.
The pipeline architecture makes it easy to add new dynamics to the system
Mapping with State
All functional languages have some version of map, which is used to apply a function f : a -> b
to a list of elements of type a
to produce a list of elements of type b
— where of course it could be that a == b
:
List.map : (a -> b) -> List a -> List b
However, when we need to generate a random number in order to process each element of the list, we need something a bit different, something like the below, where s
is the state that needs to be threaded through the computation:
mapWithState : (s -> a -> (s, a)) -> (s, List a) -> (s, List a)
Thus, to move one organism randomly, we use a function like this:
move : Seed -> Organism -> (Seed, Organism)
Given a seed and an organism, move
returns a moved organism and a new seed. Combined with mapWithState
, move
yields a function that will apply random moves to an entire list of organisms:
moveOrganisms : (Seed, List Organism) -> (Seed, List Organism)
moveOrganisms (s, list) =
mapWithState move (s, list)
Here once again we see the power of an expressive type system.
Coda: some nitty-gritty code
For those who are interested, here is the implementation of mapWithState
:
mapWithState : (s -> a -> (s, a)) -> (s, List a) -> (s, List a)
mapWithState f (state, list) =
let
folder : a -> (s, List a) -> (s, List a)
folder item (state_, list_) =
let
(newState_, item_) = f state_ item
in
(newState_, item_ :: list_)
in
List.foldl folder (state, []) list
Just a few lines of Elm code, but lines which do the exactly the job in question.