Elixir for Python Developers (Part 1)


Elixir

Before we get started, I strongly encourage to have a REPL/IEx to try out the examples. More details about installation can be found here.

What is Elixir ?

Wikipedia describes “Elixir as a functional, concurrent, general-purpose programming language that runs on the Erlang virtual machine (BEAM)”

Why Elixir ?

  • Easy to learn
  • Builds on BEAM VM which is very mature
  • Very good support for building low latency, distributed, fault tolerant applications
  • Offers very good debugging tools
  • Dirt cheap concurrency
  • Metaprogramming
  • Functional (is the new cool :D)

Helpful Commands To Get Started

IEx offers a lot of helper functions. It is very similar to IPython. Lets get started.

iex(1)> h # <- Interactive Elixir help
iex(2)> i # <- prints information about last term

Similar To Python

Basic Types

All data structures in Elixir are immutable unlike in Python. Each operation creates a new copy of the data structure. Internally these data structures are optimized to be efficient by minimizing making copies and reusing parts of changed data structures.

Atom

Elixir got a type called atom, whose name and value are the same. It starts with : or an uppercase letter

iex> is_atom(:hello)
true
iex> is_atom(Hello)
true

Atoms are used as tags for data. We will see it when using Map(Dict in Python) in Elixir.

List

List in Elixir is similar to list in Python.

iex> a=[1,2,3,4]
iex> List.replace_at(a, 1, 44)
[1, 44, 3, 4]

List also offers elegant destructuring abilities.

iex> [b|c] = a
[1, 2, 3, 4]
iex> b
1
iex> c
[2, 3, 4]

It follows the format [head|tail]. The same functionality in Python would be

In [1]: a = [1,2,3,4]
In [2]: [b, *c] = a
In [3]: b
Out[3]: 1
In [4]: c
Out[4]: [2, 3, 4]

Range

Ranges in Elixir are represented as 1..3, same as range(1,3) in Python.

iex> i 1..3 # <- Get information about 1..3
Term
  1..3
Data type
  Range

Tuple

Tuples in Elixir are similar to those in Python and can store any data and are ordered.

iex>{:tu, "hello", 5}
iex> elem(a,1) # <- Accessing Element
"hello"

Same in Python would be as follows.

('tu', "hello", 5)

BitString and Binary

Elixir has BitString which are data represented in bits and binary where data are stored as bytes. This is useful for pattern matching. bitstring allows to manipulate and match on bits, while binary works on bytes. <<>> is used to define a binary. A binary is a bitstring when the number of bits is divisible by 8(i.e a byte).

  # A bitstring
  bitstr = <<<4 :: size(3) >> # 3 bits 100, 3 not divisible by 8
  is_binary(bitstr) # false
  is_bitstring(bitstr) # true

Notice the size(3) which means 3 bits.

  # A binary
  binary = << 4 >> # 8 bits or 1 byte, so it becomes a binary
  is_binary(binary) # true
  is_bitstring(binary) # true

Strings

Strings in Elixir are represented internally as UTF-8 encoded binaries, which are a sequence of bytes.

  iex> is_binary("hellö")
  true

  # Bytesize is 6, even though there are only 5 letters, as unicode of ö needs 2 bytes
  iex> byte_size("hellö")
  6

  # To get length of String use String.length
  iex> String.length("hellö")
  5

String module in Elixir offers functions for working with strings. Strings can be sliced using String.slice.

iex(1)> name="Übung"
iex(2)> String.slice(name, 0..-1)
"Übung"
iex(3)> String.slice(name, 0..3)
"Übun"

Unlike in Python and are not same in Elixir. Double quotes() are used for Strings and single quotes for Char lists. Char list is a List with characters, while String is a UTF-8 encoded binary. To see the internal representation of a string we add null byte <<0>>.

# String
iex(1)> s = "hellö"
"hellö"

# Add null byte to see representation
iex(2)> s <> <<0>>
<<104, 101, 108, 108, 195, 182, 0>>

# Charlist
iex(3)> c = 'hellö'
[104, 101, 108, 108, 246]

Keyword List

Keyword lists are list of tuples(which can have anything) with atoms as keys. The keys are ordered and hence are used as parameter lists.

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

The Elixir syntax for these is [key: value]. For a similar functionality in Python, we can use list of tuples.

Map

Map is the equivalent of dict in Python. It follows the syntax %{“key” => “value”}

iex> map = %{"M" => "QA", "N" => 100, 200 => "D"} # <- A Map/dictionary
%{200 => "D", "M" => "QA", "N" => 100}

iex> map["M"] # <- Access value of Map
"QA"

iex> %{map | "M" => "DE"} # <- Update Value
%{200 => "D", "M" => "DE", "N" => 100}

You can also use Map module for working with maps. Maps can be nested inside maps, whereas dict in python can’t do it, as dict is unhashable. Python can use named tuples from collections, to get similar results.

# In Elixir
iex> %{%{"M" => "DE"} => "FE"}
%{%{"M" => "DE"} => "FE"}
# In Python
In [1]: {{"M":"DE"}: "FE"}
TypeError: unhashable type: 'dict'

