Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Elixir School - Basics

elixir_school_basic.livemd

Elixir School - Basics

Basics

Elixir School Basics

Atoms

A constant whose name is its value

:foo

Equivalents:

  • JavaScript const s = Symbol("foo")
  • Ruby :foo
:foo
:foo == :bar

Booleans true and false are also atoms :true and :false

[is_atom(true), is_atom(false), is_boolean(true), is_boolean(false)]

Module names are also valid atoms

Boolean Operators

||, &&, !, and, or, not are the built-in boolean operators

-20 || true
42 && true
!42

Operators and, or, not must have a boolean as their first argument

true and 42
42 and true
not false

String Interpolation and Concatenation

first_name = "Charles"
middle_name = "Thomas"
last_name = "Roth"
full_name = "#{first_name} #{middle_name}" <> " #{last_name}" <> " III"

Collections

List

Collections of values which may include multiple types and non-unique values

Implemented as linked lists therefore prepend is O(1) and append is O(n)

list = [3.14, :pie, "Apple"]
# prepend (fast)
["π" | list]
# append (slow), concatenation
list ++ ["π"]
# list subtraction
list -- ["Apple"]
# head & tail
[head | tail] = list
h = hd(list)
t = tl(list)
[h, t] == [head, tail]

Tuples

Similar to List

  • Stored contiguously in memory
  • Modification is expensive (new tuple is copied to memory)

Very useful in “pattern matching”

tup_list = {3.14, :pie, "Apple"}

Keyword List

  • Two-element tuple whose first element is an atom
  • Keys are atoms, ordered and do not have to be unique
[first_name: "Charlie", last_name: "Roth"]
[{:first_name, "Charlie"}, {:last_name, "Roth"}]

Maps

  • key-value store
  • Keys can be of any type
  • Un-ordered
person = %{:first_name => "Charlie", :last_name => "Roth", :age => 26}
person[:first_name]

A Map can be updated with “map update” syntax

%{person | :first_name => "Charles"}

When modifying a map, with “map update” syntax, only keys that are present are allowed to be updated

%{person | :location => "Sweden"}

To add keys that are not currently in the Map, use the Map.put/3 function

Map.put(person, :location, "Sweden")

Enum

Enum module includes 70 functions to work with enumerables (Map, List, Keyword List)

Lazy enumeration can be achieved with the Stream module

Enum.all?(["foo", "bar", "hello"], fn s -> String.length(s) == 3 end)
Enum.any?(["foo", "bar", "hello"], fn s -> String.contains?(s, "oo") end)
Enum.each(["one", "two", "three"], fn s -> IO.puts(s) end)
Enum.map([1, 2, 3], fn n -> n * 2 end)
Enum.filter([0, 1, 2, 3, 4, 5], fn n -> rem(n, 2) == 0 end)
Enum.reduce([1, 2, 3, 4, 5], 0, fn n, acc -> acc + n end)
Enum.reduce(["3", "2", "1", "Lift Off!!"], "", fn s, acc -> acc <> " " <> s end)
Enum.sort([4, 2, 5, 3, 1])
Enum.sort([4, 2, 5, 3, 1], :desc)

The “Capture Operator” & can be used to represent an anonymous function

Arguments supplied to the anonymous function can be accessed through &1 ... &n where n represents the order of the arguments

Enum.reduce(["3", "2", "1", "Lift Off!!"], "", &amp;(&amp;2 <> " " <> &amp;1))

Pattern Matching

Allows matching of simple values, data structures and functions

Match Operator

In Elixir, the = sign is not the same as most programming languages. The = sign is actually an operator called the match operator. Behaves as an algebraic = sign and when the match is successful, it behaves as assignment.

The = turns the expression into an equation where the LHS must match the RHS. If the match succeeds, it returns the value of the equation

x = 1
1 = x
2 = x
list = [1, 2, 3]
[1, 2, 3] = list
[2, 3, 4] = list
[1 | tail] = list
{:ok, value} = {:ok, "Success"}
{:ok, value} = {:error, "Failed to read file"}

Pin Operator

The ^ operator matches on the existing value rather than rebinding to a new one

Keys in Maps and function clasues can use the ^ operator as well

x = 1
^x = 2
{x, ^x} = {2, 1}
x
key = "hello"
%{^key => "world"} = %{"hello" => "world"}
greeting = "Hello"

