Programming Elixir

The Magic of Today's Tonic

Katie Miller (@codemiller)
OpenShift Developer Advocate at Red Hat

Scope

  • In: A brief look at Elixir and examples of some of its key elements: support for functional programming techniques, concurrency and metaprogramming
  • Out: A comprehensive description of Elixir and its syntax, Erlang, OTP or functional programming

Elixir Distilled

What?

  • Dynamically typed, general purpose programming language targeting the Erlang Virtual Machine (BEAM)
  • Elixir programs compile to Erlang VM bytecode
  • Influenced by Erlang, Ruby and Clojure
  • Supports functional programming techniques

Where?

Who?

  • Elixir was created by José Valim
  • About 130 contributors on GitHub with ~6100 commits

When?

  • Work started in January 2011
  • First version with equivalent functionality to Erlang released August 2012
  • Current version released January 2014

Why?

  • Boost productivity and extensibility of code on the Erlang VM, while maintaining compatibility with the Erlang ecosystem and harnessing its power
  • "Elixir has a non-scary syntax and combines the good features of Ruby and Erlang. ... Erlang powers things like WhatsApp and crucial parts of half the world's mobile phone networks. It's going to be great fun to see what will happen when the technology becomes less scary and the next wave of enthusiasts joins the party." - Joe Armstrong, Erlang creator

How?

  • Everything is an expression
  • Shared-nothing concurrent programming via message passing (Actor Model)
  • Metaprogramming via macros
  • Polymorphism via protocols
  • First-class documentation
  • Pattern matching
  • First-class functions, regular expression literals, UTF-8 strings, records, atoms, tuples, ranges, string sigils

Ecosystem

  • Mix build tool
  • ExUnit unit test framework
  • ExDoc documentation tool
  • Ecto language-integrated query
  • Dynamo web framework
  • EXPM library repository
  • Erlang libraries

A Taste of Elixir


