Video transcript & code
Elixir is a new programming language from longtime Rubyist and Rails core team member Jose valim. I have Elixir on the brain right now, because I recently started learning the language. Today I'd like to show you my first Elixir program. I know this is Ruby Tapas, but I hope that you'll indulge me for just this one episode. And there is a Ruby connection: not only is Elixir by a prominent Ruby programmer, but its syntax has a strong Ruby influence.
The program I'm going to show you is my attempt at implementing Conway's Game of Life. Since this is a Tapas episode and time is short, I'm not going to explain the rules of the game. If you're not familiar with it, pause this and look up the Wikipedia article on Conway's Game of Life to get an idea of how it works.
This is also not a tutorial intro to Elixir. All I'll tell you before hand is that it's a functional programming language like Lisp, Haskell, or ML built on top of the Erlang virtual machine but with a Ruby-style syntax. I won't have time to explain every feature as I use it; if anything you see here piques your interest, check out the links to further resources I've put in the show notes.
OK, let's get started. I'm actually going to start at the end. Here's a starting game of life board that I'd like my program to evolve. It's in the form of a triple-quoted string. The 'o'-s represent live cells and the dots represent dead cells. I'd like to be able to say
Life.run with the board as an argument and see the evolution animated in my terminal.
board = """ .o...... ..o..... ooo..... ........ ........ ........ ........ ........ """ Life.run(board)
To make this work I start by defining the
Life module. Inside I define the
run function, or at least the version of it which will expect a binary argument (that's Elixir-speak for a String). I define this version to pipe the board through a
parse_board function before piping it back into the
defmodule Life do def run(board) when is_binary(board) do board |> parse_board |> run end # ... end
Now I define a second version of
run with no guard clause. This one will expect an already parsed board, the format of which I'll explain in a moment. It starts out by writing a magical incantation to standard out which terminals recognize as a request to clear the screen. Then it prints out the current state of the board, sleeps for a second (using an Erlang standard library called
:timer), and gets the next evolution of the board.
This second definition of the
run function is strange enough when coming from a Ruby background, but things are about to get weirder. As in many functional programming languages, recursion takes the place of looping in Elixir. So rather than putting this whole function body inside a loop to keep evolving the game board, I instead put another call to the
run function, giving it the new state of the board, as the last statement in the function.
def run(board) do IO.write("\e[H\e[2J") Life.print_board board :timer.sleep 1000 board = next_board(board) run(board) end
In order to parse a string representation of the board, I use a simple regex to scan the input for lines of dots and 'o'-s. Then I take that list of lines and convert each line into a list of characters using
def parse_board(board) do rows = Regex.scan(%r/^[.o]+$/m, board) Enum.map rows, String.graphemes(&1) end
To print out the state of a board I simply iterate over each row of characters in the board and print out the row.
def print_board(board) do Enum.each board, fn row -> IO.puts(Enum.join(row)) end end
Now to calculate the next evolution of a game board. Here I map over rows and then columns of the board. For each cell at a given x/y position I return the next state of that cell.
def next_board(board) do Enum.map board, fn row, y -> Enum.map row, fn _, x -> next_state(board, x, y) end end end
next_state function takes a board and x/y coordinates. First it finds the cell at those coordinates, which will be either a dot or an 'o'. Then it calculates the number of live neighbors that cell has. Finally it delegates to a two-argument form of the
next_state function, passing the cell's current state and its live neighbor count.
def next_state(board, x, y) do cell = cell_at(board, x, y) live_count = live_neighbors(board, x, y) next_state(cell, live_count) end
Here is where the pattern-matching nature of Elixir gets really fun. I define a series of definitions of the
next_state function based on the rules of the game.
The next state of a live cell with 2 or three live neighbors is to stay alive.
def next_state("o", live_count) when live_count in 2..3, do: "o"
The next state of a live cell with any other number of neighbors is to die.
def next_state("o", _), do: "."
The next state of a dead cell with exactly 3 live neighbors is to become a live cell.
def next_state(".", live_count) when live_count === 3, do: "o"
And the next state of any other dead cell is to stay dead.
def next_state(".", _), do: "."
Earlier I used a
cell_at function to find the cell at given coordinates. I'll define two versions of this function. The first one will be for the case when either X or Y are negative numbers. This is off the map, so such coordinates will always be considered dead cells.
def cell_at(_, x, y) when (x < 0 or y < 0), do: "."
With that out of the way, in the next definition of
cell_at I only have to worry about positive integer coordinates. I use
Enum.at to grab the specified row. If the row isn't found—in other words, if it's off the board in the positive direction—I return a dead cell. Otherwise I look up the given X coordinate in the row, again defaulting to a dead cell if that index doesn't exist.
def cell_at(board, x, y) do case Enum.at(board, y) do nil -> "." row -> Enum.at(row, x, ".") end end
To calculate live neighbors, I use
Enum.count to count all neighbors of the given cell which are an 'o' character.
def live_neighbors(board, x, y) do Enum.count(neighbors(board, x, y), &1 === "o") end
To fetch the neighbors for that calculation, I find the coordinates for each neighbor of the given cell and use
cell_at to map from the list of coordinates to a list of cell contents.
def neighbors(board, x, y) do coords = neighbor_coords(x, y) Enum.map coords, fn [x,y] -> cell_at(board, x,y) end end
To figure out the coordinates of neighbors, I just take the given X/Y coordinates and return a list of X/Y pairs at the appropriate relative offsets. In order to visualize this logic I lay these pairs out in the shape of a square surrounding a blank center. The blank center represents the cell whose neighbors are being calculated.
def neighbor_coords(x, y) do [[x-1, y-1], [x, y-1], [x+1, y-1], [x-1, y], [x+1, y], [x-1, y+1], [x, y+1], [x+1, y+1]] end
And that's it. Now to try it out. I feed my test board, which contains a "Glider" shape, into the
Life.run function. I run the program… and then watch as the "glider" moves across the playing field, according to the rules of Conway's Game of Life. Success!
Today I've shown just a fraction of the Elixir language, and since this is my first program there are probably a lot of improvements that could be made. But I hope this has given you a taste of what the language is like. If you're intrigued at all by what you've seen today, I've put a list of recommended Elixir resources in the show notes which you can check out for more information.