In Elixir nested map updation and access can be done using Kernel.put_in and Kernel.get_in

iex> nested = %{"name" => %{"first" => "A"}}
%{"name" => %{"first" => "A"}}

# Update nested using put_in
iex> put_in(nested["name"]["first"], "B")
%{"name" => %{"first" => "B"}}

Boolean

In Python many values can be a false along with False. For eg: [], {}, None, 0, 0.0 etc return False.

In [1]: if 0:
   ...:     print("Not False")
   ...: else:
   ...:     print("False")

In Elixir it is a bit different. The only false values in Elixir are false and nil. They are actually :false and :nil. Others like [] or 0 are not a false value.

iex> if 0, do: IO.puts("Not false")
Not false

As you would have already inferred ‘#’ starts a comment in both Python and Elixir.

Modules and Functions

Modules are defined in Elixir using defmodule. Modules in Python are file based. In Elixir you can have multiple modules in a single file. Functions in Elixir should be defined inside a module. Like Python functions, Elixir also supports default arguments with ‘\\’ syntax. Functions follow the syntax def function-name do function-body end.

If the function-body is one line, it can be written as def function-name, do: function-body

defmodule Factorial do # <- Defines a module, with defmodule

 # Functions are defined with def
 def factorial(0), do: 1 # <- Pattern matching in arguments
 def factorial(n), do: n * factorial(n - 1) # <- Recursion

 def mul_by(x, n \\ 2) do # <- Default argument for n is 2
  x * n
 end

end
iex> Factorial.factorial(5)
120

Module can also have a struct inside it. Structs are similar to Maps, but offers compile time checks and allows providing default values. It can be considered as map with a fixed structure/attributes. Structs can be defined using defstruct keyword. For example we can define a struct Product which holds a name.

defmodule Product do
  defstruct [:name]
end

# And a new structure can be constructed using below syntax
%Product{name: "Hello"}

Anonymous Functions / Lambdas

lambda equivalent of Elixir is fn. It follows the syntax, fn (x) -> body end.

One main difference with fn and def is that fn makes a closure, which def doesn’t, as shown in example below.

iex> company = "T"
"T"

# Anonymous functions are defined with fn and forms a closure
iex> sayName = fn p -> IO.puts("I am #{p} in #{company}") end # <- Anonymous function
iex> sayName.("M") # <- Note '.' after sayName
I am M in T # <- 'T' came from closure

Also anonymous functions in Elixir doesn’t suffer from late-binding problems like in Python. Also since Elixir data is immutable, closure works like it got a copy of the value and not an identifier bound to lexical scope like in Python.

Variable rebinding

Variable rebinding is a source of confusion for Elixir beginners. Consider the following code.

iex(1)> name = 'J'
'J'
iex(2)> name = 'K'
'K'

How is that even possible?. Isn’t name immutable?. Yes it is immutable. What happens here is name got rebind to a new value. So anyone holding a reference to name, when name was ‘J’, will not see any difference. Only new users of name with value ‘K’ gets the new value. See the below code which shows it in action.

iex(1)> company = "T"
"T"
iex(2)> sayName = fn p -> IO.puts("I am #{p} in #{company}") end # <- Anonymous function
#Function<6.99386804/1 in :erl_eval.expr/5>
iex(3)> sayName.("M")
I am M in T
:ok
iex(4)> company = "J"
"J"
iex(5)> sayName.("M")
I am M in T # <- See company is still T
:ok

Not Similar To Python

Pattern Matching

= is called match operator. Different from usual programming languages like Python. JS/Python3 also offers a limited pattern matching. It allows only either assignment or matching and not both. (https://gist.github.com/mikaelbr/9900818). Elixir offers both at the same time, as assignment operator(=) is actually a match operator.

  iex(60)> {:ok, r} = {:ok, 42}
  {:ok, 42}
  iex(61)> r
  42

  # Match fails, as :fail != :ok
  iex(63)> {:fail, r} = {:ok, 42}
  ** (MatchError) no match of right hand side value: {:ok, 42}

  # Pattern matching on lists
  iex(63)> [h|t] = [4, 2, 1]
  [4, 2, 1]
  iex(63)> h
  4
  iex(64)> t
  [2, 1]

  # Pin operator, to pin your values
  iex(65)> name = "name"
  "name"
  iex(66)> {^name, h} = {"name", 8}
  {"name", 8}
  iex(67)> h
  8
  iex(68)> {^name, h} = {"hello", 8}
  ** (MatchError) no match of right hand side value: {"hello", 8}

  # The same can be done for functions too.
  defmodule Fact do 
  	# Matches when input is 0, and precedence of matching is from top to bottom.
  	def fact(0), do: 1
  	def fact(n), do: n*fact(n-1)
  end

Case statement can be used to match on arguments. Python unfortunately, doesn’t offer such an elegant pattern matching support.

  iex(6)> v = {1,2}
  {1, 2}
  iex(7)> case v do
  ...(7)> {1,3} -> 5
  ...(7)> _ -> 4 # it matches everything
  ...(7)> end
  4

Thats all for now. See you in part 2