Elixir

Learning for Datacurve.

https://hexdocs.pm/elixir/1.17.3/Kernel.html

Libraries

  • Mix
  • Phoenix (to deploy web apps)
  • Ecto (for actual interaction with the database)
  • Absinthe (for graphQL queries)

Why did we choose Elixir?

Some other options include:

  • Go
  • Rust
  • Node.js

Good concurrency.

Adapted very rapidly in telecomms, finance

IMMUTABLE

Elixir is an immutable language where nothing is shared by default.

Basic Types

These are the types

1          # integer
0x1F       # integer
1.0        # float, 64-bit precision.
true       # boolean
:atom      # atom / symbol
"elixir"   # string
[1, 2, 3]  # list
{1, 2, 3}  # tuple
  • atom is a constant whose value is its own name. Some other languages call these symbols. They are often useful to enumerate over distinct values, such as:
:apple
:orange
:watermelon

Ahh, in that sense, true is an atom

  • true, false, and nil are atoms that you don’t need to specify the : beforehand
true == :true

Because elixir is functional, you don't have these objects and types!!

This is how you define constants

@five_min_in_seconds 60 * 5

Lists and tuples

Data Structures

  • Lists - stored as a linkedlist
  • Tuple - stored contiguous in memory

Accessing elements for lists

list = [1, 2, 3]
hd(list)
# 1
tl(list)
# [2, 3]
Enum.at(list, 0) # Access the first element 
# 1 
Enum.at(list, 1)  # Access the second element 
# 2 Enum.at(list, 2)

Concatenation is done using ++ and --

[1, 2, 3] ++ [4, 5, 6] #[1, 2, 3, 4, 5, 6]
[1, true, 2, false, 3, true] -- [true, false] #[1, 2, 3, true]

For tuples

tuple = {:ok, "hello"}
# {:ok, "hello"}
elem(tuple, 1)
 # "hello"
tuple_size(tuple)
2

Basic rule:

  • If function is named size if the operation is in constant time (the value is pre-calculated)
  • If length, then linear time

Whenever you see a value in IEx and you are not quite sure what it is, you can use the i/1 to retrieve information about it:

i ~c"hello"
Term
  i ~c"hello"
Data type
  List
Description
  ...
Raw representation
  [104, 101, 108, 108, 111]
Reference modules
  List
Implemented protocols

Pattern Matching

Cannot skip this. Similar to python.

{a, b, c} = {:hello, "world", 42}

Use the pin operator ^ when you want to pattern match against a variable’s existing value rather than rebinding the variable.

iex> x = 1
1
iex> ^x = 2

case, cond and if

If any variable is declared or changed inside if, case, and similar constructs, the declaration and change will only be visible inside the construct. For example:

x = 1
1
if true do
  x = x + 1
end
2
# x is still 1 here

If you want to change a value, assign x to it at the end

x = if true do
  x + 1
else
  x
end

Instead of else if, elixir uses cond.

cond do
  2 + 2 == 5 ->
    "This will not be true"
  2 * 2 == 3 ->
    "Nor this"
  1 + 1 == 2 ->
    "But this will"
end
 
# returns "But this will"

case allows us to compare a value against many patterns until we find a matching one: (it’s like a switch statement)

x = 1
case x do
  ^x -> "This will match"
  _ -> "Will not match"
end

DANGER

Do NOT forget to add ^ in front, because that checks the value instead of reassigning it.

Anonymous functions

add = fn a, b -> a + b end
# check if add is a function that expects exactly 2 arguments
is_function(add, 2)
# true

Binaries and strings

If you want to see the exact bytes that a string would be stored in a file, a common trick is to concatenate the null byte <<0>> to it:

"hello" <> <<0>>
# <<104, 101, 108, 108, 111, 0>>
"hello" == <<104, 101, 108, 108, 111>>
# true
  • is_binary A binary always contains a complete number of bytes.

Bitstring

A bitstring is a contiguous sequence of bits in memory.

By default, 8 bits (i.e. 1 byte) is used to store each number in a bitstring, but you can manually specify the number of bits via a ::n modifier to denote the size in n bits, or you can use the more verbose declaration ::size(n):

<<42>> == <<42::8>>
true
<<3::4>>
# <<3::size(4)>>
<<0::1, 0::1, 1::1, 1::1>> == <<3::4>>

Any value that exceeds what can be stored by the number of bits provisioned is truncated:

<<1>> == <<257>>

