Elixir

Learning for Datacurve.

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

In Elixir, the convention of using an underscore (e.g., _params) is a way to signal that a variable is intentionally unused.

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.

I need to get a good understanding of how types work in elixir.

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

We can invoke anonymous functions by passing arguments to it. Note that a dot (.) between the variable and parentheses is required to invoke an anonymous function. Th

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

IO and the file system

Printing (like a cout)

IO.puts("hello world")

But really, the thing that you need is

IO.inspect(value)

Retrieving values (like a cin)

IO.gets("yes or no? ")
yes or no? yes
""yes\n""

File

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
require

Require imports the macros and functions in the module. You still need to use the module name.

import

Import is the same as require, except you don’t need to specify the module name when using the functions.

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)

Example use

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.

defprotocol Utility do
  @spec type(t) :: String.t()
  def type(value)
end
 
defimpl Utility, for: BitString do
  def type(_value), do: "string"
end
 
defimpl Utility, for: Integer do
  def type(_value), do: "integer"
end
Utility.type("foo")
"string"
Utility.type(123)
"integer"

Typespecs

You can define types in the spec. Because variables are dynamically typed.

  • mostly for documentation, but you can use this for type checking tools like dialyzer
defmodule StringHelpers do
  @typedoc "A word from the dictionary"
  @type word() :: String.t()

  @spec long_word?(word()) :: boolean()
  def long_word?(word) when is_binary(word) do
    String.length(word) > 8
  end
end

The spec tells us what the function

Operators

The pipe operator |, or |>.

  • This |> simply provides whatever data into the first argument of the function

Ranges

You can do something like (1…3)

Guards

You can have 1 liner function definitions like this

def process(:ok, result), do: "Success: #{result}"
defmodule URI.Parser do
  @doc "Defines a default port"
  @callback default_port() :: integer

  @doc "Parses the given URL"
  @callback parse(uri_info :: URI.t()) :: URI.t()
end

Implementing behaviours, that’s like the module Bar or Baz has some higher level behaviours.

defmodule Foo do
  @behaviour Bar
  @behaviour Baz
 
  # Will warn if neither Bar nor Baz specify a callback named bar/0.
  @impl true
  def bar(), do: :ok
 
  # Will warn if Baz does not specify a callback named baz/0.
  @impl Baz
  def baz(), do: :ok
end

https://hexdocs.pm/elixir/main/Module.html

But there’s other places?

I’m confused at the difference between using defprotocol, and then a defimpl, as opposed to using two defmodule, and then using @behaviour and @callback

defprotocol is a means to achieving polymorphism.

A behavior in Elixir defines a set of function signatures (callbacks) that a module must implement.

  • It’s an interface

  • defprotocol/defimpl is about polymorphism and dispatching based on data types.

  • @behaviour/@callback is about enforcing a contract that modules must follow, without polymorphism.

https://www.djm.org.uk/posts/elixir-behaviours-vs-protocols-what-is-the-difference/

  • Protocols handle polymorphism at the data/type level
  • Behaviours provide it at the module level.