Written by Pete Corey on Feb 20, 2017.

In a recent article, we wrote an Elixir application to play Conway’s Game of Life with Elixir processes. While this was an excellent exercise in “thinking with processes”, the final result wasn’t visually impressive.

Usually when you implement the Game of Life, you expect some kind of graphical interface to view the results of the simulation.

Let’s fix that shortcoming by building out a Phoenix based front-end for our Game of Life application and render our living processes to the screen using an HTML5 canvas.

Creating an Umbrella Project

Our game of life simulation already exists as a server-side Elixir application. We somehow need to painlessly incorporate Phoenix into our application stack so we can build out our web-based front-end.

Thankfully, Elixir umbrella projects let us do exactly this.

Using an umbrella project, we’ll be able to run our life application and a Phoenix server simultaneously in a single Elixir instance. Not only that, but these two applications will be able to seamlessly reference and communicate with each other.

To turn our Life project into an umbrella project, we’ll create a new folder in the root of our project called apps/life/, and move everything from our Life project into that folder.

Next, we’ll recreate the mix.exs file and the config folder and corresponding files needed by our umbrella application in our project root. If everything has gone well, we’ll still be able to run our tests from our project root:


mix test

And we can still run our life application through the project root:


iex -S mix

Now we can go into our new apps folder and create a new Phoenix application:


cd apps/
mix phoenix.new interface --no-ecto

Notice that we’re forgoing Ecto here. If you remember from last time, our Game of Life simulation lives entirely in memory, so we won’t need a persistence layer.

Once we’ve created our Phoenix application, our umbrella project’s folder structure should look something like this:


.
├── README.md
├── apps
│   ├── interface
│   │   └── ...
│   └── life
│       └── ...
├── config
│   └── config.exs
└── mix.exs

Notice that interface and life are complete, stand-alone Elixir applications. By organizing them within an umbrella project, we can coordinate and run them all within a single Elixir environment.

To make sure that everything is working correctly, let’s start our project with an interactive shell, and fire up the Erlang observer:


iex -S mix phoenix.server
:observer.start

If we navigate to http://localhost:4000/, we should see our Phoenix framework hello world page. Not only that, but the observer shows us that in addition to our Phoenix application, our life application is alive and kicking on the server as well.

Channeling Life

Now that our Phoenix server is set up, we can get to the interesting bits of the project.

If you remember from last time, every time we call Universe.tick, our Game of Life simulation progresses to the next generation. We’ll be using Phoenix Channels to receive “tick” requests from the client and to broadcast cell information to all interested users.

Let’s start the process of wiring up our socket communication by registering a "life" channel in our UserSocket module:


channel "life", Interface.LifeChannel

Within our Interface.LifeChannel module, we’ll define a join handler:


def join("life", _, socket) do
  ...
end

In our join handler, we’ll do several things. First, we’ll “restart” our simulation by clearing out any currently living cells:


Cell.Supervisor.children
|> Enum.map(&Cell.reap/1)

Next, we’ll spawn our initial cells. In this case, let’s spawn a diehard methuselah at the coordinates {20, 20}:


  Pattern.diehard(20, 20)
  |> Enum.map(&Cell.sow/1)

Lastly, we’ll return a list positions of all living cells in our system:


  {:ok, %{positions: Cell.Supervisor.positions}, socket}

Cell.Supervisor.positions is a helper function written specifically for our interface. It returns the positions of all living cells in a list of structs:


def positions do
  children()
  |> Enum.map(&Cell.position/1)
  |> Enum.map(fn {x, y} -> %{x: x, y: y} end)
end

Now that our join handler is finished up, we need to write our “tick” handler:


def handle_in("tick", _, socket) do
  ...
end

In our tick handler, we’ll call Universe.tick to run our simulation through to the next generation:


Universe.tick

Next, we’ll broadcast the positions of all living cells over our socket:


broadcast!(socket, "tick", %{positions: Cell.Supervisor.positions})

And finally, we return from our tick handler with no reply:


{:noreply, socket}

Rendering Life

Now that our "life" channel is wired up to our Game of Life simulator, we can build the front-end pieces of our interface.

The first thing we’ll do is strip down our index.html.eex template and replace the markup in our app.html.eex template with a simple canvas:


<canvas id="canvas"></canvas>

Next, we’ll start working on our app.js file.

We’ll need to set up our canvas context and prepare it for rendering. We want our canvas to fill the entire browser window, so we’ll do some hacking with backingStorePixelRatio and devicePixelRatio to set the scale, height and width of our canvas equal to window.innerWidth and window.innerHeight respectively. Check out the source for specifics.

Now we’ll need a render function. Our render function will be called with an array of cell position objects. Its job is to clear the screen of the last render and draw a square at every cell’s given {x, y} position:


function render(positions) {
    context.clearRect(0, 0, canvas.width, canvas.height);
    positions.forEach(({x, y}) => {
        context.fillRect(x * scale, y * scale, scale, scale);
    });
}

Now that our canvas is set up and ready to render, we need to open a channel to our Phoenix server.

We’ll start by establishing a socket connection:


let socket = new Socket("/socket");
socket.connect();

Next, we’ll set up to our "life" channel:


let channel = socket.channel("life", {});

When we join the channel, we’ll wait for a successful response. This response will contain the initial set of living cells from the server. We’ll pass those cells’ positions into our render function:


channel.join()
  .receive("ok", cells => render(cells.positions));

We’ll also periodically request ticks from the server:


setTimeout(function tick() {
  channel.push("tick");
  setTimeout(tick, 100);
}, 100);

Every tick will result in a "tick" event being broadcast down to our client. We should set up a handler for this event:


channel.on("tick", cells => {
  render(cells.positions);
});

Once again, we simple pass the cells’ positions into our render function.

That’s it! After loading up our Phoenix application, we should see life unfold before our eyes!

Phoenix as an Afterthought

While Conway’s Game of Life is interesting, and “thinking in processes” is an important concept to grasp, there’s a bigger point here that I want to drive home.

In our first article, we implemented our Game of Life simulation as a standalone, vanilla Elixir application. It wasn’t until later that we decided to bring the Phoenix framework into the picture.

Using Phoenix was an afterthought, not a driving force, in the creation of our application.

Should we choose to, we could easily swap out Phoenix with another front-end framework with no fears about effecting the core domain of the project.


Throughout my career as a software developer I’ve worked on many software projects. Without fail, the most painful of these projects have been the those tightly coupled to their surrounding frameworks or libraries.

Loosely coupled applications, or applications with a clear distinction between what is core application code and what is everything else, are easier to understand, test, and maintain.

Some languages and frameworks lend themselves more easily to this kind of decoupling. Thankfully, Elixir’s process model, the concept of Elixir “applications”, and umbrella projects make this kind of decoupling a walk in the park.

Taken this as a reminder to build your framework around your application. Don’t build your application around your framework.