Hello Elixir World

in elixir

After spending too long trying to love lisp, I think I'm finally starting to understand the beauty in functional languages - and elixir is to blame. It's very different than most languages I know, but in an almost magical way.

Here's my very first elixir program, which is a terminal based tic-tac-toe game. It's probably not that great elixir example regarding best practices, but I hope it'll show some of the magic I find in this language.

A Finished Game

The tic-tac-toe game should read a move from each player and play that move in a loop, ending when one of the players win. Here's a game played from start to finish in my terminal:

localhost:elxr ynonperek$ elixir game.elxrs 
. . . 
. . . 
. . . 
Next Move: 0,0
---
X . . 
. . . 
. . . 
Next Move: 0,1
---
X O . 
. . . 
. . . 
Next Move: 1,0
---
X O . 
X . . 
. . . 
Next Move: 1,1
---
X O . 
X O . 
. . . 
Next Move: 2,0
---
X O . 
X O . 
X . . 
Game Over, X Won
localhost:elxr ynonperek$ 

In an object oriented langauge you may consider the game to be an object providing a play method. The state of the game would be saved as a member variable. For example in Ruby we might write:

g = Game.new()
g.play(1, 1)
g.print_board()

Alas elixir being a functional language it does not allow a mutable data structure to hold the data. That's why we need to replace g.play to return a value. In Elixir we'll write:

g = Game.init
g = Game.play(g, 1, 1)
Game.print_board(g)

The key concept here is immutability. Change happens when variables refer to new data structures.

Let's Play

The game data structure is just a hash map with current player and board keys:

  def init() do
    %{
      board: { ".", ".", ".", ".", ".", "." , ".", ".", "."  },
      player: "X"
    }
  end

The percent sign represents hash, so if you know perl you should feel at home.

Now my play function is the first thing I really liked in elixir:

  def play(g, idx) do
    cond do
      elem(g.board, idx) == "." ->
        %{
          g |
          player: next(g[:player]),
          board: put_elem(g.board, idx, g[:player])
        }

      true ->
        IO.puts "Sorry, that cell's taken"
        g
    end
  end

With all data being immutable, play can't change the game data structure. Instead it returns a new hash with a new player and a new board.

Both new player and new board are of course related to the old ones. A board after a move has the current player set in the played square. A player after a move is set to the next player.

The keyword cond is elixir's way to split the function to multiple cases (the above example will play only if the square is available).

Who's Next

Surely we can use a ternary to find the next player, but that wouldn't be fun. With elixir we can cycle over a collection and get the next item:

  def next(player) do
    Stream.cycle(["X", "O"]) |>
    Stream.drop_while(&(&1 == player)) |>
    Enum.at(0) 
  end

Cycle returns a stream that looks something like ["x", "O", "X", "O", ...]. The pipe operator at the end of the line will send that steram to the next function, i.e. drop_while. Its pipe operator will send the stream to the next function in line which returns the first element.

And The Winner Is...

When getting the winner I found elixir supports overloaded function, and that saved me from inventing new function names:

  def win?(g) do
    win?(g, "X") || win?(g, "O")
  end

  def win?(g, player) do    
    win?(g, player, 0, 1, 2) ||
    win?(g, player, 3, 4, 5) ||
    win?(g, player, 6, 7, 8) ||
    win?(g, player, 0, 3, 6) ||
    win?(g, player, 1, 4, 7) ||
    win?(g, player, 2, 5, 8) ||
    win?(g, player, 0, 4, 8) ||
    win?(g, player, 2, 4, 6)
  end

  def win?(g, player, i, j, k) do
    elem(g.board, i) == elem(g.board, j) &&
    elem(g.board, j) == elem(g.board, k) &&
    elem(g.board, i) == player &&
    player
  end

Note how no state is shared between the functions, as all data is immutable. All shared data is explicitly sent to the functions that need it.

The function elem takes a tuple and an index and returns the item at the specified index. And question mark is a valid character in a function name (so rubysts can feel at home).

Recursive Loops

The last implementation detail loop handling. Elixir doesn't have the classic for/while loops, and instead we use recursive function calls. Using tail call optimisation it shouldn't hog your stack.

  def gameloop(g) do
    Game.print_board(g)
    cond do
      winner = win?(g) ->
        IO.puts("Game Over, #{winner} Won")

      true ->
        [row, col] = read_move_from_user()
        g = Game.play(g, row, col)
        IO.puts("---")
        gameloop(g)
    end
  end

Full Source Code

Writing the game in elixir was very fun, and I'm sure I'll continue to play with the language, aiming to learn phoenix, which is elixir's web framework.

In the meantime here's the full source code for the described game. Hope you'll find it useful and educational:

defmodule Game do

  def init() do
    %{
      board: { ".", ".", ".", ".", ".", "." , ".", ".", "."  },
      player: "X"
    }
  end

  def play(g, row, col) do
    play(g, row * 3 + col)
  end

  def play(g, idx) do
    cond do
      elem(g.board, idx) == "." ->
        %{
          g |
          player: next(g[:player]),
          board: put_elem(g.board, idx, g[:player])
        }

      true ->
        IO.puts "Sorry, that cell's taken"
        g
    end
  end

  def win?(g) do
    win?(g, "X") || win?(g, "O")
  end

  def win?(g, player) do    
    win?(g, player, 0, 1, 2) ||
    win?(g, player, 3, 4, 5) ||
    win?(g, player, 6, 7, 8) ||
    win?(g, player, 0, 3, 6) ||
    win?(g, player, 1, 4, 7) ||
    win?(g, player, 2, 5, 8) ||
    win?(g, player, 0, 4, 8) ||
    win?(g, player, 2, 4, 6)
  end

  def win?(g, player, i, j, k) do
    elem(g.board, i) == elem(g.board, j) &&
      elem(g.board, j) == elem(g.board, k) &&
        elem(g.board, i) == player &&
          player
  end

  def next(player) do
    Stream.cycle(["X", "O"]) |>
    Stream.drop_while(&(&1 == player)) |>
    Enum.at(0) 
  end

  def read_move_from_user do
    Enum.map(
      String.split(
        String.trim(
          IO.gets "Next Move: "
        ),
        ","),
        &String.to_integer(&1)
    )
  end

  def gameloop(g) do
    Game.print_board(g)
    cond do
      winner = win?(g) ->
        IO.puts("Game Over, #{winner} Won")

      true ->
        [row, col] = read_move_from_user()
        g = Game.play(g, row, col)
        IO.puts("---")
        gameloop(g)
    end
  end

  def print_board(g) do
    board = Enum.chunk_every(Tuple.to_list(g[:board]), 3)

    Enum.map board, fn(row) ->
      Enum.map row, fn(cell) ->
        IO.write(cell <> " ")
      end
      IO.puts ""
    end
  end  
end

g = Game.init
Game.gameloop(g)

Edit: Improving next and read code

I posted this code on reddit and got 2 great suggestions. The first regarding next function, which was indeed overly clever in my original implementation. Here's the better one using elixir's pattern matching:

def next("X"), do: "O"
def next("O"), do: "X"

The second is about read_move_from_user which could benefit from elixir's pipe operator. Here's the modified implementation:

  def read_move_from_user do
    IO.gets("Next Move: ") |>
    String.trim |>
    String.split(",") |>
    Enum.map(&String.to_integer(&1))
  end

Comments