Making an Ecosystem Simulation in Haskell (Part 1)

For this week’s post, I decided to start coding up a simulation of a small “world” and its ecosystem. The world is 20 by 20 units in dimension and is populated by 3 different types of creatures: Rabbits, Foxes, and Wolves. The creatures will interact with the world, moving around it, hunting their prey, and reproducing. Each creature will have a brain which will drive its behavior based on a finite state machine (FSM).

My goal with this little project is to learn three parts of Haskell which I haven’t had time to explore yet; Haskell’s tool stack, pseudo-random number generation, and the State monad. In this post I’ll discuss the Haskell Stack and System.Random. In my next post about this project, I’ll discuss how to use the State monad to make a FSM.

Using the Haskell Stack

The Haskell Stack is a set of tools which ship with GHC which allow you to create a virtual environment to build your Haskell project in. If you use Stack, your project is isolated from all other projects on your machine and you can have separate versions for GHC and each of the libraries you use.

Setup

My project is called “fsm”. In order to set up the stack for my project I used the following command:

stack new fsm

The command creates a directory called fsm for the project. Next, you need to go into the fsm directory and call stack setup to initialize the project:

cd fsm
stack setup

Stack setup may take some time because it may have to download a recent version of GHC and other libraries.

The directory structure of your project should look something like this after stack setup:

.
├── .gitignore
├── LICENSE
├── Setup.hs
├── app
│   └── Main.hs
├── fsm.cabal
├── package.yaml
├── src
│   └── Lib.hs
├── stack.yaml
└── test
    └── Spec.hs

.gitignore is used for git integration. You can simply call git init and git add all of the files in this directory to manage your project with git.

LICENSE is the project license file.

Setup.hs is used by the cabal build system.

app/Main.hs is the main source file for the project executable.

fsm.cabal is used to store the versions of GHC and libraries for the current build.

package.yaml contains the versions for various libraries required for building the project.

src/Lib.hs is the main library file for the project. Other libraries can be added to src.

stack.yaml contains the package resolver version and the various package dependencies defined by the user. You can call stack init to create a stack.yaml file if it doesn’t exist.

test/Spec.hs is the source file for the testing framework for the project.

Adding dependencies

I needed to add three libraries to my project: Data.Matrix, System.Random, and System.Random.Shuffle.

In order to add these, you need to edit packages.yaml to add the package dependencies under the dependencies heading:

dependencies:
- base >= 4.7 && < 5
- matrix
- mtl >= 2.2.2
- random >= 1.1
- random-shuffle >= 0.0.4

You can find the name and version of a package on Hackage by looking in the top left corner of the page:

package_name

By default, stack searches the LTS resolver for the package. If your package isn’t in the LTS, you can add it into the extra-deps field in stack.yaml:

extra-deps:
- acme-missiles-0.3 # not in the LTS

You can use stack build to run GHC to build the project and stack exec to run the project executable:

stack build
stack exec fsm-exe

The executable initially just prints out a message and quits.

You can also run an interpreter with your project loaded using the following command:

stack ghci

Finally, you can install your package executable using the following command:

stack install fsm-exe

Making a random world

After initializing the project I changed the primary function in the src/Lib.hs module to runSimulation:

module Lib
    ( runSimulation
    ) where

Then I added some imports which are necessary for the simulation:

import System.IO
import Data.List
import Data.Matrix
import Control.Monad.State
import System.Random
import System.Random.Shuffle

Next, I defined some types of Creatures and made them instances of the Show typeclass:

data Creature = Empty | Rabbit | Fox | Wolf deriving (Eq, Ord)

instance Show Creature where
    show Empty = " "
    show Rabbit = "R"
    show Fox = "F"
    show Wolf = "W"

The simulated “world”, is a rectangular region broken up into a grid of cells which each contain a Creature type, or Empty if the cell is empty. This grid is going to be represented by a matrix of Creatures. I created a world grid using the Data.Matrix.matrix function like so:

initGrid :: Int -> Int -> Matrix Creature
initGrid sizeI sizeJ = matrix sizeI sizeJ (\(i, j) -> Empty)

The matrix function takes a size in both dimensions and a generator function to make a matrix. In my case, the generator function just creates an Empty creature type in every cell.

In order to add a creature to the world, I created a gridInsert function:

gridInsert :: Creature -> Int -> Int -> Matrix Creature -> Maybe (Matrix Creature)
gridInsert creature i j grid = safeSet creature (i, j) grid

gridInsert takes a Creature, some coordinates, and the grid and calls safeSet on the matrix to set the value of the cell to the creature. gridInsert evaluates to a Maybe Matrix because the coordinates may be outside of the grid, in which case, safeSet evaluates to Nothing.

printGrid takes a Maybe Matrix and prints it to the command line:

printGrid :: Maybe (Matrix Creature) -> IO ()
printGrid Nothing = return ()
printGrid (Just grid) = putStrLn $ prettyMatrix grid

Data.Matrix.prettyMatrix converts the matrix into a string so it can be printed to the command line with putStrLn.

Let’s skip ahead to the runSimulation function:

