Elixir for Python programmers, part 2

I already wrote about the high-level differences between Python and Elixir. Learning Elixir from Python comes with some headaches, gotchas and other nuances that may be obvious to those that come from Erlang, but unintelligible to those coming from Python. I had to struggle with these myself.

I feel the material out there ignores too much the nuances and ah-has of the language, making a lot peole have more headaches than they should. Me included.

It's important to keep in mind Elixir has more syntactical constructs than Python, often with the same pratical result. It offers many more ways of writing a certain piece of code, with the difference being often personal preference.

Of course, this guide can be useful to non-Python programmers as well, but I will be often making comparisons to how something works in Python.

Getting started

Install elixir following this guide.

Hex

Hex is Elixir's package manager. It is usually run through mix.

The first time you run an hex command through mix, mix will ask you to install hex.

Mix

Mix is Elixir's build tool. It can do a lot of things:

  • mix new <folder> --app=<app> creates a new project
  • mix deps.get downloads dependencies
  • mix format formats the code
  • mix test runs tests
  • mix docs builds the documentation (requires ex_doc)
  • mix hex.publish publishes a package

Projects can add their own commands to mix, so you may have extra commands depending on your dependencies. For example, I use credo for static analysis:

mix credo

Iex

Iex is Elixir's REPL. It's not as fancy as ipython, but it's more useful than the standard Python REPL.

  • --erl "-kernel shell_history enabled" enables history between sessions
  • -S mix loads the local app into iex

They can be combined into iex -erl "-kernel shell_history enabled" -S mix for a nicer iex experience.

Iex can be also configured using a .iex.exs file. This article has a detailed explanation.

Atoms

Atoms are a primitive, singleton, immutable type. Atoms are not garbage collected, take less memory than a string and for these reasons they work well in combination with pattern matching.

It's important to note that an atom is not equivalent to the corresponding string, but always only to itself:

:atom == "atom" # false
:atom == :atom

:ok and :errorare often used to communicate inequivocabily the result of an operation and to allow to handle the result with pattern matching.

Finally, true, false and nil are atoms, so :true == true, :false == false and :nil == nil.

Tuples, lists and maps

Elixir has tuples, lists, maps and keyword lists. The syntax does not help, and it took me a fair amount of time to get them right.

On the plus side, maps and keyword lists return nil if you try to access a non-existing key.

This is a tuple:

{"hello", "world"}

It is accessed with elem:

elem(tuple, index)

This is a linked list:

["hello", "world"]

Only the first and last element can be accessed with ease:

List.first(list)
List.last(list)

But they can be summed and subtracted, which makes them useful for iteration:

["hello"] ++ ["world"]
["hello", "world"] -- ["world"]

Adding at the beginning is faster:

["hello" | ["world"]]

This is a map (like a dictionary):

%{"hello" => "world"}

It can be accessed with keys:

map["hello"]

Atoms can be used as keys:

map = %{:hello => "hello"}
map[:hello]

This is a keyword list, which is just tuples inside a list:

[{:italy, "Rome"}, {:netherlands, "Amsterdam"}]

However, keyword lists are usually written like this:

[italy: "Rome", netherlands: "Amsterdam"]

And Elixir converts it to the first. They can be accessed with keys:

keylist[:italy]

Macros

In Python we can dynamically create any entity, from properties to classes. In Elixir we can do something similar only at compile time with macros.

A macro is simply a template that receives some arguments and is then compiled at compile time. At run time, the generated code is used as is.

Macros are more powerful than Python object-oriented metaprogramming, but they are harder to write and understand. The general line on macros is that they should not be used, unless it's the only way to do something.

Macros are an advanced topic, but because Elixir makes extensive use of them it's important to have an idea of what they are.

Alias, Import, Use and Require

Elixir does not import modules like in Python: all modules that are found are always available by their fully qualified name. Even inside the module itself.

Where the module is defined does not matter, as long as Elixir can find it.

alias can be used to shorten the fully qualified name of module, to its last part:

defmodule MyApp.Core do
  alias Plug.Conn.Utils

  def run do
    Utils.token("foo")
  end
end

require loads a module's macros into the current one:

defmodule MyApp.Core do
  require Logger

  def run do
    Logger.info("I am running!")
  end
end

import is an enhanced alias + require. Usually, it loads all macros from a module, but it's also possible to specify exactly what to get. Imported functions are available without their module name:

defmodule MyApp.Core do
  import Logger
  import Plug.Conn.Utils

  def run do
    token("foo")
    Logger.info("I am running!")
  end