defmodule Hello do
  def greet(name // "World") do
    {:ok, "Hello " <> name <> ". Your rating is: #{rate_name name}"}
  end

  def rate_name(name), do: size(name) * :math.pi

  def print({_, str}), do: IO.puts str
end

Hello.greet |> IO.inspect
# {:ok, "Hello World. Your rating is: 15.707963267948966"}

Hello.greet("codemiller") |> Hello.print
# Hello codemiller. Your rating is: 31.41592653589793
                                    

Ingredients of an Elixir

Functional Features

Origins

  • Functional programming paradigm: Computation as the evaluation of mathematical functions
  • The FP style makes it easier to reason about code
  • Functional programming is based on lambda calculus

#1 Immutability

  • All values are immutable in Elixir
  • Unlike Erlang, in Elixir you can rebind variables
  • As nothing is mutable, data can be reused when building new structures

defrecord Liquid, name: :nil, alcohol_pc: 0
defrecord Beverage, name: :nil, content: {Liquid.new, 0}

sav = Liquid.new name: "Sauvignon Blanc", alcohol_pc: 11.5
house = Beverage.new name: "House White Wine", content: {sav, 150}
special = house

house.name "House White"
IO.inspect house
# Beverage[name: "House White Wine",
#  content: {Liquid[name: "Sauvignon Blanc", alcohol_pc: 11.5], 150}]

house = house.name "House White"
IO.inspect house
# Beverage[name: "House White", 
#  content: {Liquid[name: "Sauvignon Blanc", alcohol_pc: 11.5], 150}]

IO.inspect special
# Beverage[name: "House White Wine", 
#  content: {Liquid[name: "Sauvignon Blanc", alcohol_pc: 11.5], 150}]
                                        

Some Other Appearances

  • Strings in Java and JavaScript
  • Strings and tuples in Python
  • Frozen objects in Ruby
  • Read-only attributes in Perl
  • Val declarations in Scala
  • Collections in Clojure
  • All data structures in Haskell

#2 Pattern Matching and Guards

  • Pattern matching deconstructs structured data
  • list = ["b","w","s"]
    [a,b,c] = list
    IO.inspect b # "w"
    [a,b,c] = ["beer","wine","spirits"]
    IO.inspect b # "wine"
    [a,^b,c] = list
    **(MatchError) no match of right hand side value: ["b","w","s"]
  • Elixir will match patterns inside patterns
  • Guard clauses add predicates that allow you to match based on argument content,
    eg. when is_number(num_drinks) and num_drinks > 0
  • Only a subset of expressions are permitted in guards

sav_blanc = Liquid.new(name: "Sauvignon Blanc", alcohol_pc: 11.5)
soda_water = Liquid.new(name: "Soda Water")
wine = Beverage.new(name: "Glass Wine", content: {sav_blanc, 150})
soda = Beverage.new(name: "Glass Soda", content: {soda_water, 250})

defmodule Barcalc.Convert do
  def standard_drinks({Beverage[content: {Liquid[alcohol_pc: pc],
                         ml}], qty}) when pc > 0 do
    std = ml / 1000 * pc * 0.789 * qty 
    IO.puts "It's #{:io_lib.format('~.1f', [std])} standard drinks."
  end

  def standard_drinks({Beverage[], _}), do: IO.puts "No alcohol."
end

Barcalc.Convert.standard_drinks({wine, 2})
# It's 2.7 standard drinks.

Barcalc.Convert.standard_drinks({soda, 5})
# No alcohol.
                                        

Some Other Appearances

  • Erlang
  • Haskell
  • OCaml
  • Scala
  • Rust
  • Roy

#3 List Processing

  • Lists are processed recursively; there are no loops
  • The vertical bar is a list constructor (cons); lists can be pattern matched using this structure:
  • list = ["wine" | ["beer", "spirits"]]
    # ["wine", "beer", "spirits"]
    double_head_list = ["wine", "beer" | ["spirits"]]
    # ["wine", "beer", "spirits"]
    
  • Elixir also has list comprehensions:
  • lc x inlist [1, 2], y inlist [10, 20], x*y < 30, do: x*y
    # [10, 20, 20]
    
# defrecord Drink, name: :nil, content: [{Liquid.new, 0}]
def standard_drinks(drinks) do
  sum_std_drinks(drinks, 0)
end
  
defp sum_std_drinks([], acc), do: acc 
defp sum_std_drinks([ {drink, qty} | others ], acc) do
  sum_std_drinks(others, acc + calc_item(drink, qty))   
end
  
defp calc_item(Drink[content: ingrts], qty)
  qty * (std_per_ingrt(ingrts) |> sum_ingrts)
end

defp std_per_ingrt(ingrts) do
  lc {Liquid[alcohol_pc: pc], ml} inlist ingrts, do: calc_std(ml, pc)
end

defp calc_std(vol_ml, alc_pc), do: vol_ml / 1000 * alc_pc * 0.789

defp sum_ingrts(ingrts), do: do_sum_ingrts(ingrts, 0)
defp do_sum_ingrts([], acc), do: acc
defp do_sum_ingrts([ h | t ], acc), do: h + do_sum_ingrts(t, acc)

sav_blanc = Liquid.new(name: "Sauvignon Blanc", alcohol_pc: 11.5)
middy = Liquid.new(name: "Mid-Strength Beer", alcohol_pc: 3.4)
tequila = Liquid.new(name: "Tequila", alcohol_pc: 38)
triple_sec = Liquid.new(name: "Triple Sec", alcohol_pc: 40)
lime_juice = Liquid.new(name: "Lime Juice")

wine = Drink.new(name: "Glass of Wine", content: [{sav_blanc, 150}])
beer = Drink.new(name: "Schooner of Beer", content: [{middy, 425}])
margarita = Drink.new(name: "Margarita", content: [{tequila, 30}, 
 {triple_sec, 15}, {lime_juice, 15}])

Barcalc.Convert.standard_drinks([{wine,1}, {beer,2}, {margarita,1}])
# 5.014095
                                        

Some Other Appearances

  • Cons cell lists: Common Lisp, Clojure, Haskell, F#, OCaml, Erlang
  • List comprehensions: Python, CoffeeScript, Perl, Groovy, Scala, Haskell, OCaml, Erlang, Clojure, Ceylon, Common Lisp

#4 Lambdas and Higher-Order Functions

  • There are two notations for anonymous functions:
  • total_cost = fn (cost, quantity) -> cost * quantity end 
    total_cost = &(&1 * &2)
    total_cost.(3.95, 4)
    # 15.8
    
  • A higher-order function takes a function as an argument or returns one; map, filter and fold/reduce are prominent examples
defmodule Barcalc.Convert do
  def standard_drinks(drinks) do
    Enum.map(drinks, &calc_for_item/1) |> Enum.reduce(0, &(&1 + &2))
  end
  
  defp calc_for_item({Drink[content: ingrts], quantity}) do
    quantity * sum_drink(ingrts)
  end

  defp sum_drink(ingrts) do
    Enum.reduce(ingrts, 0, fn {Liquid[alcohol_pc: pc], ml}, acc ->
      calc_standard_drinks(ml, pc) + acc
    end)
  end

  defp calc_standard_drinks(volume_ml, alcohol_pc) do
    volume_ml / 1000 * alcohol_pc * 0.789
  end
end

Barcalc.Convert.standard_drinks([{wine,1}, {beer,2}, {margarita,1}])
# 5.014095
                                        

Some Other Appearances

  • JavaScript
  • PHP
  • Clojure
  • Perl
  • C#
  • Python
  • Scala
  • Haskell

#5 Stream Processing

  • Stream functions allow you to enumerate a collection lazily
  • Streams can be infinite
  • Stream module functions include cycle, repeatedly, iterate, unfold and resource
defmodule Barcalc.Convert do
  def standard_drinks(drinks) do
    Stream.map(drinks, &calc_for_item/1) |> Stream.scan(0, &(&1+&2))
  end

  defp calc_for_item({Drink[content: ingrts], quantity}) do
    quantity * sum_drink(ingrts)
  end

  defp sum_drink(ingrts) do
    Enum.reduce(ingrts, 0, fn {Liquid[alcohol_pc: pc], ml}, acc ->
      calc_standard_drinks(ml, pc) + acc
    end)
  end

  defp calc_standard_drinks(volume_ml, alcohol_pc) do
    volume_ml / 1000 * alcohol_pc * 0.789
  end
end

Barcalc.Convert.standard_drinks(Stream.cycle([{wine, 1}, {beer, 1}, 
 {margarita, 1}])) |> Enum.take(5)
# [1.361025, 2.50113, 3.87399, 5.235015, 6.37512]

Some Other Appearances

  • Generators in Python 3
  • Clojure sequences
  • Miranda and Haskell (lazy by default)
  • Scheme and OCaml (special lazy syntax)

Concurrency Support

Origins

  • Elixir leverages the Erlang Virtual Machine and Open Telecom Platform (OTP)
  • "The Erlang flagship project...has achieved a NINE nines reliability (yes, you read that right, 99.9999999%)." - Joe Armstrong, Erlang creator
  • OTP offers high reliability, supervised processes, distributed applications, a framework for hot code swapping, and failure management
  • In OTP, systems are hierarchies of applications made up of processes that follow conventions called behaviours; some processes may be supervisors

Concurrency in Elixir

  • Actor-based concurrency: lightweight, share-nothing processes using inter-process messages to synchronise activities
  • Processes run across all CPUs with very little overhead
  • iex> rec = spawn fn -> receive do {pid,msg} -> IO.puts msg end end
    #PID<0.43.0>
    iex> spawn fn -> (rec <- {self, "Oh, hai!"}) end
    #PID<0.45.0>
    Oh, hai!
  • Mix adds example OTP config to new projects by default

Some Other Appearances

  • Erlang
  • Scala and Akka
  • Go goroutines and channels
  • Rust tasks

Metaprogramming Capabilities

Origins

  • Macros inspired by Lisp-style macros in Clojure; Lisp macros were introduced in the early 1960s
  • Macros, or macroinstructions, offer a way to transform program source code
  • Syntactic macros, such as Lisp-style macros, manipulate abstract syntax trees
  • Macros are powerful but should not be overused

Elixir Macros

  • Elixir is a homoiconic language - code can be represented using its own data structures; those representations can be manipulated with macros
  • Macros are defined inside a module with 'defmacro'; parameters passed to macros are not evaluated
  • The quote function takes a block of code and returns its internal representation
  • The unquote function is used within quote blocks to inject a code fragment
  • Macros are hygenic

Some Other Appearances

  • Common Lisp, Scheme and Racket
  • Clojure
  • Scala (experimental)
  • Rust
  • Io

Conclusion

  • Functional programming features help us write code that is demonstrably correct and composed of small, reusable pieces that are easy to test and maintain
  • Support for building concurrent, distributed, fault-tolerant applications helps us write programs to suit today's multicore, highly available, distributed computing environments
  • Metaprogramming with macros helps us write domain-specific languages, which can help improve productivity and communication

One Final Ingredient

References, Resources
and Credits

References and Resources

Image Credits

Programming Elixir

http://elixir.codemiller.com

Katie Miller (@codemiller)
OpenShift Developer Advocate at Red Hat