A binary is a bitstring where the number of bits is divisible by 8.

Keyword lists and Maps

Keyword lists

list = [a: 1, b: 2]
# [a: 1, b: 2]
list ++ [c: 3]
# [a: 1, b: 2, c: 3]
[a: 0] ++ list
# [a: 0, a: 1, b: 2]
  • This is like a vector of pairs? (Yes, but remember that list is a linked list in elixir, as opposed to variable size array)

Here, the keys are atoms, so you don’t need to write the : in front yourself.

In case of duplicate keys, values added to the front are the ones fetched.

list = [a: 1, b: 2]
# [a: 1, b: 2]
new_list = [a: 0] ++ list
# [a: 0, a: 1, b: 2]
new_list[:a]
# 0

Keyword lists are important because they have three special characteristics:

  • Keys must be atoms.
  • Keys are ordered, as specified by the developer.
  • Keys can be given more than once.

Whenever you need to store key-value pairs, maps are the “go to” data structure in Elixir. A map is created using the %{}

map = %{:a => 1, 2 => :b}

Getting keys

map.name
# "John"
map.agee
# ** (KeyError) key :agee not found in: %{name: "John", age: 23}

Elixir developers typically prefer to use the map.key syntax and pattern matching instead of the functions in the Map module when working with maps because they lead to an assertive style of programming.

Compared to keyword lists, we can already see two differences:

  • Maps allow any value as a key.
  • Maps’ keys do not follow any ordering.

Modules and functions

Just like in python, you have modules, and functions within modules.

Write modules into files so they can be compiled and reused.

Use the defmodule keyword to define a module.

defmodule Math do
  def sum(a, b) do
    a + b
  end
end
 
Math.sum(1, 2)

Usually, 3 directories:

  • _build - contains compilation artifacts
  • lib - contains Elixir code (usually .ex files)
  • test - contains tests (usually .exs files)

We have def and defp (define private).

defmodule Math do
  def sum(a, b) do
    do_sum(a, b)
  end
 
  defp do_sum(a, b) do
    a + b
  end
end
 
IO.puts Math.sum(1, 2)    #=> 3
IO.puts Math.do_sum(1, 2) #=> ** (UndefinedFunctionError)

Enumerables and Streams

The Enum module provides a huge range of functions to transform, sort, group, filter and retrieve items from enumerables. It is one of the modules developers use frequently in their Elixir code. For a general overview of all functions in the Enum module, see the Enum cheatsheet.

https://hexdocs.pm/elixir/enum-cheat.html

Processes

Processes

  • The strong concurrency is achieved through the processes

alias, require, import and use

# Alias the module so it can be called as Bar instead of Foo.Bar
alias Foo.Bar, as: Bar
 
# Require the module in order to use its macros
require Foo
 
# Import functions from Foo so they can be called without the `Foo.` prefix
import Foo
 
# Invokes the custom code defined in Foo as an extension point
use Foo
alias

Aliases are frequently used to define shortcuts. In fact, calling alias without an :as option sets the alias automatically to the last part of the module name, for example:

alias Math.List

is the same as

alias Math.List, as: List

Note that alias is _lexically scoped.

use

The use macro is frequently used as an extension point.

Behind the scenes, use requires the given module and then calls the __using__/1 callback on it allowing the module to inject some code into the current context.

defmodule AssertionTest do
  use ExUnit.Case, async: true
 
  test "always pass" do
    assert true
  end
end

When you call use, you’re allowing the module you are “using” to add some functionality into your module. That module (which you’re using) typically has a __using__/1 macro that injects (i.e., automatically defines or adds) certain functions, behaviors, or setup into your module.

defmodule MyMacros do
  defmacro __using__(_) do
    quote do
      # Injecting a function
      def say_hello do
        IO.puts "Hello from MyMacros!"
      end
    end
  end
end

Module Attributes

Module attributes in Elixir serve three purposes:

  1. as module and function annotations
  2. as temporary module storage to be used during compilation
  3. as compile-time constants

It’s essentially like a const variable

You declare it like this (use an @, and then the name of the the attribute, followed by a space and the actual value of the attribute)

@alphabet "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
defmodule MyServer do
  @service URI.parse("https://example.com")
  IO.inspect @service
end

Structs

Structs are extensions built on top of maps that provide compile-time checks and default values.

Protocols

Protocols are a mechanism to achieve polymorphism in Elixir where you want the behavior to vary depending on the data type.