Just a sip of Elixir

19 March 2021

Elixir is a functional programming language I’ve been excited by for some time. Here is a quick taste.

Atoms (named constants)

:foobar
#=> :foobar

Lists

[1, 2, 3, 4]
#=> [1, 2, 3, 4]

[1 | [2, 3, 4]]
#=> [1, 2, 3, 4]

[1 | [2 | [3 | [4 | []]]]]
#=> [1, 2, 3, 4]

Tuples

{:ok, "Go!"}
#=> {:ok, "Go!"}

Strings and binaries

"Héllö"
#=> "Héllö"

"Héllö" == <<72, 195, 169, 108, 108, 195, 182>>
#=> true

<<240, 159, 146, 156>>
#=> "💜"

Keyword lists (ordered associative lists)

list = [{:a, 1}, {:b, 2}]
#=> [a: 1, b: 2]

list == [a: 1, b: 2]
#=> true

list[:a]
#=> 1

Maps

map = %{:a => 1, 2 => :b}
#=> %{2 => :b, :a => 1}

map[2]
#=> :b

map[:c]
#=> nil

Anonymous functions

add = fn a, b -> a + b end

add = &(&1 + &2) # Shorthand

add.(2, 4)
#=> 6

Pattern matching

{a, b, c} = {:hello, "world", 42}
#=> {:hello, "world", 42}
a
#=> :hello
b
#=> "world"

{a, b, c} = {:hello, "world"}
#=> ** (MatchError) no match of right hand side value: {:hello, "world"}

{:ok, result} = {:ok, 13}
#=> {:ok, 13}
result
#=> 13

{:ok, result} = {:error, :oops}
#=> ** (MatchError) no match of right hand side value: {:error, :oops}

[head | tail] = [1, 2, 3]
#=> [1, 2, 3]
head
#=> 1
tail
#=> [2, 3]

Modules and named functions

defmodule HelloWorld do
  def greet(name) do
    IO.puts "Hello #{name}!"
  end
end

HelloWorld.greet("World")
#stdout> Hello World!
#=> :ok

Recursion

defmodule Fibonacci do
  def fib(0), do: 0
  def fib(1), do: 1
  def fib(n) when n > 1, do: fib(n-1) + fib(n-2)
end

Fibonacci.fib(10)
#=> 55

Fibonacci.fib(-4)
#=> ** (FunctionClauseError) no function clause matching in Fibonacci.fib/1

Map function

defmodule ListOps do
  def map([], _f), do: []
  def map([head | tail], f) do
    [f.(head) | map(tail, f)]
  end
end

ListOps.map([1, 2, 3], &(&1 * 3))
#=> [3, 6, 9]

Enum.map and Enum.reduce

Enum.map([1, 2, 3], &(&1 * 3))
#=> [3, 6, 9]

Enum.reduce([1, 2, 3], 0, fn(x, acc) -> x + acc end)
#=> 6

[1, 2, 3]
|> Enum.map(&(&1 * 3))
|> Enum.reduce(0, fn(x, acc) -> x + acc end)
#=> 18

Pipe operator (|>)

Enum.take(Enum.filter(Enum.map(1..1_000_000, &(&1 * 3)), &Integer.is_odd/1), 10)
#=> [3, 9, 15, 21, 27, 33, 39, 45, 51, 57]

1..1_000_000
|> Enum.map(&(&1 * 3))
|> Enum.filter(&Integer.is_odd/1)
|> Enum.take(10)
#=> [3, 9, 15, 21, 27, 33, 39, 45, 51, 57]

Stream module (lazy enumerables)

1..1_000_000
|> Stream.map(&(&1 * 3))
|> Stream.filter(&Integer.is_odd/1)
|> Enum.take(10)
#=> [3, 9, 15, 21, 27, 33, 39, 45, 51, 57]

Processes

defmodule PingPong do
  def await(count) do
    new_count = count + 1
    receive do
      {:ping, caller} -> send(caller, {:pong, new_count})
    end
    await(new_count)
  end
end

pid = spawn(PingPong, :await, [0])
#=> #PID<0.94.0>

send(pid, {:ping, self})
#=> {:ping, #PID<0.59.0>}

flush
#stdout> {:pong, 1}
#=> :ok

Task (run asynchronous job)

task = Task.async(fn -> :some_result end)
#=> %Task{pid: #PID<0.211.0>, ref: #Reference<0.0.2.334>}

Task.await(task)
#=> :some_result

Agent (store state)

{:ok, pid} = Agent.start(fn -> %{} end)
#=> {:ok, #PID<0.72.0>}
Agent.update(pid, fn map -> Map.put(map, :key, "value") end)
#=> :ok
Agent.get(pid, fn map -> Map.get(map, :key) end)
#=> "value"

Quoted expressions (AST)

quote do: sum(1, 2, 3)
#=> {:sum, [], [1, 2, 3]}

quote do: 1 + 2
#=> {:+, [context: Elixir, import: Kernel], [1, 2]}

Macro.to_string({:+, [context: Elixir, import: Kernel], [1, 2]})
#=> "1 + 2"

quote do: sum(1, 2 + 3, 4)
#=> {:sum, [], [1, {:+, [context: Elixir, import: Kernel], [2, 3]}, 4]}

inner = [3, 4, 5]
Macro.to_string(quote do: [1, 2, unquote(inner), 6])
#=> "[1, 2, [3, 4, 5], 6]"

Macros

defmacro unless(expr, opts) do
  quote do
    if(!unquote(expr), unquote(opts))
  end
end

unless true do
  IO.puts "this will never be seen"
end
defmodule MyMacro do
  defmacro twice(block) do
    quote do
      IO.puts "About to do something twice"
      unquote(block)
      unquote(block)
    end
  end
end

import MyMacro

twice do
  IO.puts "Hi there!"
end
#stdout> About to do something twice
#stdout> Hi there!
#stdout> Hi there!