Elixir for Object-Oriented Developers

@morganlaco

rails conf

elixir

why elixir?

scalability

  • Lightweight threads
  • ~10^5 concurrent processes
  • distribution

fault tolerance

  • Supervisors

functional programming

  • Fast
  • Maintainable

great error messages

performance

case study

before

  • 150 AWS instances
  • Log-jammed responses
  • multiple engineers per app
  • multiple complex caching strategies

after

  • 1/15th servers
  • 10-30ms avg response
    (Hella fast!!!)
  • Largest avg spike 400 ms
  • Largest outlier 800ms
  • ~1 engineer/app
  • no caching

How?

how?

1. Elixir faster at GC

how?

2. Elixir consumes all CPU cores

how?

3. Elixir is a compiled language

how?

4. phoenix makes smart use of the BEAM

how?

5. Database is not slow

Watch brian cardarella's talk!

original
content
warning

the fours awakens

Ruby

class ConnectFour::Game
  def play
    while continue_game
      get_move
      handle_move
    end
    
    display_result
  end
  # ...
end

ConnectFour::Game.new.play

elixir

defmodule ConnectFour do
  def start(_type, _args) do
    state = ConnectFour.initial_state
    do_turn(state)
  end
  
  def do_turn(state) do
    ConnectFour.IO.display_board(state[:board])
    
    res = with \
      {:ok, move} <- ConnectFour.IO.get_move(state[:turn]),
      {:ok, new_board} <- \
        ConnectFour.Board.update_board(state[:board], move, state[:turn]),
    do:
      new_state(state, new_board)
      
    case res do
      # ...
      new_state ->
        if ConnectFour.WinChecker.check(new_state[:board]) do
          ConnectFour.IO.do_win(new_state)
        else  
          do_turn(new_state)
        end
    end
  end

ConnectFour.start

tail recursion

tail recursion

# Ruby
array = [1, 2, 3]
(1..3).each do |i|
  array[i] = array[i] * 2
end
# Elixir
def do_fun_things(x) do
  # Do them thangs
  do_fun_things(y)
  # ^-- Last thing
end

What is it?

tail recursion

how does it work?

def fibonacci(n) do
  cond do
    n == 0
      0
    n == 1
      1
    true ->
      fibonacci(n, 0, 1)
  end
end

def fibonacci(n, a, b) do
  if n <= 0 do
    a
  else
    fibonacci(n - 1, b, a + b)
  end
end
n a b
4 - -
4 0 1
3 1 1
2 1 2
1 2 3
0 3 5

tail recursion

how does it work?

fibonacci(n)

fibonacci(n)

fibonacci(n)

fibonacci(n)

<process>

def fibonacci(n) do
  cond do
    n == 0
      0
    n == 1
      1
    true ->
      fibonacci(n, 0, 1)
  end
end

def fibonacci(n, a, b) do
  if n <= 0 do
    a
  else
    fibonacci(n - 1, b, a + b)
  end
end

tail recursion

how does it work?

fibonacci(n)

fibonacci(n)

fibonacci(n)

fibonacci(n)

<process>

def fibonacci(n) do
  cond do
    n == 0
      0
    n == 1
      1
    true ->
      fibonacci(n, 0, 1)
  end
end

def fibonacci(n, a, b) do
  if n <= 0 do
    a
  else
    fibonacci(n - 1, b, a + b)
  end
end

audience
participation
alert

immutable data

snokes_true_identity = "Plagueis"
# => "Plagueis"
snokes_true_identity = "Leia"
# => What happens?

immutability

snokes_true_identity = "Plagueis"
# => "Plagueis"
snokes_true_identity = "Leia"
# => "Leia"

immutability

what the fuzz?

immutability

before

after

foo
foo
foo = 4
foo = 5

4

5

immutability

before

after

foo
foo
foo = 4
foo = 5

4

5

4

foo

immutability

  • Functional programming
  • thread safe

syntax

pipes!

pipes!

defmodule ConnectFour.IO do
  def get_move(turn) do
    validate_move(
      String.trim(
      IO.gets(:stdio, "[#{translate_player(turn)}] Enter move: "
    )))
  end

  def validate_move(move) do
    cond do
      String.match?(move, ~r/[1-6]/) ->
        {:ok, String.to_integer(move) - 1}    
      String.match?(move, ~r/q(uit)?/) ->
        {:quit}
      true ->
        {:error}
    end
  end
