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
- 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
Lists and tuples
Data Structures
- Lists - stored as a linkedlist
- Tuple - stored contiguous in memory
Accessing elements for lists
Concatenation is done using ++
and --
For tuples
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:
Pattern Matching
Cannot skip this. Similar to python.
Use the pin operator ^
when you want to pattern match against a variable’s existing value rather than rebinding the variable.
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:
If you want to change a value, assign x to it at the end
Instead of else if
, elixir uses cond
.
case
allows us to compare a value against many patterns until we find a matching one: (it’s like a switch statement)
DANGER
Do NOT forget to add
^
in front, because that checks the value instead of reassigning it.
Anonymous functions
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:
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)
:
<<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
- 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.
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 %{}
Getting keys
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.
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).
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
)
But really, the thing that you need is
Retrieving values (like a cin
)
File
alias, require, import and use
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:
is the same as
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.
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.
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
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.
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
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.
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.