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.