Austin Elixir

Why Elixir?

2015-07-08

Luke Imhoff

luke_imhoff@rapid7.com Kronic.Deth@gmail.com
@limhoff-r7 @KronicDeth
@KronicDeth

Outline

  1. Functional Languages
  2. Erlang
  3. Elixir
  4. Why Elixir and not Erlang?
  5. Examples
  6. Resources

Functional Languages

What is a functional language?

  • Mathematical functions
  • Avoids changing state
  • Immutable data

Advantages of functional languages

  • Lack of shared, mutable state
  • Immutable data

(Perceived) disadvantages

  • Cryptic syntax
  • Harder to reason about immutability
  • Recursion + accumulation harder than iteration and mutation
  • "It's not how I think!"

Erlang

Facts

  • Developed for telecoms
  • Massively scalable
  • Hot code reloading

Design Philosophy

  • Fault tolerant
  • Failures expected and embraced
  • Concurrency model is simple, but powerful

WhatsApp

  • Text messages, images, video, user location, and audio over data plans instead of SMS
  • 2,277,845 simultaneous TCP connections
  • 24 cores
  • 1 Erlang VM
  • $22 billion in cash and (Facebook) stock

Elixir

Modules and Functions


defmodule Math do
  def sum(a, b) do
    a + b
  end

  def square(x) do
    x * x
  end
end

Math.sum(1, 2) # 3
Math.square 3 # 9
                        

Pattern Matching and Guards


defmodule Geometry do
  def area({:rectangle, w, h}) do
    w * h
  end

  def area({:circle, r}) when is_number(r) do
    3.14 * r * r
  end
end

Geometry.area({:rectangle, 2, 3}) #=> 6
Geometry.area({:circle, 3})       #=> 28.25999999999999801048
                        

Recursion


defmodule Recursion do
  def sum_list([head | tail], acc) do
    sum_list(tail, acc + head)
  end

  def sum_list([], acc) do
    acc
  end
end

Recursion.sum_list([1,2,3], 0) #=> 6
                        

Concurrency