greet = fn
  ^greeting, name -> "Hi #{name}"
  greeting, name -> "#{greeting}, #{name}"
end
greet.("Hello", "Charlie")
greet.("God morgon", "Charlie")

Control Structures

Control structures in Elixir are implemented as functions such as if/2 where the first argument is the conditional to evaluate and the second argument is the continuation function. However, in practice they are used as macros and not the typical language constructs like in most programming languages.

Available control structures:

  • if/2
  • unless/2
  • case/2
  • cond/1
  • with/1
if String.valid?("Hello") do
  "Valid string :)"
else
  "Invalid string :("
end
unless is_integer(123) do
  "Not an integer"
end

case/2 is the same as Rust’s match operator

case {:ok, "Hello world"} do
  {:ok, msg} -> msg
  {:error} -> "Oops"
  _ -> "idk what is going on"
end
case {1, 2, 3} do
  {1, x, 3} when rem(x, 2) == 0 ->
    "Matched"

  _ ->
    "Second number is odd"
end

The with/2 function is useful when you want to perform something similar to nested case/2 functions but desire a more composable way to write these conditions

person = %{first: "Charles", last: "Roth"}

with {:ok, first} <- Map.fetch(person, :first),
     {:ok, last} <- Map.fetch(person, :last),
     do: last <> ", " <> first
person = %{first: "Charles"}

with {:ok, first} <- Map.fetch(person, :first),
     {:ok, last} <- Map.fetch(person, :last),
     do: last <> ", " <> first

Functions

Anonymous Functions

adder = fn a, b -> a + b end
adder.(2, 3)
adder_short = &amp;(&amp;1 + &amp;2)
adder_short.(4, 5)

Pattern matching can be used with function signatures as well

handle_result = fn
  {:ok, result} -> IO.puts("Handling...")
  {:ok, _} -> IO.puts("Handling some other stuff...")
  {:error} -> IO.puts("Error occurred")
end
some_result = 42
handle_result.({:ok, some_result})
handle_result.({:error})

Named Functions

defmodule Greeter do
  def greet(name) do
    "Hey, " <> name
  end

  def goodbye(name), do: "Good bye, " <> name
end
Greeter.greet("Charlie")
Greeter.goodbye("Charlie")

Functions have an arity, such as case/2, where /2 represents the number arguments the function accepts

This allows you to have several functions with the same name but with a different number of arguments

defmodule Greeter2 do
  def hello(), do: "Hello, anon"
  def hello(name), do: "Hello, " <> name
  def hello(name1, name2), do: "Hello, " <> name1 <> " and " <> name2
end
IO.puts(Greeter2.hello())
IO.puts(Greeter2.hello("Charlie"))
IO.puts(Greeter2.hello("Charlie", "eilrahC"))

Multiple functions with the same name but different arities is actually an example of pattern matching, a simple one that is. Pattern matching in function signatures can be more powerful than just the number of arguments, they can be matched by the contents of the arguments themselves

defmodule Greeter1 do
  def hello(%{name: person_name}) do
    IO.puts("Hello, " <> person_name)
  end

  def full_hello(%{name: person_name} = person) do
    IO.puts("Hello, " <> person_name)
    IO.inspect(person)
  end
end

Here the function Greeter1.hello/1 takes a Map as its argument. We then destructure this Map and extract the value at the key :name and assign that to the variable person_name. This assignment only happens if the function signature match is correct, else the result is an error

Greeter1.hello(%{name: "Charlie Roth"})
Greeter1.hello(%{name: "Miranda Nichols"})
# partial pattern match of the passed Map
Greeter1.hello(%{name: "Parker Roth", location: "Arizona", age: 21})
Greeter1.hello("Charlie Roth")
Greeter1.full_hello(%{name: "Parker Roth", location: "Arizona", age: 21})

Guards

Functions can have “attached” guard statements that will be checked after the function signature is matched

This allows you to have even more granular matching on which function to execute

defmodule Greeter3 do
  def hello(names) when is_list(names) do
    names |> Enum.join(", ") |> hello
  end

  def hello(name) when is_binary(name) do
    phrase() <> name
  end

  defp phrase, do: "Hello "
end
Greeter3.hello(["Charlie", "Miranda", "Parker"])

