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
, andnil
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 artifactslib
- 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:
- as module and function annotations
- as temporary module storage to be used during compilation
- 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.