end

use is a macro that extends a module with another. To work, the parent module must define a __using__ macro.

When a module is used, all its functions and macros will become available in the current one. Anything can happen inside the __using__ macro and most libraries will at least import themselves.

defmodule MyAppTest.Core do
  use ExUnit.Case

  test "the run function" do
    assert false
  end
end

There is no return

In Elixir, there is no return keyword. A function always returns its last executed line:

def hello do
    "hello"
end

hello() # returns "hello"

If is a macro

Elixir has an if construct, but it's a trick. It's a macro and it behaves like a function: what happens inside stays inside, but the result is returned.

This does not work:

y = 0
x = 0
if y == 0 do
  x = 1
end
# x is still 0

this does:

y = 0
x =
  if y == 0 do
    1
  else
    0
  end

Control flow happens with cond and case.

Cond

cond is the closest construct Elixir has to a Python if/else-if/if. However, pattern-matching and case mean cond is an uncommon sight:

y = 0
cond do
    y == 0 ->
        x = 1
    true ->
        x = 0
end

Case

case is similar to cond, but meant for patterns rather than values. Usually, it's used to decide what to do with result of a function:

case Tesla.get('http://example.com') do
    {:ok, response} -> IO.puts("success")
    {:error, error} -> IO.puts("Something went wrong")
end

For is also a trick

In Elixir, iteration happens with function from the Enum module:

Enum.each(["hello", "world"], fn i ->
    IO.puts(i)
end)

Enum provides a number of functions, so here it's just about picking the right one, usually either one of Enum.each, Enum.map or Enum.reduce

Enum.map([1, 2], fn x -> x * 2 end) # [2, 4]
Enum.reduce(["hello", "world"], [], fn i, acc ->
    if i == "hello" do
        acc ++ ["hello"]
    else
        acc
    end
end)

But Elixir also as a for! Like if, it's also a macro. Elixir's for can be used to generate things at compile-time, such as in html templates or in Plug's routers.

Functions

Functions are where Elixir really takes the distance from Python, in terms of code writing. So far we know we don't really have if, for and return, but these are minor changes in respect to how a programmer can think about code.

In Elixir, functions with different number of arguments are different functions:

defmodule MyApp.Core do
  def run() do
    IO.puts("Running!")
  end

  def run(port) do
    IO.puts("Running on port #{port}!")
  end
end

Functions support pattern matching in arguments:

defmodule MyApp.Core do
  def run(port, host) do
    IO.puts("Running on host #{host}, port #{port}!")
  end

  def run(%{port: port, host: host}) do
    run(port, host)
  end

  def run([port, host]) do
    run(port, host)
  end
end

A common use of pattern-matching is to handle different inputs formats, and reducing them to a common one, like the example above, where we can pass port and host in three different ways, but the under the hood, it's always the first function:

MyApp.Core.run(8000, "localhost")
MyApp.Core.run(%{:port => 8000, :host => "localhost"})
MyApp.Core.run([8000, "localhost"])

Of course, we could have different code in each function, but I wanted to show a meaningful case. In Python, we would have been buried in if blocks to do the same:

def run(*args):
    if len(args) == 2:
        print(f'Running on host #{args[1]}, port #{args[0]}')
    else:
        if type(args[0]) == list:
            print(f'Running on host #{args[0][1]}, port #{args[0][0}')
        elif type(args[0]) == dict:
            print(f'Running on host #{args[0]["host"]}, port #{args[0]["port"})

? and !

Some functions end with a ? or a !. These are merely a naming standard. The question mark means the function returns a boolean, the exclamation mark means that the function will raise an error if it fails, instead of (usually) returning a tuple that must be handled with case:

case Tesla.get('http://example.com') do
    {:ok, response} -> IO.puts("success")
    {:error, error} -> IO.puts("Something went wrong")
end
Tesla.get!("http://example.com") # returns directly there response

Inline functions

Functions can be inlined:

def run([port, host]), do: run(port, host)

Private functions

Functions can be private, and that usually means the compiler will inline them at compile-time:

defp run(port, host), do: IO.puts("running!")

Pipelining

Functions can be chained together with the pipeline operator. The pipeline fills the first argument of the next function with the result of the previous. For example:

data =
  "https://api.example.com"
    |> Tesla.get!()
    |> Map.get(:body)
    |> Jason.decode!()

Pipeling makes for readable code, but requires extra thought in deciding how arrange functions.

Next

That covers the basic syntax of Elixir. In the third part, I will focus on writing Elixir applications and the available tooling.