Function signatures can have default values for arguments when they are not supplied to the function

defmodule Greeter4 do
  def hello(name, lang \\ "en") do
    phrase(lang) <> name
  end

  defp phrase("en"), do: "Hello, "
  defp phrase("es"), do: "Hola, "
end
IO.puts(Greeter4.hello("Charlie"))
IO.puts(Greeter4.hello("Charlie", "es"))

Pipe Operator

Best practice when using the |> operator is when the arity of a function is more than 1, use parentheses. Otherwise just writing the function name like String.split is fine

"Charles Thomas Roth" |> String.split()
"Charles Thomas Roth" |> String.upcase() |> String.split()

Modules

Modules are Elixir’s way to organize functions into namespaces

There are some basic examples of modules above

Nested Modules

defmodule Example.Greetings do
  def morning(name) do
    "Good morning #{name}"
  end

  def evening(name) do
    "Good evening #{name}"
  end
end
Example.Greetings.morning("Charlie")

Module Attributes

Commonly used as constants that are scoped to the module they are within

Reservered module attributes in Elixir:

  • moduledoc - Documents the current module
  • doc - Documentation for functions and macros
  • behaviour - Use an OTP for user-defined behaviour
defmodule Example do
  @greeting "Hello"

  def greeting(name) do
    ~s(#{@greeting} #{name})
  end
end
Example.greeting("Charlie")

Structs

Special maps with a defined set of keys and default values

Defined within a module which it inherits the name from

defmodule Example.User do
  defstruct name: "Charlie", roles: []
end
%Example.User{}
%Example.User{name: "Miranda", roles: ["Physicist"]}
u = %Example.User{}
u = %{u | name: "Charles"}
u = %{u | roles: ["Programmer", "Guru"]}

Module Composition

The alias macro enables the reference and make use of one module inside of another

defmodule Sayings.Greeting do
  def basic(name), do: "Hello, " <> name
end

defmodule Sayings.Goodbye do
  def basic(name), do: "Goodbye, " <> name
end

defmodule Example do
  alias Sayings.Greetings

  def greeting(name), do: Greetings.basic(name)
end

When aliasing a module that has a possible conflict with the current module or another module, or you just want to shorten the referencing name

defmodule Example do
  alias Sayings.Greeting, as: Hi

  def print_msg(name), do: Hi.basic(name)
end

Since modules are namespaces and the author has the ability to “nest” modules, it is possible alias multiple modules from the “parent” modules

defmodule Example do
  alias Sayings.{Greeting, Goodbye}

  def print_msg(name) do
    name |> Greeting.basic() |> IO.puts()
    name |> Goodbye.basic() |> IO.puts()
  end
end
Example.print_msg("Charlie")

The import macro adds the functions of the module it is importing to the scope of the module

# last/1 is not in the scope of the program's module
last([1, 2, 3])
import List
last([1, 2, 3])

The require macro does the same thing as import except only for macros of the module it is referring to

The use macro can add functionality to a module that is using it

The module that is referenced by the use macro is required to have a callback __using__/1 defined

This will be called and add the functionality provided in this call back

This is a powerful feature of metaprogramming in Elixir but should be used wisely

Comprehensions

Comprehensions can often be used to produce more concise statements for Enum and Stream iteration

# lists
for x <- [1, 2, 3, 4, 5], do: x * x
# keyword lists
for {_key, val} <- [one: 1, two: 2, three: 3], do: val
# maps
for {k, v} <- %{"a" => "A", "b" => "B", "c" => "C"}, do: {k, v}
# binaries
for <>, do: <>
# multiple generators
for n <- [1, 2, 3, 4], times <- 1..n do
  String.duplicate("*", times)
end

“Filters” can be applied to comprehensions in a similar fashion to guards

import Integer
# all even numbers between 1 and 10
for x <- 1..10, is_even(x), do: x
# all even numbers, between 1 and 100, that are also a multiple of 3
for x <- 1..100,
    is_even(x),
    rem(x, 3) == 0,
    do: x

Listen comprehensions can also be useful to transform data

This can be accomplished by using :into

# create a map from a keyword list
for {k, v} <- [one: 1, two: 2, three: 3], into: %{}, do: {k, v}
# create a binary from a integer list
for c <- [72, 101, 108, 108, 111], into: "", do: <>