Introduction
Rust is an uprising programming language that is loved by the community. The main reason for the upsurge of Rust in recent times is that the language is built for performance and concurrency with safety in mind. At present, the popular use case of Rust is WebAssembly.
So, I wanted to get a gist of the language. The best way(according to me) to learn is to recreate something in the new programming language, which you have already done in a familiar programming language. Hence, I went on to program a simple version of Conway’s Game of Life.
Conway’s Game of Life
Game of Life is a cellular automaton proposed by the mathematician John Horton Conway. The universe of Game of Life is an infinitely large plane with square cells, which are either in two states alive, or dead, bound by certain rules.
- A live cell with less than two live neighbors dies.
- A live cell with more than three live neighbors dies.
- A dead cell with exactly three neighbors becomes alive.
- A live cell with two or three neighbors continues to live.
For more information on Game of Life visit this link. To visualize and play with Game of Life you can visit my web application.
Getting Rust
The first step is to install Rust, which you can do by visiting the official Rust site.
You can create your Rust project by entering the following command in your terminal.
cargo new game_of_life
Get into the directory using the command
cd game_of_life
Inside this main directory, you will see a main.rs
file inside the src folder. This main.rs
is the main file that will be built and executed.
Rusting Game of Life
In this example, we going to recreate a blinker, an interesting pattern in Game of Life.
In our example, the universe of Game of Life is a two-dimensional vector in Rust, which we will call it a grid, with 1 representing a live cell and 0 representing a dead cell.
The code is given below.
// function to compute the next generation
fn gol(grid: &Vec<Vec<i8>>) -> Vec<Vec<i8>> {
// get the number of rows
let n = grid.len();
// get the number of columns
let m = grid[0].len();
// create an empty grid to compute the future generation
let mut future: Vec<Vec<i8>> = vec![vec![0; n]; m];
// iterate through each and every cell
for i in 0..n {
for j in 0..m {
// the current state of the cell (alive / dead)
let cell_state = grid[i][j];
// variable to track the number of alive neighbors
let mut live_neighbors = 0;
// iterate through every neighbors including the current cell
for x in -1i8..=1 {
for y in -1i8..=1 {
// position of one of the neighbors (new_x, new_y)
let new_x = (i as i8) + x;
let new_y = (j as i8) + y;
// make sure the position is within the bounds of the grid
if new_x > 0 && new_y > 0 && new_x < n as i8 && new_y < m as i8 {
live_neighbors += grid[new_x as usize][new_y as usize];
}
}
}
// substract the state of the current cell to get the number of alive neighbors
live_neighbors -= cell_state;
// applying the rules of game of life to get the future generation
if cell_state == 1 && live_neighbors < 2 {
future[i][j] = 0;
} else if cell_state == 1 && live_neighbors > 3 {
future[i][j] = 0;
} else if cell_state == 0 && live_neighbors == 3 {
future[i][j] = 1;
} else {
future[i][j] = cell_state;
}
}
}
// return the future generation
future
}
// main function
fn main() {
// set the number of rows and columns of the grid
let (rows, cols) = (5, 5);
// create the grid
let mut grid: Vec<Vec<i8>> = vec![vec![0; cols]; rows];
// set the initial state of the grid (blinker)
grid[1][2] = 1;
grid[2][2] = 1;
grid[3][2] = 1;
// print the initial state of the grid;
println!("Initial grid:");
grid.iter().for_each(|i| {
println!("{:?}", i);
});
println!("");
// Number of generations
const ITR: u8 = 5;
// compute and print the next generation
for i in 0..ITR {
grid = gol(&grid);
println!("Generation {}:", i+1);
grid.iter().for_each(|i| {
println!("{:?}", i);
});
println!("");
}
}
Then run the command given below to build and execute the file.
cargo run
The output of the code is given below. As you can see from the output, the oscillating structure blinker in the middle of the grid.
Initial grid:
[0, 0, 0, 0, 0]
[0, 0, 1, 0, 0]
[0, 0, 1, 0, 0]
[0, 0, 1, 0, 0]
[0, 0, 0, 0, 0]
Generation 1:
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
[0, 1, 1, 1, 0]
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
Generation 2:
[0, 0, 0, 0, 0]
[0, 0, 1, 0, 0]
[0, 0, 1, 0, 0]
[0, 0, 1, 0, 0]
[0, 0, 0, 0, 0]
Generation 3:
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
[0, 1, 1, 1, 0]
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
Generation 4:
[0, 0, 0, 0, 0]
[0, 0, 1, 0, 0]
[0, 0, 1, 0, 0]
[0, 0, 1, 0, 0]
[0, 0, 0, 0, 0]
Generation 5:
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
[0, 1, 1, 1, 0]
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
Observations
At a first glance, the Rust code looks similar to any C styled language, but there are some interesting, distinct points to note.
At line 33 shown below
live_neighbours += grid[new_x as usize][new_y as usize]
We need to cast the type of the variables new_x
and new_y
to usize
. Suppose if the variable used to index an element of a vector is an integer, then we might get into overflow conditions like using negative numbers. The Rust compiler gives us an error if we index a vector with any other types except unsigned types.
Suppose we want to access the variables of unsigned type with signed type in calculations, then we need to cast the variables to one common type. Like at lines 28 and 29 shown below
let new_x = (i as i8) + x;
let new_y = (j as i8) + y;
We need to cast the type of the variables i
and j
to i8
because the variables new_x
, new_y
, x
, and y
are of type i8
whereas i
and j
are of type usize
because the variables n
and m
are of type usize. Also, if our code leads to some negative values of the variable of type usize, the compiler will panic, stopping the execution.
If we want our variables to be mutable, we must make sure to tell that to the compiler by using the keyword mut. As shown in lines 11 and 65.
let mut future: Vec<Vec<i8>> = vec![vec![0; n]; m];
let mut grid: Vec<Vec<i8>> = vec![vec![0; cols]; rows];
Conclusion
I had a fun time implementing Game of Life in Rust. The development time of the program took a bit of time because the Rust compiler kept on complaining, but the execution was smooth. Whereas for other languages, we write the program, and we might run into errors at runtime, then come again to debug the code.
The Rust compiler also gives us suggestions for the errors and warnings which beat compilers of other programming languages. Often I was in doubt while checking the output because the program(in Rust) gave the output that is desired in the very first trial. This shows the power of the Rust compiler; once the code compiles then there is a high chance that the program might work.
The observations we saw coincides with the safety that Rust promises. This is just the tip of the iceberg, Rust also provides a lot of other features like borrowers, concurrency, and so on. The future of Rust while looking at the current state is bright, but will it stand the test of time; only time will tell the tale.
Top comments (0)