Elixir for Python Developers (Part 2)


Elixir

Error Handling in Elixir

Pattern matching support in Elixir enables us to write clean and clear error handling code. with keyword in elixir is similar to the Either Monad.

 # This one succeeds and gives 10
 iex(3)> with a <- 5,
 ...(3)> b <- 5,
 ...(3)> do: a + b
 10

 # This one fails at 4 <- 5 and hence returns 5, the failing value
 iex(6)> with a <- 5,
 ...(6)> 4 <- ,
 ...(6)> do: 100
 5

Looping

Loops in functional languages are done using recursion. Elixir being a functional language, lacks looping constructs like for, and we will have to resort to recursion. Commonly used functional constructs like map, filter and reduce are available in the Enum module. It can thus be used for traversing lists/data structures. In order to map over a data structure, we can use Enum.map

Enum.map(1..3, fn x -> x * 2 end)

In Python, we normally write a for loop. Python also got support for functional style of programming. In Python, it would be

list(map(lambda x: x*2, range(1, 4)))

In Python we use the list() to get the output. It is because, in Python3 range is lazy, while Enum in Elixir is strict. For lazy iteration, Elixir provides Stream module.

stream = Stream.map(1..3, fn x -> x*2 end)
# To evaluate stream, use Enum.map
Enum.map(stream, fn x -> x end)

Polymorphism

Polymorphism is achieved in Elixir using Protocols. Protocols can be implemented for a data type and dispatching to data type specific implementation of a function will be automatic. In Python, this can be implemented using Inheritance/Abstract Base classes.

defprotocol SayName do 
 @doc "Says the type of data structure"
 def name(d)
end

defmodule User do 
 defstruct [:name]
end

# If you want to implement this protocol, just implement the name function name
defimpl SayName, for: BitString do 
 def name(string), do: "I am bitString"
end

defimpl SayName, for: Map do 
 def name(map), do: "I am map"
end

defimpl SayName, for: User do 
 def name(user) do 
  "I am #{user.name}"
 end
end

SayName.name("ff")
"I am bitString"

SayName.name(%{ok:  "fff"})
"I am map"

SayName.name(%User{name: "hellouser"})
"I am hellouser"

The Python equivalent will have to make an ABC class with sayName function and all other classes should inherit from it.

from abc import ABC, abstractmethod

# We make a base abstract class 
class SayYourName(ABC):
 @abstractmethod
 def name(self):
  return 'base method'

# We inherit from the Abstract base class and override say_name fucntion
class BitString(SayYourName):
 def name(self):
  return "I am bitString"

If you implement a protocol for Any, it will be there for all data types, if you explicitly derive it. Or use fallback_to_any in defprotocol. IEx use Inspect protocol to print values.

Elixir also got behaviours, which is similar to interface in other languages. They are defined using @callback and @behaviour

 # This is the Behaviour
 defmodule P do 
 @callback p()

 # Every module which need to have this Behaviour should implement p()
 defmodule PP do 
  @behaviour P
  def p(), do: :something

The difference between Behaviour and Protocol is that, dispatching based on data type is automatic in Protocols.

Data Type Ordering

Like Python, Elixir have ordering for types. This is something I am not fond of personally. I like the approach taken by statically typed languages like Java, where only values of same type can be compared. In Elixir, types follows the order number < atom < reference < function < port < pid < tuple < map < list < bitstring

 iex> 1 < :atom
 true

iex> {"hello"} < "hello"
true

 # In Python
 In [1]: "ddd" > 4
 Out[1]: True

 In [2]: 4 > {}
 Out[2]: False

Input/Output

IO module is used to read/write to File/console. File module provides functions for working with files. File operations are a bit different in Elixir. When we open a file, the handle we get back is a PID(Process ID) which have access to the file. Reading and writing occurs by sending messages to this process.

 iex(1)> {:ok, f} = File.open "h", [:write]
 {:ok, #PID<0.87.0>}
 # As you can see we got a new process here.
 IO.write(f, "h")
 :ok

When we call IO.write, a message is sent to process f for writing to file. By modelling IO as processes, nodes can read/write files in other nodes. Each process have a special IO device called group leader for handling stdio operations. When you write to stdio, you are sending a message to this group leader. Elixir docs provides more information on this.

Metaprogramming

Metaprogramming allows you to generate code at compile time. Elixir provides this functionality using Macros. Macros are expanded at compile time.

Macros Demonstrated

Building block of elixir macro is a 3 element tuple. You can get this representation with quote.

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

The elements are { function name, metadata, argument list } respectively. It follows typing {atom | tuple, list, list | atom}. Another example which shows how nesting is done is given below.

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

Macro.to_string converts macro to string representation.

  iex(10)> number = 1
  1
  iex(11)> Macro.to_string(quote do: 10 + unquote(number))
  "10 + 1"

Unquote is used to evaluate a quoted expression. Macros always receive and return quoted expressions. Some values like functions and references cannot be quoted. It means you won’t get the contents of a function/reference. Lets look at a Macro example.

defmodule F do
  def fun_unless(clause, do: expression) do
   if(!clause, do: expression)
  end

 # defmacro defines a macro
 defmacro macro_unless(clause, do: expression) do
   IO.inspect(expression)
   quote do
     if (!unquote(clause)), do: unquote(expression) # unquote evaluates the expression
   end
 end
end

Now we have defined a Macro. In order to use the macro, we need to require/import it. Elixir provides require, which makes sure that macros are compiled before using them. Elixir also provides import which is like require, but in addition, it also puts Macro into to current scope.

# Trying out 
iex> require F
iex(7)> F.macro_unless false, do: IO.puts "F"
# IO.puts "F" in quoted form
{{:., [line: 7], [{:__aliases__, [line: 7], [:IO]}, :puts]}, [line: 7], ["F"]}
# Return value
F
:ok

Elixir also provides use, which when used, executes the _using_ function defined in the module. If you are interested in learning more, Elixir docs are very comprehensive.

Sofar we have compared programming language constructs in both Python and Elixir. Now lets dive into parts which are specific to Elixir, and what it provides to build distributed fault-tolerant applications. Lets see again in part 3.