In Progress
Unit 1, Lesson 1
In Progress

Elixir

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 run function.

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 String.graphemes.

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

The 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.

Responses