akrisanov
8/30/2018 - 8:47 PM

Elixir Random Notes

Elixir Random Notes

An important feature of Elixir is that any two types can be compared; this is particularly useful in sorting. We don’t need to memorize the sort order, but it is important to be aware of it:

number < atom < reference < function < port < pid < tuple < map < list < bitstring

Elixir implements list collections as linked lists. This means that accessing the list length is an operation that will run in linear time (O(n)). For this reason, it is typically faster to prepend than to append.

Head / Tail

hd [3.14, :pie, "Apple"]
tl [3.14, :pie, "Apple"]
[head | tail] = [3.14, :pie, "Apple"]

Tuples

Tuples are similar to lists, but are stored contiguously in memory. This makes accessing their length fast but modification expensive; the new tuple must be copied entirely to memory.

iex> {3.14, :pie, "Apple"}

iex> File.read("path/to/existing/file")
{:ok, "... contents ..."}
iex> File.read("path/to/unknown/file")
{:error, :enoent}

Keyword lists

iex> [foo: "bar", hello: "world"]
[foo: "bar", hello: "world"]
iex> [{:foo, "bar"}, {:hello, "world"}]
[foo: "bar", hello: "world"]

The three characteristics of keyword lists highlight their importance:

  • Keys are atoms.
  • Keys are ordered.
  • Keys may not be unique.

For these reasons, keyword lists are most commonly used to pass options to functions.

Maps

In Elixir, maps are the “go-to” key-value store. Unlike keyword lists, they allow keys of any type and are un-ordered.

iex> map = %{:foo => "bar", "hello" => :world}
iex> map[:foo]

iex> %{:foo => "bar", :foo => "hello world"}
%{foo: "hello world"}

iex> %{foo: "bar", hello: "world"} == %{:foo => "bar", :hello => "world"}
true

there is a special syntax for maps containing only atom keys:

iex> %{foo: "bar", hello: "world"} == %{:foo => "bar", :hello => "world"}

In addition, there is a special syntax for accessing atom keys:

iex> map.hello
"world"

Another interesting property of maps is that they provide their own syntax for updates:

iex> %{map | foo: "baz"}
%{foo: "baz", hello: "world"}

For a full list of functions visit the official Enum docs; for lazy enumeration use the Stream module.

Pin Operator

Lesson

The match operator performs assignment when the left side of the match includes a variable. In some cases this variable rebinding behavior is undesirable. For these situations we have the pin operator: ^. When we pin a variable we match on the existing value rather than rebinding to a new one.

iex> x = 1
1
iex> ^x = 2
** (MatchError) no match of right hand side value: 2
iex> {x, ^x} = {2, 1}
{2, 1}
iex> x
2

Elixir 1.2 introduced support for pins in map keys and function clauses.

Modules

Lesson

Mix

def deps do
  [
    {:phoenix, "~> 1.1 or ~> 1.2"},
    {:phoenix_html, "~> 2.3"},
    {:cowboy, "~> 1.0", only: [:dev, :test]},
    {:slime, "~> 0.14"}
  ]
end
$ mix deps.get

The current environment can be accessed using Mix.env. As expected, the environment can be changed via the MIX_ENVenvironment variable: $ MIX_ENV=prod mix compile

Sigils

Lesson

Documentation

Always document a module. If you do not intend to document a module, do not leave it blank. Consider annotating the module false:

@moduledoc false

Testing

  • assert
  • assert_raise
  • assert_received does not wait for messages, with assert_receive you can specify a timeout.
  • capture_io
  • capture_log
  • Use refute when you want to ensure a statement is always false.

Test setup

  • setup
  • setup_all
defmodule ExampleTest do
  use ExUnit.Case
  doctest Example

  setup_all do
    {:ok, recipient: :world}
  end

  test "greets", state do
    assert Example.hello() == state[:recipient]
  end
end

Mocking

The simple answer to mocking in Elixir is: don’t. You may instinctively reach for mocks but they are highly discouraged in the Elixir community and for good reason.

For a longer discussion there is this excellent article. The gist is, that instead of mocking away dependencies for testing (mock as a verb), it has many advantages to explicitly define interfaces (behaviors) for code outside your application and using Mock (as a noun) implementations in your client code for testing.

To switch the implementations in your application code, the preferred way is to pass the module as arguments and use a default value. If that does not work, use the built-in configuration mechanism. For creating these mock implementations, you don’t need a special mocking library, only behaviours and callbacks.