runSimulation :: IO ()
runSimulation = let width = 20
                    height = 20
                    initialGrid = initGrid width height
                    generator = mkStdGen 126590563
                    (initialCount, newGenerator) = randomR (10 :: Int, floor ((fromIntegral (width * height)) * 0.1)) generator
                    initialCoordinates = take initialCount (shuffle' ((,) <$> [1..width] <*> [1..height]) (width * height) newGenerator)
                    initialPopulation = unfoldr generatePopulation (initialCoordinates, newGenerator)
                in putStrLn ("Population simulation with " ++ (show initialCount) ++ " creatures.\n") >>
                   printGrid (populateGrid initialPopulation (Just initialGrid))

runSimulation creates an initialGrid with the initGrid function. It then creates a standard random number generator with the mkStdGen function and a random seed value.

I needed an initialCount to determine the number of animals which the world will start with:

(initialCount, newGenerator) = randomR (10 :: Int, floor ((fromIntegral (width * height)) * 0.1)) generator

I used randomR, which generates a random number in a range using the random number generator. The range I chose is between 10 cells of animals and up to 10% of the cells in the world grid filled with animals.

I then generated a list of coordinates for the initial population using the following line:

initialCoordinates = take initialCount (shuffle' ((,) <$> [1..width] <*> [1..height]) (width * height) newGenerator)

Let’s look at this piece-by-piece starting with the Applicative list sub-expression:

(,) <$> [1..width] <*> [1..height]

This takes advantage of the fact that lists in Haskell are members of the Applicative type class, which I described in Making a Text Adventure in Haskell (Part 2). The result of this sub-expression is a list of all combinations of elements in [1..width] joined with all elements of [1..height] joined by the tuple operator (,). The tuple operator creates a tuple out of its parameters. The result of this computation is a list of all unique coordinate tuples in the grid.

shuffle’ takes a list, the size of the list, and a random number generator. It evaluates to a randomly shuffled version of the list. So, the following line evaluates to a list of all grid coordinates shuffled randomly:

shuffle' ((,) <$> [1..width] <*> [1..height]) (width * height) newGenerator

In order to populate the world, I take the first initialCount coordinates from the shuffled list to determine where the initial animals will be placed (initialCoordinates):

initialCoordinates = take initialCount (shuffle' ((,) <$> [1..width] <*> [1..height]) (width * height) newGenerator)

The next thing which happens in runSimulation is that the initialPopulation is created:

initialPopulation = unfoldr generatePopulation (initialCoordinates, newGenerator)

This expression calls unfoldr, which is described in my post Working with Lists, and generatePopulation to create the initial population of the grid.

Let’s see what generatePopulation does:

generatePopulation :: (RandomGen g) => ([(Int, Int)], g) -> Maybe ((Creature, Int, Int), ([(Int, Int)], g))
generatePopulation ([], generator) = Nothing
generatePopulation (((i, j) : coords), generator)
    | creatureIndex == 0 = Just ((Rabbit, i, j), (coords, generator1))
    | creatureIndex == 1 = Just ((Fox, i, j), (coords, generator1))
    | creatureIndex == 2 = Just ((Wolf, i, j), (coords, generator1))
      where (creatureIndex, generator1) = randomR (0 :: Int, 2 :: Int) generator

The function takes a tuple with a list of coordinate tuples and a generator, and it evaluates to a tuple with the creature and its coordinates, as well as an updated state for unfoldr. The function chooses which Creature will be in the cell by generating a random creatureIndex between 0 and 2 using the randomR function. If creatureIndex is 0, it creates a Rabbit at (i, j), if creatureIndex is 1, it creates a Fox, and if creatureIndex is 2, it creates a Wolf. Since this is called by unfoldr, initialPopulation contains a randomized list of tuples with Creatures and their unique coordinates.

The last thing which happens in runSimulation is that the initialGrid is populated with the initialPopulation by calling populateGrid and the grid is printed to the console:

printGrid (populateGrid initialPopulation (Just initialGrid))

populateGrid is defined as follows:

populateGrid :: [(Creature, Int, Int)] -> Maybe (Matrix Creature) -> Maybe (Matrix Creature)
populateGrid [] Nothing = Nothing
populateGrid [] (Just grid) = Just grid
populateGrid ((creature, i, j) : creatures) (Just grid) = populateGrid creatures (gridInsert creature i j grid)
populateGrid _ Nothing = Nothing

The function simply inserts all of the creatures provided in the initialPopulation list into the grid recursively by calling gridInsert.

Here’s what the world looks like initially when printGrid is called:

(       R           W                   F )
(                                   R     )
(                                         )
(                                     W   )
(         W                       R       )
(                         F               )
(   R               R       F   F R       )
(             F             F             )
(                                     W   )
(                             W           )
(         W                               )
(                       W                 )
(           W R       W                   )
( W                           W           )
(                       W                 )
(                                         )
(         F             W                 )
(                     W                   )
(                                         )
(                                     F   )

As you can see, the world is represented by a matrix of creatures, with about 10% of the space occupied by creatures.

Next time I’ll discuss how creatures can move around the grid and interact with other creatures, how they can make decisions using a FSM, and how their actions at each point in time can be simulated using the State monad.

Continue reading Making an Ecosystem Simulation in Haskell (Part 2).

The code for this simulation is available at: https://github.com/WhatTheFunctional/EcosystemSimulation

Resources:

The Haskell Stack

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s