end

pipes!

defmodule ConnectFour.IO do
  def get_move(turn) do
    IO.gets(:stdio, "[#{translate_player(turn)}] Enter move: ")
    |> String.trim
    |> validate_move
  end

  def validate_move(move) do
    cond do
      String.match?(move, ~r/[1-6]/) ->
        {:ok, String.to_integer(move) - 1}    
      String.match?(move, ~r/q(uit)?/) ->
        {:quit}
      true ->
        {:error}
    end
  end
end

pipes!

def insert_at_end(list, item) do
  Enum.reverse(list)
  |> Enum.into(list, [item])
  |> Enum.reverse(list)
end

pipes!

def insert_at_end(list, item) do
  Enum.reverse(list)
  |> Enum.into([item])
  |> Enum.reverse
end

pipes!

defmodule ConnectFour.IO do
  # ...
  def display_board(board) do
    # Bad!
    [h|t] = Listify.listify(board)
    |> Antitranspose.antitranspose

    display_board(h, t)
    IO.puts "      1 2 3 4 5 6"
  end
  # ...
end

pipes!

defmodule ConnectFour.IO do
  # ...
  def display_board(board) do
    # Good!
    [h|t] = prepare_board(board)
    display_board(h, t)
    IO.puts "      1 2 3 4 5 6"
  end
  
  def prepare_board(board) do
    Listify.listify(board)
    |> Antitranspose.antitranspose
  end
  # ...
end

with

case get_move(state[:turn]) do
  {:ok, move} ->
    case ConnectFour.Board.update_board(state[:board],\
      move, state[:turn]) do
      {:ok, new_board} ->
        new_state(state, new_board)
        |> do_turn
      {:error} ->
        do_turn(state) # redundant; can we use `with`?
  {:error} ->
    # do error stuff
    IO.puts("Invalid move")
    do_turn(state)
end

with

res = with \
  {:ok, move} <- get_move(state[:turn]),
  {:ok, new_board} <- \
    ConnectFour.Board.update_board(state[:board], \
    move, state[:turn]),
do:
  new_state(state, new_board)
  |> do_turn
  
case res do
  {:error} ->
    IO.puts "error!"
end
function isHorizontal(grid) {
  for (let x = 0; x < rowsNum; x++) {
    for (let y = 0; y < columnsNum; y++) {
      // Current piece in this row
      let piece = grid[y][x];
      // Reset things if piece is 0
      if (piece === 0) {
        found = 0;
        foundPiece = 0;
        continue;
      }
      if (piece !== foundPiece) {
        found = 1;
        foundPiece = piece;
        continue;
      }
      found++;
      if (found >= 4) {
        return true;
      }
    }
  }  
  return false; // nothing was found in the same row
}
defmodule ConnectFour.WinChecker.Orthogonal do
  def check_vertical(board) do
    check(board, [])
  end
  
  def check([], row) do
    check_row(row)
  end
  
  def check(remaining, []) do
    [next_row | other_rows] = remaining
    check(other_rows, next_row)
  end
  
  def check(remaining, row) do
    if check_row(row) do
      true
    else
      [next_row | other_rows] = remaining
      check(other_rows, next_row)
    end
  end
end
defmodule ConnectFour.WinChecker.Orthogonal do
  def check_row(row) do
    [cell | other_cells] = row
    check_row(cell, other_cells, 0, 0)
  end

  def check_row(cell, remaining, last, matching) do
    new_matching = 
      if cell == last && cell != 0 do
        matching + 1
      else
        1
      end
    if new_matching == 4 do
      true
    else
      [next_cell | others] = remaining
      check_row(next_cell, others, cell, new_matching)  
    end  
  end
end
defmodule ConnectFour.WinChecker.Orthogonal do
  def check_row(cell, [], last, matching) do
    new_matching =
      if cell == last do
        matching + 1
      else
        1
      end
    new_matching == 4
  end
end

# Using accumulators in recursive fns
# http://langintro.com/elixir/article2/

resources

thank you!

Elixir for OO Developers (ODev Edition)

By Morgan Laco

Elixir for OO Developers (ODev Edition)

  • 1,552