iex> pid = spawn_link fn ->
...>   receive do
...>     {:ping, client} -> send client, :pong
...>   end
...> end
#PID<9014.59.0>
iex> send pid, {:ping, self}
{:ping, #PID<0.73.0>}
iex> flush
:pong
:ok
                        

Why Elixir and not Erlang?

Tooling

Mix

Rake + Bundler

  • Create projects
  • Manages dependencies
  • Compiles code and dependencies
  • Runs tests

ExUnit

Unit Testing

  • Adds mix test
  • Defines DSL for defining tests
  • Defines DSL for checking results

ExDoc

  • Documentation is a first class feature of Elixir
  • Documentation examples can be tested with ExUnit's doctest
  • Documentation can be hosts on hexdocs.pm

defmodule Atom do
  @moduledoc """
  Convenience functions for working with atoms.
  See also `Kernel.is_atom/1`.
  """

  @doc """
  Converts an atom to a string.

  Inlined by the compiler.

  ## Examples

      iex> Atom.to_string(:foo)
      "foo"
  """
  @spec to_string(atom) :: String.t
  def to_string(atom) do
    :erlang.atom_to_binary(atom, :utf8)
  end
end
                        

Tool Demo

Erlang Macros vs Elixir Macros

Erlang Elixir
Run when? Preprocess Compile
Code generation String substitution AST manipulation
Language Features Functions with Arguments Full

Elixir Macros


defmodule ElixirLunchAndLearnTest do
  use ExUnit.Case

  test "the truth" do
    assert 1 + 1 != 2
  end
end
                        

Capture Code

Compile Data into Code


defmodule MimeTypes do
  HTTPotion.start
  HTTPotion.Response[body: body] = HTTPotion.get(
    "http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types"
  )

  Enum.each String.split(body, %r/\n/), fn (line) ->
    unless line == "" or line =~ %r/^#/ do
      [ mimetype | _exts ] = String.split(line)


      def is_valid?(unquote(mimetype)), do: true
    end
  end

  def is_valid?(_mimetype), do: false
end

MimeTypes.is_valid?("application/vnd.exn") #=> false
MimeTypes.is_valid?("application/json")    #=> true
                        

Protocols

  • Allow extending functions to support new types
  • Support for protocol is separate from type definition

Blank.blank?


defprotocol Blank do
  @doc "Returns true if data is considered blank/empty"
  def blank?(data)
end
                        

# Integers are never blank
defimpl Blank, for: Integer do
  def blank?(_), do: false
end

# Just empty list is blank
defimpl Blank, for: List do
  def blank?([]), do: true
  def blank?(_),  do: false
end
                        

Structs


defprotocol Phoenix.Param do
  def to_param(term)
end

defmodule User do
  @derive {Phoenix.Param, key: :email}
  defstruct [:id, :email]
end

user = %User{id: 1, username: "alice@example.com"}
Phoenix.Param.to_param(user) # "alice@example.com"
                        

Examples

  1. Pipes
  2. FizzBuzz
  3. Process Scaling

Pipes

  • Eliminate deeply nested function calls
  • Eliminate throw-away temporary variables
  • Write shell-style pipelines between functions

# deeply nested function calls
def main(argv) do
  output(process(parse_args(argv)))
end
                        

# single-use variables
def main(argv) do
  parsed_args = parse_args(argv)
  processed = process(parsed_args)
  output(processed)
end
                        

# Pipes
def main(argv) do
  argv
  |> parse_args
  |> process
  |> output
end
                        

If-less fizz-buzz


defmodule FizzBuzz do
  def fizz_buzz(n), do: fizz_buzz(rem(n, 3), rem(n, 5), n)

  defp fizz_buzz(0, 0, _), do: "FizzBuzz"
  defp fizz_buzz(0, _, _), do: "Fizz"
  defp fizz_buzz(_, 0, _), do: "Buzz"
  defp fizz_buzz(_, _, n), do: n
end

FizzBuzz.fizz_buzz 10 # "Buzz"
FizzBuzz.fizz_buzz 11 # 11
FizzBuzz.fizz_buzz 12 # "Fizz"
FizzBuzz.fizz_buzz 13 # 13
FizzBuzz.fizz_buzz 14 # 14
FizzBuzz.fizz_buzz 15 # "FizzBuzz"
                    

Process Scaling

Process Memory


f = fn -> receive do
  after
    :infinity -> :ok
  end
end
{_, bytes} = Process.info(spawn(f), :memory)
bytes # 2680
                        

Process Time (1)


defmodule ElixirLunchAndLearn.Chain do
  def run(n) do
    {microseconds, result} = :timer.tc(__MODULE__, :create_processes, [n])
    IO.puts "#{result} (Calculated in #{microseconds} microseconds)"
  end
end
                        

Process Time (2)


defmodule ElixirLunchAndLearn.Chain do
  def create_processes(n) do
    last = Enum.reduce 1..n,
                       self,
                       fn(_, send_to) ->
                         spawn(__MODULE__, :counter, [send_to])
                       end

    # start the count by sending
    send last, 0

    # and wait for the result to come back to us
    receive do
      final_answer when is_integer(final_answer) ->
        "Result is #{inspect final_answer}"
    end
  end
end
                       

Process Time (3)


defmodule ElixirLunchAndLearn.Chain do
  def counter(next_pid) do
    receive do
      n ->
        send next_pid, n + 1
    end
  end
end
                        

Process Time (4)


> elixir --erl "+P 1000000" -r lib/elixir_lunch_and_learn/chain.ex -e "ElixirLunchAndLearn.Chain.run(1_000_000)"
Result is 1000000 (Calculated in 11658944 microseconds)
                        

Resources

Intros

Help

Videos

Books

Websites