Elixir logo

Programación funcional con Elixir (1)

Por: Jhan S. Álvarez

VC@SOFT

Elixir logo
Elixir logo

1. Introducción

Elixir logo

1.1. ¿Qué es Elixir?

Lenguaje de programación

  • Funcional
  • Tipado dinámico
  • Extensible
  • Sintáxis simple, inspirada en Ruby
  • Entorno de ejecución BEAM (Erlang VM)
  • Ecosistema extenso en librerías y herramientas

1.1 ¿Qué es Elixir?

Elixir is a dynamic, functional language for building scalable and maintainable applications.

  • Inmutabilidad
  • Procesos livianos (green threads)
  • Concurrencia basada en mensajes → ❌ Deadlocks
  • Manejo de errores expresivo y sin excepciones
  • Reinicio de procesos fallidos
  • Code reloading
Elixir logo

1.2. La máquina virtual BEAM

  • Máquina abstracta que ejecuta código Erlang
  • Sucedió a otras implementaciones de Erlang
  • Componente ERTS (Erlang Runtime System, cuya implementación de referencia es Erlang/OTP)
  • Interpreta bytecode BEAM
  • Procesos: Unidades aisladas de ejecución en la VM
Elixir logo

1.3. Otros lenguajes BEAM

Elixir logo

1.3.1. LFE (Lisp Flavoured Erlang)

(defmodule tut1
  (export all))

(defun double (x)
  (* 2 x))
Elixir logo

1.3.2. Gleam

import gleam/string

pub fn greet(name: String) -> String {
  string.concat(["Hello ", name, "!"])
}
Elixir logo

1.3.3. Purerl

module Main where

import Prelude
import Effect (Effect)
import Effect.Console (log)

main :: Effect Unit
main = do
  log "Hello Erlang!"
Elixir logo

2. Sintáxis

defmodule Math do
  @moduledoc "Defines useful mathematical functions."

  @doc """
  Returns the sum of two numbers.

      iex> Math.sum(2, 3)
      5
  """
  def sum(a, b) do
    a + b
  end

  @doc "Returns the absolute value of a number."
  def abs(a) when is_integer(a) and a < 0, do: -a
  def abs(a) when is_integer(a), do: a
end
math.ex
Elixir logo

2.1. Shell interactivo

Elixir logo

2.1.1. Ayuda

Elixir logo

2.2. Programación funcional

Una expresión que no sea un literal (constante) o variable corresponde a un llamado a una función.

iex> 2 + 2
4
iex> Kernel.+(2, 2)
4
iex> unless 3**2 + 4**2 == 5**2 do
...>   "pythagoras was wrong"
...> end
nil
iex> unless(
...>   Kernel.==(
...>     Kernel.+(Kernel.**(3, 2), Kernel.**(4, 2)),
...>     Kernel.**(5, 2)
...>   ),
...>   [{:do, "pythagoras was wrong"}]
...> )
nil

2.2 Programación funcional

Todo código Elixir válido es una expresión, por lo tanto produce un valor.

iex> n = 47
47
iex> parity =
...>   if rem(n, 2) == 0 do
...>     "even"
...>   else
...>     "odd"
...>   end
"odd"
iex> parity
"odd"

2.2 Programación funcional

Todo valor es inmutable, en lugar de modificar un objeto se crea otro a partir del objeto previo.

iex> x = %{a: 1}
%{a: 1}
iex> Map.put(x, :b, 2)
%{a: 1, b: 2}
iex> x
%{a: 1}

2.2 Programación funcional

El operador |> permite encadenar funciones.

iex> (IO.gets("enter a sentence: ")
...> |> String.split()  # la expresión anterior pasa a ser el primer argumento
...>        # entonces equivale a: String.split(IO.gets("enter a sentence: "))
...> |> IO.inspect()        # imprimir una representación del valor (palabras)
...> |> Enum.map(&String.length/1)
...> |> Enum.sum())
enter a sentence: this is a sentence
["this", "is", "a", "sentence"]
15
Elixir logo

2.3. Pattern matching

[Pattern matching] allows us to assert on the shape or extract values from data structures.

2.3 Pattern matching

iex> x = 0
0
iex> x = 1                      # "reasignar" un mismo identificador
1
iex> x
1
iex> y = {1, 2}                 # tupla
{1, 2}
iex> {^x, z} = y                # se hace match con el valor actual de x e y
{1, 2}
iex> z
2
iex> [x | xs] = [1, 4, 9, 16]   # lista, y match con _cabeza_ y _cola_
[1, 4, 9, 16]
iex> {x, xs}
{1, [4, 9, 16]}
iex> [_, 9, _] = xs             # omitimos partes con _
[4, 9, 16]
iex> match?([5, _, _], xs)      # existe o no match?
false
iex> [4, x] = xs                # la estructura no es la misma
... (MatchError) ...
iex> [4, x | _] = xs; x         # tenemos 2 o más elementos
9
iex> [4, 8, x] = xs             # el elemento 1 no corresponde
... (MatchError) ...

2.3 Pattern matching

Ejemplo: IO y sistema de archivos
iex> IO.puts("hello")
hello
:ok
iex> :ok = IO.puts("hello")     # match con un átomo, la operación fue correcta?
hello
:ok
iex> {:ok, file} = File.open("/etc/passwd", [:read])
{:ok, #PID<...>}
iex> {:ok, user_info} =
...>   case IO.read(file, :line) do
...>     user_info when is_binary(user_info) -> {:ok, user_info}
...>     :eof -> {:ok, nil}
...>     {:error, _} = error -> error
...>   end
{:ok, "root:x:0:0::/root:/bin/bash\n"}
iex> :ok = File.close(file)
:ok
Elixir logo

2.4. Tipos y estructuras de datos

Elixir logo

2.4.1. Tipos numéricos

iex> 1
1
iex> 10_000
10000
iex> 0.1
0.1
iex> :math.pi()
3.141592653589793
iex> is_number(-0.5)
true
iex> is_integer(3.14)
false
iex> is_float(:math.pi())
true
iex> 1 / 2
0.5
iex> div(5, 2)
2
iex> rem(23, 7)
2
Elixir logo

2.4.2. Tipo booleano

iex> 1 == 1.0
true
iex> 1 === 1.0
false
iex> true
true
iex> false
false
iex> 1 !== 1.0 === true
true
iex> is_boolean(:yes) === false
true
iex> IO.inspect(true) || IO.inspect(false)
true
true
iex> IO.inspect(false) && IO.inspect(true)
false
false
Elixir logo

2.4.3. Strings

iex> "ABC"
"ABC"
iex> """
...> multiline
...> string
...> """
"multiline\nstring\n"
iex> ~s(Perl-like string delimiters)
"Perl-like string delimiters"
iex> String.downcase("Ártico") |> IO.inspect() |> String.length()
"ártico"
6
iex> "AaBbCc" =~ "aBbC"
true
iex> "AaBbCc" =~ ~r/^[abc]+$/i
true
iex> to_string(1.5)
"1.5"
iex> IO.puts("\\n ascii code is #{?\n}. π = #{math.pi()}")
\n ascii code is 10. π = 3.141592653589793
:ok
Elixir logo

2.4.4. Átomos

iex> is_atom(true)
true
iex> is_atom(false)
true
iex> is_atom(nil)
true
iex> :ok
:ok
iex> String.to_atom("error")
:error
iex> :"🤓" |> IO.inspect() |> Atom.to_string()
:"🤓"
"🤓"
iex> is_atom(String)
true
iex> :erlang.display(String)
Elixir.String
true
iex> :erlang.display(Elixir)
Elixir
true
Elixir logo

2.4.5. Tuplas

[Tuples are] fixed-size containers for multiple elements.

iex> t1 = {:ok, "result"}
{:ok, "result"}
iex> elem(t1, 0)
:ok
iex> put_elem(t1, 1, "result_updated")
{:ok, "result_updated"}
iex> {}
{}
iex> {:a, :b, {:c, :d}} |> IO.inspect() |> tuple_size()
{:a, :b, {:c, :d}}
3
iex> File.open("/etc/shadow", [:read]) |> IO.inspect() |> elem(1)
{:error, :eacces}
:eacces
iex> {:ok, _} = Task.start(fn ->
...>   Process.sleep(1000)
...>   IO.puts("hello from process #{inspect(self())}")
...> end)
{:ok, #PID<0.124.0>}
hello from process #PID<0.124.0>
Elixir logo

2.4.6. Listas

Linked lists hold zero, one, or more elements in the chosen order. [Lists] are internally represented in pairs containing the head and the tail of a list.

iex> []
[]
iex> [1, "dos", :tres, 4.0]
[1, "dos", :tres, 4.0]
iex> [1 | []]
[1]
iex> [1 | [2 | []]]
[1, 2]
iex> [x | xs] = [1, 2, 3, 4, 5]
[1, 2, 3, 4, 5]
iex> {x, xs}
{1, [2, 3, 4, 5]}
iex> hd(xs)
2
iex> tl(xs)
[3, 4, 5]

2.4.6 Listas

Patterns
iex> numbers = [1, 2, 3, 4]
[1, 2, 3, 4]
iex> [] = numbers
... (MatchError) ...
iex> [_, _] = numbers
... (MatchError) ...
iex> [_ | _] = numbers
[1, 2, 3, 4]
iex> [_, _ | _] = numbers
[1, 2, 3, 4]

2.4.6 Listas

Modificación
iex> xs = []
[]
iex> xs = ["hello" | xs]
["hello"]
iex> xs = xs ++ ["world", "!"]
["hello", "world", "!"]
iex> List.delete(xs, "world")
["hello", "!"]
iex> ys = List.insert_at(xs, 1, "there")
["hello", "there", "world", "!"]
iex> List.zip([List.delete_at(ys, -1), tl(ys)])
{{"hello", "there"}, {"there", "world"}, {"world", "!"}]

2.4.6 Listas

Enumerables
iex> odds_r = 1..10//2
1..10//2
iex> evens_r = 2..10//2
2..10//2
iex> odds = Enum.to_list(odds_r)
[1, 3, 5, 7, 9]
iex> Enum.each(odds, fn x -> IO.puts("#{x}^#{x} = #{x ** x}") end)
1^1 = 1
3^3 = 27
...
9^9 = 387420489
:ok
iex> Enum.map(evens_r, &rem(&1, 3))
[2, 1, 0, 2, 1]
iex> randoms = Enum.map(1..10, fn _ -> :rand.uniform(10) end)
[1, 9, 1, 8, 3, 4, 10, 5, 5, 2]
iex> Enum.sort(randoms) |> Enum.reverse()
[10, 9, 8, 5, 5, 4, 3, 2, 1, 1]
iex> Enum.uniq(randoms)
[1, 9, 8, 3, 4, 10, 5, 2]
iex> Enum.at(randoms, 4)
3
Elixir logo

2.4.7. Mapas

Maps are the key-value data structure in Elixir. Key-value pairs in a map do not follow any order.

iex> user = %{first_name: "John", last_name: "Doe"}
%{first_name: "John", last_name: "Doe"}
iex> user.first_name
"John"
iex> user.second_name
... (KeyError) ...
iex> user[:last_name]
"Doe"
iex> user[:second_name]
nil
iex> Map.get(user, :first_name)
"John"
iex> Map.get(user, :second_name)
nil
iex> Map.fetch(user, :last_name)
{:ok, "Doe"}
iex> Map.fetch(user, :second_name)
:error
iex> Map.fetch!(user, :first_name)
"John"
iex> Map.fetch!(user, :second_name)
... (KeyError) ...

2.4.7 Mapas

Sintáxis y claves
iex> user = %{first_name: "Jane", last_name: "Doe"}
%{first_name: "Jane", last_name: "Doe"}
iex> %{:first_name => _, :last_name => _} = user
%{first_name: "Jane", last_name: "Doe"}
iex> m1 = %{"name" => "map", "delimiter" => "%{...}"}
%{"delimiter" => "%{...}", "name" => "map"}
iex> m2 = %{user => {:map, Atom}, m1 => {:map, String}}
%{
  %{first_name: "Jane", last_name: "Doe"} => {:map, Atom},
  %{"delimiter" => "%{...}", "name" => "map"} => {:map, String}
}
iex> m3 = %{{:k1_1, :k1_2} => :v1, {:k2_1, :k2_2} => :v2}
%{{:k1_1, :k1_2} => :v1, {:k2_1, :k2_2} => :v2}
iex> is_map(m3)
true
iex> is_map_key(m3, {:k1_1, :k1_2})
true
iex> is_map_key(m3, {:k1_1, :k2_1})
false

2.4.7 Mapas

Patterns
iex> kz_cities = %{"Almaty" => 1_328_362, "Shymkent" => 454_583, "Karaganda" => 451_800}
%{"Almaty" => 1328362, "Karaganda" => 451800, "Shymkent" => 454583}
iex> %{} = kz_cities
%{"Almaty" => 1328362, "Karaganda" => 451800, "Shymkent" => 454583}
iex> %{"Shymkent" => shymkent} = kz_cities
%{"Almaty" => 1328362, "Karaganda" => 451800, "Shymkent" => 454583}
iex> shymkent
454583
iex> %{"Shymkent" => _, "Taraz" => taraz} = kz_cities
... (MatchError) ...
iex> players_by_country = %{
...>   co: [{"Hermes Ibáñez", "coplayer104"}, {"Daniela López", "coplayer042"}],
...>   ve: [{"José Martínez", "veplayer291"}, {"Andrés González", "veplayer091"}],
...>   es: [{"Rubén Armadillo", "esplayer012"}, {"Rosa Guerrero", "esplayer053"}]
...> }
%{...}
iex> %{es: [{esp0_name, esp0_user} | _], co: [_, {_, cop1_user} | _]} = players_by_country
%{...}
iex> [esp0_name, esp0_user, cop1_user] |> Enum.each(&IO.inspect/1)
"Rubén Armadillo"
"esplayer012"
"coplayer042"
:ok

2.4.7 Mapas

Modificación
iex> m1 = %{a: 1, b: 2}
%{a: 1, b: 2}
iex> %{m1 | a: 3}
%{a: 3, b: 2}
iex> %{m1 | c: 4}
... (KeyError) ...
iex> Map.put(m1, :c, 4)
%{a: 1, b: 2, c: 4}
iex> Map.update(m1, :a, 1, fn x -> x + 2 end) # o &(&1 + 2)
%{a: 3, b: 2}
iex> Map.update(m1, :c, 3, &(&1 + 1)) |> Map.delete(:a)
%{b: 2, c: 3}
iex> Map.merge(m1, %{b: 3, c: 4})
%{a: 1, b: 3, c: 4}
Elixir logo

2.4.8. Keyword lists (Erlang proplists)

(…) a list that consists exclusively of two-element tuples. The first element (…) is known as the key, and it must be an atom. The second element, known as the value, can be any term.

iex> [{:a, 1}, {:b, 2}]
[a: 1, b: 2]
iex> [{:b, 2}, {a: 1}]
[b: 2, a: 1]
iex> kw1 = [a: 1, b: 2]
[a: 1, b: 2]
iex> kw2 = [b: 2, a: 1]
[b: 2, a: 1]
iex> [b: b, a: a] = kw1
... (MatchError) ...
iex> [b: b, a: a] = kw2
[b: 2, a: 1]
iex> {a, b}
{1, 2}

2.4.8 Keyword lists (Erlang proplists)

Lectura de valores
iex> query = [table: "customers", limit: 10, sort: :customer_id]
[table: "customers", limit: 10, sort: :customer_id]
iex> query.limit
... (KeyError) ...
iex> query[:limit]
10
iex> query[:group_by]
nil
iex> Keyword.get(query, :sort)
:customer_id
iex> Keyword.fetch(query, :table)
{:ok, "customers"}
iex> Keyword.fetch!(query, :where)
... (KeyError) ...

2.4.8 Keyword lists (Erlang proplists)

Modificación
iex> grep = [extended: true, ignore_case: false, word: true, pattern: "username", pattern: "address"]
[...]
iex> grep[:pattern]
"username"
iex> Keyword.get_values(grep, :pattern)
["username", "address"]
iex> Enum.filter(grep, fn {k, _} -> k == :pattern end)
[pattern: "username", pattern: "address"]
iex> Enum.filter(grep, fn {_, v} -> is_boolean(v) end)
[extended: true, ignore_case: true, word: true]
iex> Keyword.put(grep, :pattern, "city")
[..., word: true, pattern: "city"]
iex> Keyword.update(grep, :extended, false, &(not IO.inspect(&1)))
true
[extended: false, ignore_case: true, ...]
iex> grep ++ [pattern: "website", extended: false]
[extended: true, ..., pattern: "address", pattern: "website", extended: false]
iex> Keyword.merge(grep, [pattern: "country", extended: false])
[ignore_case: true, word: true, pattern: "country", extended: false]
Elixir logo

2.4.9. Estructuras

A struct is a tagged map that allows developers to provide default values for keys.

defmodule User do
  defstruct [
    :first_name,
    :last_name,
    :username,
    :password,
    :birthday,
    logged_in: false,
    avatar: "file://./priv/img/avatar/default.jpg"
  ]
end
user.ex

2.4.9 Estructuras

iex> default = %User{}
%User{
  first_name: nil,
  last_name: nil,
  username: nil,
  password: nil,
  birthday: nil,
  logged_in: false,
  avatar: "file://./priv/img/avatar/default.jpg"
}
iex> :erlang.display(default)
#{'__struct__'=>'Elixir.User',avatar=><<"file://./priv/img/avatar/default.jpg">>,birthday=>nil,first_name=>nil,last_name=>nil,logged_in=>false,password=>nil,username=>nil}
true
iex> default.avatar
"file://./priv/img/avatar/default.jpg"
iex> default[:avatar]
... (UndefinedFunctionError) ...
iex> %User{avatar: avatar} = default
%User{...}
iex> avatar
"file://./priv/img/avatar/default.jpg"
iex> is_struct(default)
true

2.4.9 Estructuras

Struct vs. Map
iex> john = %User{first_name: "John", last_name: "Doe", username: "johndoe"}
%User{...}
iex> john_map = Map.from_struct(john)
%{
  avatar: "file://./priv/img/avatar/default.jpg",
  birthday: nil,
  ...
}
iex> %{} = john
%User{...}
iex> %User{} = john_map
... (MatchError) ...
iex> john_init = %{first_name: "John", second_name: "Edward"}
%{first_name: "John", second_name: "Edward"}
iex> struct(User, john_init)
%User{
  first_name: "John",
  last_name: nil,
  ...
}
iex> struct!(User, john_init)
... (KeyError) ...

2.4.9 Estructuras

Modificación
iex> default = %User{}
%User{...}
iex> %User{default | first_name: "Jane", last_name: "Doe"}
%User{
  first_name: "Jane",
  last_name: "Doe",
  username: nil,
  ...
}
iex> Map.put(default, :username, "janedoe")
%User{
  ...,
  username: "janedoe",
  ...
}
iex> Map.delete(default, :first_name) # ❌
%{...}
iex> Map.put(default, :second_name, nil) # ❌
%{...}
Elixir logo

2.4.10. Funciones

defmodule Complex do
  defstruct [re: 0, im: 0]

  def sum(%__MODULE__{} = z, %__MODULE__{} = w) do
    %__MODULE__{re: z.re + w.re, im: z.im + w.im}
  end

  def product(%__MODULE__{} = z, %__MODULE__{} = w) do
    re = z.re * w.re - z.im * w.im
    im = z.re * w.im + z.im * w.re
    %__MODULE__{re: re, im: im}
  end
end
complex.ex

2.4.10 Funciones

Notación capture &module.function/arity
iex> complex_sum = &Complex.sum/2
&Complex.sum/2
iex> z = %Complex{re: 0.5, im: -0.5}
%Complex{re: 0.5, im: -0.5}
iex> w = %Complex{re: 2, im: -1.5}
%Complex{re: 2, im: -1.5}
iex> Complex.sum(z, w)
%Complex{re: 2.5, im: -2.0}
iex> complex_sum.(z, w)
%Complex{re: 2.5, im: -2.0}
iex> is_function(complex_sum)
true
iex> is_function(complex_sum, 2)
true
iex> complex_product = &Complex.product/2
#Function<...>
iex> is_function(complex_product, 3)
false

2.4.10 Funciones

Notación capture con argumentos
iex> complex_conjugate = &%Complex{re: &1.re, im: -&1.im}
#Function<...>
iex> z = %Complex{re: 0.125, im: -0.875}
%Complex{re: 0.125, im: -0.875}
iex> complex_conjugate.(z)
%Complex{re: 0.125, im: 0.875}
iex> w = %Complex{re: 0.0, im: 1.0}
%Complex{re: 0.0, im: 1.0}
iex> [^z, ^w] = [z, w] |> Enum.map(complex_conjugate) |> Enum.map(complex_conjugate)
[%Complex{re: 0.125, im: -0.875}, %Complex{re: 0.0, im: 1.0}]

2.4.10 Funciones

Notación fn lambda o función anónima
iex> import Complex
Complex
iex> complex_dot_product = fn {a0, a1}, {b0, b1} ->
...>   sum(product(a0, b0), product(a1, b1))
...> end
#Function<...>
iex> vz = {%Complex{re: 0.5, im: 0.5}, %Complex{re: -0.5, im: -0.5}}
{%Complex{re: 0.5, im: 0.5}, %Complex{re: -0.5, im: -0.5}}
iex> vw = {%Complex{re: 1.0, im: -1.0}, %Complex{re: -1.0, im: 0.0}}
{%Complex{re: 1.0, im: -1.0}, %Complex{re: -1.0, im: 0.0}}
iex> complex_dot_product.(vz, vw)
%Complex{re: 1.5, im: 0.5}
Elixir logo

2.4.11. Otros tipos

  • Binarios: Strings, cadenas de bits. <<0xCA, 0xFE, 0xBA, 0xBE>>; <<b2::2, _::6>> = 0xBE
  • Charlists: Listas de caracteres. 'abc' = [0x61, 0x62, 0x63]
  • Referencias: make_ref/0
  • Identificador de procesos: self/0; send/2; Process.list/0
Elixir logo

2.5. Expresiones

Estructuras sintácticas que al evaluarse producen un valor. En Elixir toda estructura sintáctica válida es una expresión, y toda expresión distinta de un valor literal (constante) o una variable se traduce en un llamado a una función. Repasamos algunas funciones particulares que se asemejan a declaraciones o estructuras de control de otros lenguajes de programación.

Muchas de estas funciones son en realidad macros que se traducen en otras funciones llamadas formas especiales que en realidad no son macros ni funciones Erlang, sino que son directamente interpretadas por el compilador.

2.5 Expresiones

Estas funciones y macros predefinidos e importados por defecto se encuentran y documentan en los módulos Kernel y Kernel.SpecialForms, el último corresponde a todas las formas especiales definidas por Elixir pero solo se ha definido para documentar estas formas. Los siguientes son algunos macros y funciones definidos en el módulo Kernel.

Elixir logo

2.5.1. Operadores

Aritméticos
iex> +1
1
iex> -1
-1
iex> 1 + 1
2
iex> 2 - 1
1
iex> 1.5 * 2.0
3.0
iex> 3 / 2
1.5
iex> div(13, 4)
3
iex> rem(13, 4)
1
iex> 4 ** 4
256

2.5.1 Operadores

Strings
iex> "AB" <> "C"
"ABC"
iex> first_name = "John"
"John"
iex> last_name = "Doe"
"Doe"
iex> "Dr. " <> first_name <> " " <> last_name
"Dr. John Doe"
iex> "Dr. #{first_name} #{last_name}"
"Dr. John Doe"

2.5.1 Operadores

Listas
iex> [] ++ [1, 2, 3]
[1, 2, 3]
iex> [0, 1] ++ [2, 3]
[0, 1, 2, 3]
iex> [0, 0, 1, 0, 1, 0, 0, 1] -- [0, 0, 0, 1, 1]
[0, 0, 1]
iex> [1, 2, 3, 4, 5, 6] -- [2, 2, 4, 4]
[1, 3, 5, 6]
iex> 0 in [3, 2, 1]
false
iex> 10 in 0..10
true
iex> {:a, 1} in %{a: 1}
true
iex> {:a, 1} in [a: 2, b: 2]
false

2.5.1 Operadores

Rangos
iex> 1..10 |> IO.inspect() |> Enum.to_list()
1..10
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
iex> 1..10 |> Map.to_list()
[__struct__: Range, first: 1, last: 10, step: 1]
iex> String.slice("hello", ..)
"hello"
iex> powers = [1, 4, 9, 16, 25, 36]
[1, 4, 9, 16, 25, 36]
iex> Enum.slice(powers, 1..4)
[4, 9, 16, 25]
iex> Enum.slice(powers, 1..10//2)
[4, 16, 36]

2.5.1 Operadores

Comparaciones
iex> 1 == 1.0
true
iex> 1 === 1.0
false
iex> 1 != 1.0
false
iex> 1 !== 1.0
true
iex> 1 > 2
false
iex> 1 < 2
true
iex> 2 >= 2
true
iex> 3.5 <= 3.0
false

2.5.1 Operadores

Operadores lógicos
iex> IO.inspect(nil) || IO.inspect(true)
nil
true
true
iex> IO.inspect(nil) && IO.inspect(true)
nil
nil
iex> nil or false
... (BadBooleanError) ...
iex> IO.inspect(true) and IO.inspect(false)
true
false
false
iex> IO.inspect(true) or IO.inspect(false)
true
true
iex> not nil
... (ArgumentError) ...
iex> not true
false
iex> [true, false, nil, "", 0] |> Enum.map(&!/1)
[false, true, true, false, false]
Elixir logo

2.5.2. Definiciones

# definir módulo
defmodule Math do
  # anidar módulo -> Math.Complex
  defmodule Complex do
    # define una estructura
    defstruct [re: 0.0, im: 0.0]
  end

  # definir función pública
  def abs(x) when sign(x) === -1, do: -x
  def abs(x) when sign(x) in [0, 1], do: x

  # definir función privada
  defp sign(x) when is_number(x) and x < 0, do: -1
  defp sign(x) when x == 0, do: 0
  defp sign(x) when is_number(x), do: 1
end

2.5.2 Definiciones

  • defdelegate/2: Delega una función a otro módulo.
  • defexception/1: Define un tipo de excepción.
  • defprotocol/2: Define un protocolo.
  • defimpl/3: Implementa un protocolo.
  • defmacro/2: Define un macro público.
  • defmacrop/2: Define un macro privado.
Elixir logo

2.5.3. then/2

iex> then(5, &(&1 * 2))
10
iex> (IO.gets("your name: ")
...> |> String.trim_trailing()
...> |> then(&"hello, #{&1}!")
...> |> IO.puts())
your name? Esteban
hello, Esteban
:ok
iex> (1..100
...> |> Enum.split_with(&(rem(&1, 2) === 0))
...> |> IO.inspect()
...> |> then(fn {evens, odds} -> Enum.sum(evens) - Enum.sum(odds) end))
{[2, 4, 6, ...],
 [1, 3, 5, ...]}
50
Elixir logo

2.5.4. if/2

Verifica valores truthy, diferentes a false y nil
iex> ({n, ""} = IO.gets("enter a number: ")
...> |> String.trim_trailing()
...> |> Integer.parse())
enter a number: 63
63
iex> if(rem(n, 2) === 0, do: "even", else: "odd")
"odd"
iex> if rem(n, 2) === 0 do
...>   "even"
...> else
...>   "odd"
...> end
"odd"
iex> nil
nil
iex> |> if do; "truthy"; else; "falsy"; end
"falsy"
iex> |> String.length()
5
iex> |> then(&if(&1 === 0, do: :empty))
nil

2.5.4 if/2

iex> Enum.each(1..30, fn n ->
...>   if rem(n, 15) === 0 do
...>     "FizzBuzz"
...>   else
...>     if rem(n, 3) === 0 do
...>       "Fizz"
...>     else
...>       if rem(n, 5) === 0 do
...>         "Buzz"
...>       else
...>         Integer.to_string(n)
...>       end
...>     end
...>   end
...> end)
1
2
Fizz
4
Buzz
...
FizzBuzz

2.5.4 if/2

iex> Enum.each(
...>   1..30,
...>   &(if(
...>       rem(&1, 15) === 0,
...>       do: "FizzBuzz",
...>       else:
...>         if(
...>           rem(&1, 3) === 0,
...>           do: "Fizz",
...>           else:
...>             if(
...>               rem(&1, 5) === 0,
...>               do: "Buzz",
...>               else: Integer.to_string(&1)
...>             )
...>         )
...>     )
...>     |> IO.puts())
...> )
1
2
Fizz
4
Buzz
...
FizzBuzz
Elixir logo

2.5.5. unless/2

if not ...
iex> people = %{"bob" => "bob@example.com", "alice" => "alice@example.com"}
%{"bob" => "bob@example.com", "alice" => "alice@example.com"}
iex> [nil, "", "alice", "john"] |> Enum.map(fn name ->
...>   unless is_nil(name) or name == "" do
...>     IO.puts("searching #{name}")
...>     people[name]
...>   end
...> end)
searching alice
searching john
[nil, nil, "alice@example.com", nil]
iex> "" |> unless(do: "falsy", else: "truthy")
"truthy"
Elixir logo

2.5.6. match?/2

Verifica un patrón sin asignación de variables
iex> match?(a, {:ok, "result"})
warning: ...
true
iex> match?({:ok, result}, {:ok, "result"})
warning: ...
true
iex> result
... (CompileError) ...
iex> match?([_ | _], [])
false
iex> match?(%{a: _, b: _}, %{c: 3, b: 2, a: 1})
true
iex> match?(%{c: _}, %{a: 1, b: 2})
false
iex> match?({:ok, result} when is_integer(result), {:ok, "result"})
false
Elixir logo

2.5.7. raise

Lanza una excepción
iex> 1 / 0
... (ArithmeticError) ...
iex> %ArithmeticError{}
%ArithmeticError{message: "bad argument in arithmetic expression"}
iex> %ArithmeticError{} |> Map.to_list()
[
  __exception__: true,
  __struct__: ArithmeticError,
  message: "bad argument in arithmetic expression"
]
iex> raise ArithmeticError
... (ArithmeticError) bad argument in arithmetic expression
...
iex> raise ArithmeticError, message: "my message"
... (ArithmeticError) my message
...
iex> raise %ArithmeticError{message: "my struct message"}
... (ArithmeticError) my struct message
...
iex> ArithmeticError.message(%ArithmeticError{})
"bad argument in arithmetic expression"

2.5.7 raise

iex> a = -1
-1
iex> :math.sqrt(a)
... (ArithmeticError) ...
iex> try do
...>   {:ok, :math.sqrt(a)}
...> rescue
...>   ArithmeticError ->
...>     :error
...>   exception ->
...>     IO.puts("oops!")
...>     reraise exception, __STACKTRACE__
...> end
:error
Elixir logo

2.5.8. throw/1

iex> name = "Ae4eA"
"Ae4eA"
iex> try do
...>   name = String.trim(name) |> String.capitalize()
...>   unless String.length(name) <= 16, do: throw({:error, :name_too_long})
...>   unless name =~ ~r/^[A-Z][a-z]*$/, do: throw({:error, :invalid_chars})
...>   {:ok, name}
...> catch
...>   {:error, error} -> error
...> end
:invalid_chars
Elixir logo

2.5.9. use

Introduce el código (AST) generado por el macro __using__/1 del módulo indicado, opcionalmente un keyword list permite pasar opciones al macro.

defmodule HelloRouter do
  use Plug.Router

  plug :match
  plug :dispatch

  get "/hello" do
    name = conn.params[:name] || "world"
    send_resp(conn, 200, "hello, #{name}!")
  end

  match _ do
    send_resp(conn, 404, "ENOENT")
  end
end
hello_router.ex

2.5.9 use

defmodule Plug.Router do
  # ...

  defmacro __using__(opts) do
    quote location: :keep do
      import Plug.Router
      @plug_router_to %{}
      @before_compile Plug.Router

      use Plug.Builder, unquote(opts)

      # ...
    end
  end

  # ...
end
plug/router.ex

2.5.9 use

Expansión inicial del macro use
defmodule HelloRouter do
  import Plug.Router
  @plug_router_to %{}
  @before_compile Plug.Router

  use Plug.Builder, []

  # ...

  plug :match
  plug :dispatch

  # ...
end
Elixir logo

2.5.10. |>/2

defmodule NaivePipe do
  defmacro arg |> {name, meta, args} do
    {name, meta, [arg | args]}
  end
end
naive_pipe.ex

2.5.10 |>/2

iex> import Kernel, except: ["|>": 2]
Kernel
iex> 1..100 |> Enum.length()
... (CompileError) ...: undefined function |>/2 ...
iex> import NaivePipe
NaivePipe
iex> (1..100
...> |> Enum.map(&(&1 ** 2 |> Integer.digits() |> Enum.sum()))
...> |> Enum.sum())
1480
Elixir logo

2.5.11. Sigils

Sigils are useful to encode text with their own escaping rules, such as regular expressions, datetimes, and others.

iex> ~D[2023-06-09] |> IO.inspect() |> Map.to_list()
~D[2023-06-09]
[__struct__: Date, calendar: Calendar.ISO, day: 9, month: 6, year: 2023]
iex> ~T[20:53:38.408] |> IO.inspect() |> Map.to_list()
[
  __struct__: Time,
  calendar: Calendar.ISO,
  hour: 20,
  microsecond: {408000, 3},
  minute: 53,
  second: 38
]
iex> "0xcafebabe" =~ ~r/^0x[0-9a-f]+$/i
true
iex> last_name = "doe"
"doe"
iex> "John Jr. Doe" =~ ~r/\b#{last_name}\b/i
true
iex> "b###c" =~ ~R/^[a-z]#{2,}+[a-z]$/
true

2.5.11 Sigils

iex> (Kernel.__info__(:macros)
...> |> Keyword.keys()
...> |> Enum.uniq()
...> |> Enum.map(&Atom.to_string/1)
...> |> Enum.filter(&String.starts_with?(&1, "sigil_")))
["sigil_C", "sigil_D", "sigil_N", "sigil_R", "sigil_S", "sigil_T", "sigil_U",
  "sigil_W", "sigil_c", "sigil_r", "sigil_s", "sigil_w"]
iex> h(sigil_w)
Handles the sigil ~w for list of words.

It returns a list of "words" split by whitespace. Character unescaping and
interpolation happens for each word.

...

## Examples

  iex> ~w(foo #{:bar} baz)
  ["foo", "bar", "baz"]

...
Elixir logo

2.6. Formas especiales

El concepto special form es frecuente en el contexto de la familia de lenguajes funcionales LISP. Por ejemplo, en el estándar ANSI de Common Lisp se define como una forma (unidad de evaluación o ejecución de un programa), que tiene una sintáxis y/o reglas de evaluación especiales que modifican el entorno de ejecución y/o el flujo de control. Algunas formas especiales en Common Lisp serían el condicional if, let y let* para declaración de variables, progn para ejecutar otras formas secuencialmente, entre otras.

2.6 Formas especiales

En Elixir son los building blocks del lenguaje, es decir que todo programa de Elixir consiste de una secuencia de formas especiales. A diferencia del módulo Kernel, no están implementadas en Elixir sino directamente en el compilador, que actualmente está implementado en Erlang. Por ejemplo, el macro if está definido en términos de la forma especial case, el macro unless está definido usando if, por lo tanto también termina traducido en case. El módulo Kernel.SpecialForms documenta estas estructuras, pero no se debe importar o usar, puesto que las formas están definidas globalmente.

Elixir logo

2.6.1. Algunas que ya hemos usado

%{} Mapas
& Funciones
. Acceso a módulos, funciones, mapas, estructuras
= Pattern matching
fn Funciones lambda
try Manejo de errores y finalización
^ Variables fijadas en pattern matching
{} Tuplas
Elixir logo

2.6.2. Representación interna de la sintáxis

Las expresiones no literales en Elixir se representan internamente con tuplas de 3 elementos, el primero de los cuales es un átomo o tupla que se refiere a una función, macro o forma especial, el segundo metadatos de compilación y el tercero una lista de argumentos. Para generar esta representación para un programa podemos usar la forma especial quote.

2.6.2 Representación interna de la sintáxis

iex> quote do: 1 + 1
{:+, [...], [1, 1]}
iex> quote do: (1 - 1; 2 * 2)
{:__block__, [], [{:-, [...], [1, 1]}, {:*, [...], [2, 2]}]}
iex> quote do: 1 + 2 + 3
{:+, [...],
 [{:+, [...], [1, 2]}, 3]}
iex> quote do: x.y(z)
{{:., [], [{:x, [], ...}, :y]}, [], [{:z, [], ...}]}
iex> quote do
...>   if true do
...>     {:ok, result}
...>   else
...>     :error
...>   end
...> end
{:if, [...],
 [true, [do: {:ok, {:result, [], ...}}, else: :error]]}
iex> quote do: fn x -> x + 1 end
{:fn, [],
 [
   {:->, [],
    [
      [{:x, [], ...}],
      {:+, [...], [{:x, [], ...}, 1]}
    ]}
 ]}
Elixir logo

2.6.3. cond

Evalúa una o más condiciones
iex> cond do
...>   false -> IO.inspect(1)
...>   true -> IO.inspect(2)
...> end
2
2
iex> cond do
...>   false -> IO.inspect(1)
...>   false -> IO.inspect(2)
...> end
... (CondClauseError) ...
iex> cond do
...>   true -> IO.inspect(1)
...>   true -> IO.inspect(2)
...> end
1
1
iex> cond do
...>   nil -> IO.inspect(1)
...>   false -> IO.inspect(2)
...>   0 -> IO.inspect(3)
...>   true -> IO.inspect(4)
...> end
3
3
Elixir logo

2.6.4. case

Match de un valor con uno o más patterns
iex> result = {:ok, [1, 2, 3], [1, 4, 9]}
{:ok, [1, 2, 3], [1, 4, 9]}
iex> case result do
...>   :ok -> IO.inspect(1)
...>   {:ok, _} -> IO.inspect(2)
...>   {:ok, _, _} -> IO.inspect(3)
...> end
3
3
iex> case result do
...>   {:ok, x, y} when is_binary(x) -> y
...>   {:ok, [x | xs], []} -> {x, xs}
...>   {:ok, [x | xs], [y | ys]} -> {x + y, xs ++ ys}
...>   {:ok, [1 | xs], [1 | ys]} -> xs ++ ys
...> end
{2, [2, 3, 4, 9]}
iex> case result do
...>   :ok -> IO.puts("ok")
...>   :error -> IO.puts("error")
...> end
... (CaseClauseError) ...
Elixir logo

2.6.5. with

Pattern matching secuencial
with :ok <- MyModule.operation1(),
     {:ok, foo} <- MyModule.operation2(),
     {:ok, bar} <- MyMoudle.operation3(foo),
     %{baz: baz} <- foo[:data] do
  MyModule.operation4(baz)
else
  :error -> {:error, :unknown}
  {:error, error} = result -> result
  error -> {:error, :unknown, error}
end

2.6.5 with

iex> calc_length = fn ->
...>   with input when is_binary(input) <- IO.gets("enter x: "),
...>        input <- String.trim(input),
...>        {x, ""} when is_number(x) and x > 0 <- Float.parse(input),
...>        input when is_binary(input) <- IO.gets("enter y: "),
...>        input <- String.trim(input),
...>        {y, ""} when is_number(y) and y > 0 <- Float.parse(input) do
...>     {:ok, :math.sqrt(x * x + y * y)}
...>   else
...>     _ -> :error
...>   end
...> end
#Function<...>
iex> calc_length.()
enter x: x
:error
iex> calc_length.()
enter x: 1.5
enter y: -3
:error
iex> calc_length.()
enter x: 3
enter y: 4
{:ok, 5.0}
Elixir logo

2.6.6. for

iex> squares = [0, 1, 4, 9, 16, 25, 36, 49, 64]
[0, 1, 4, 9, 16, 25, 36, 49, 64]
iex> for n <- squares, do: n
[0, 1, 4, 9, 16, 25, 36, 49, 64]
iex> for n <- squares, rem(n, 2) == 0, do: n
[0, 4, 16, 36, 64]
iex> squares = for n <- 0..8, do: n * n
[0, 1, 4, 9, 16, 25, 36, 49, 64]
iex> for a <- [1, 2, 3], b <- [4, 5, 6], do: {a, b}
[{1, 4}, {1, 5}, {1, 6}, {2, 4}, {2, 5}, {2, 6}, {3, 4}, {3, 5}, {3, 6}]
iex> for a when rem(a, 2) == 0 <- [1, 2, 3, 4],
...>     b when rem(b, 2) == 1 <- [5, 6, 7, 8] do
...>   {a, b}
...> end
[{2, 5}, {2, 7}, {4, 5}, {4, 7}]
iex> pairs = fn list ->
...>   for {a, i} <- Enum.with_index(list),
...>       {b, j} <- Enum.with_index(list),
...>       j > i do
...>     {a, b}
...>   end
...> end
#Function<...>
iex> pairs.(1..5)
[{1, 2}, {1, 3}, {1, 4}, {1, 5}, {2, 3}, {2, 4}, {2, 5}, {3, 4}, {3, 5}, {4, 5}]

2.6.6 for

iex> for line <- IO.stream(:line) |> Stream.take(5),
...>     line = String.trim(line),
...>     String.starts_with?(line, "#") do
...>   String.trim(line, "#")
...> end
abcde
#x
 qwerty
  ##123
 16#ffff
["x", "123"]
Elixir logo

2.6.7. try

try do
  do_something()
rescue
  ArgumentError ->
    handle_argument_error()

  exception in [ArithmeticError, CaseClauseError] ->
    handle_arith_or_clause_error(exception)

  exception ->
    handle_other_error(exception)
catch
  x when is_number(x) ->
    handle_thrown_number(x)

  value ->
    handle_thrown_value(value)
after
  do_something_under_any_circumstance()
end

2.6.7 try

iex> try_div = fn a, b ->
...>   try do
...>     {:ok, div(a, b)}
...>   rescue
...>     ArithmeticError -> :error
...>   end
...> end
#Function<...>
iex> try_div.(9, 3)
{:ok, 3}
iex> try_div.(-1, 0)
:error

2.6.7 try

iex> super_optimized_sum = fn a, b ->
...>   try do
...>     unless is_number(a), do: throw({:error, :a_is_not_a_number})
...>     unless is_number(b), do: throw({:error, :b_is_not_a_number})
...>     if a == 0, do: throw({:ok, b})
...>     if b == 0, do: throw({:ok, a})
...>     IO.puts("penalization! (#{a}, #{b})")
...>     Process.sleep(1000)
...>     {:ok, a + b}
...>   catch
...>     {:error, _} = result -> result
...>     {:ok, _} = result -> result
...>     other -> throw(other)
...>   end
...> end
#Function<...>
iex> for {a, b} <- [{:a, 1}, {1, :b}, {1, 0}, {0.0, 0.1}, {0.1, 0.3}] do
...>   super_optimized_sum.(a, b)
...> end
penalization! {0.1, 0.3}
[error: :a_is_not_a_number, error: :b_is_not_a_number, ok: 1, ok: 0.1, ok: 0.4]

2.6.7 try

case File.open("/etc/passwd", [:read]) do
  {:ok, file} ->
    try do
      do_something_with_users_database(file)
    after
      File.close(file)
    end

  _ ->
    :error
end
Elixir logo

2.6.8. Manejo de módulos

  • alias - Define una abreviación de un nombre de módulo.
  • import - Importa definiciones públicas de un módulo.
  • require - Permite usar los macros exportados por un módulo.
Elixir logo

2.6.9. unquote

Permite generar código de forma dinámica dentro de bloques quote
iex> print_ast = &(IO.inspect(&1) |> Macro.to_string() |> IO.puts())
#Function<...>
iex> {a, b} = {2, 4}
{2, 4}
iex> quote(do: a + b) |> print_ast.()
{:+, [...],
 [
   {:a, [], ...},
   {:b, [], ...}
 ]}
a + b
:ok
iex> quote(do: unquote(a) + unquote(b)) |> print_ast.()
{:+, [...], [1, 2]}
1 + 2
:ok

2.6.9 unquote

defmodule MyMacros do
  defmacro match_bad(pattern, expr) do
    quote do
      case expr do
        pattern -> true
        _ -> false
      end
    end
  end

  defmacro match(pattern, expr) do
    quote do
      case unquote(expr) do
        unquote(pattern) -> true
        _ -> false
      end
    end
  end
end
my_macros.ex

2.6.9 unquote

Erlang/OTP 25 ...

warning: variable "expr" is unused ...
...
iex> require MyMacros
MyMacros
iex> MyMacros.match_bad({:ok, _}, {:ok, "result"})
... (CompileError) ...
iex> MyMacros.match({:ok, _}, {:ok, "result"})
true
iex> MyMacros.match({:ok, _}, :error)
false
iex> print_expansion = &(Macro.expand(&1, __ENV__) |> Macro.to_string() |> IO.puts())
#Function<...>
iex> quote(do: MyMacros.match_bad(1, 2)) |> print_expansion.()
case expr do
  pattern -> true
  _ -> false
end
:ok
iex> quote(do: MyMacros.match(1, 2)) |> print_expansion.()
case 2 do
  1 -> true
  _ -> false
end
:ok
Elixir logo

3. Módulos

Elixir logo

3.1. Declaración

iex> defmodule A, do: nil
{:module, A, <<...>>, nil}
iex> A.__info__(:functions)
[]
iex> defmodule B do
...>   IO.puts("hello from B!")
...>   def hello(), do: IO.puts("hello from B.hello()!")
...>   defp hello_priv(), do: IO.puts("hello from B.hello_priv()!")
...>   def hello2(name \\ "B.hello2()") do
...>     IO.puts("hello, #{name}!")
...>     hello_priv()
...>   end
...> end
hello from B!
{:module, B, <<...>>, :ok}
iex> B.__info__(:functions)
[hello: 0, hello2: 0, hello2: 1]
iex> B.hello
hello from B.hello()!
:ok
iex> B.hello2("world")
hello, world!
hello from B.hello_priv()!
:ok
Elixir logo

3.2. Importación y uso

defmodule Arithmetic do
  defmodule Integers do
    def sum(a, 0) when is_integer(a), do: a
    def sum(0, b) when is_integer(b), do: b
    def sum(a, b) when is_integer(a) and is_integer(b), do: a + b

    def sub(a, 0) when is_integer(a), do: a
    def sub(0, b) when is_integer(b), do: b
    def sub(a, b) when is_integer(a) and is_integer(b), do: a - b

    def mul(a, 0) when is_integer(a), do: 0
    def mul(a, 1) when is_integer(a), do: a
    def mul(a, -1) when is_integer(a), do: -a
    def mul(a, b) when is_integer(a) and is_integer(b) and b > 1, do: a + mul(a, b - 1)
    def mul(a, b) when is_integer(a) and is_integer(b) and b < 0, do: -a - mul(a, -b - 1)
  end
end
arithmetic.ex
Elixir logo

3.2.1. Compilación

$ elixirc arithmetic.ex
$ ls
arithmetic.ex  Elixir.Arithmetic.Integers.beam
$ iex
iex> exports(Arithmetic.Integers)
mul/2     sub/2     sum/2
iex> Arithmetic.Integers.sum(2, 3)
5
iex> Arithmetic.Integers.sub(3, 4)
-1
iex> Arithmetic.Integers.mul(20, 30)
600
iex> Arithmetic.Integers.mul(-16, -256)
4096
Elixir logo

3.2.2. alias

iex> alias Arithmetic, as: Arith
Arithmetic
iex> Arith
Arithmetic
iex> :erlang.display(Arith)
'Elixir.Arithmetic'
true
iex> Arith.Integers.sub(0, 0)
0
iex> alias Arithmetic.Integers
Arithmetic.Integers
iex> Integers
Arithmetic.Integers
iex> Integers.sum(2, 2)
4
iex> alias Arith.Integers, as: IntOperations
Arithmetic.Integers
iex> IntOperations
Arithmetic.Integers
iex> IntOperations.mul(1, 1)
1

3.2.2 alias

iex> alias A
A
iex> alias A, as: Z
A
iex> Z
A
iex> alias A.B
A.B
iex> B
A.B
iex> alias A.B.C.D
A.B.C.D
iex> D
A.B.C.D
iex> alias A.B.C.D, as: Y
A.B.C.D
iex> Y
A.B.C.D
Elixir logo

3.2.3. import

iex> import Arithmetic.Integers
Arithmetic.Integers
iex> mul(-8, 7)
-56
iex> import Arithmetic.Integers, only: [sum: 2]
Arithmetic.Integers
iex> mul(-8, 7)
... (CompileError) ...
iex> sum(-1, -1)
-2
iex> import Arithmetic.Integers, except: [sum: 2]
Arithmetic.Integers
iex> sum(-1, -1)
... (CompileError) ...
iex> sub(3, -4)
7
Elixir logo

3.3. Funciones

defmodule MyFunctions do
  defp ok(a), do: {:ok, a}

  def foo(atom \\ :baz) when is_atom(atom) do
    ok(atom)
  end

  def get_user_name(%{name: name, id: id} = _user) do
    name || make_default_name(id)
  end

  defp make_default_name(id) when is_integer(id) do
    "user#{Integer.to_string(id) |> String.pad_leading(8, "0")}"
  end

  def first_arg(a, _), do: a
  def second_arg(_, a), do: a

  def make_grep_cmd(opts \\ []) when is_list(opts) do
    opts = Keyword.merge([ignore_case: true, extended_regexp: true], opts)
    "/usr/bin/grep #{args_to_str(opts)}"
  end

  defp args_to_str(args) do
    OptionParser.to_argv(args) |> Enum.join(" ")
  end
end
Elixir logo

3.4. Atributos (@)

defmodule MyModule do
  @moduledoc "Documentation for the A module."

  @on_load :on_load_my_module

  def on_load_my_module() do
    IO.puts("hello from loaded module #{__MODULE__}!")
  end

  @doc "Returns its first argument."
  @spec identity(term) :: term
  def identity(value), do: value

  @my_magic_number 0xCAFEBABE

  @doc """
  Returns this module's magic number.

      iex> magic_number
      3405691582
  """
  def magic_number, do: @my_magic_number
end
my_module.ex

3.4 Atributos (@)

$ elixirc my_module.ex
$ ls
Elixir.MyModule.beam  my_module.ex
$ iex
iex> MyModule.magic_number
hello from loaded module Elixir.MyModule!
3405691582
iex> MyModule.my_magic_number
... (UndefinedFunctionError) ...
iex> MyModule.@my_magic_number
... (CompileError) ...
Elixir logo

3.5. Comportamientos

defmodule Cache do
  @doc "Retrieves a value by key."
  @callback get(key :: any) :: any

  @doc "Inserts a value."
  @callback put(key :: any, value :: any) :: :ok | {:error, atom}

  @doc "Deletes a value by key."
  @callback delete(key :: any) :: :ok | {:error, atom}
end

3.5 Comportamientos

defmodule MapCache do
  @behaviour Cache

  @dict_key :map_cache

  @impl true
  def get(key) do
    Process.get(@dict_key, %{}) |> Map.fetch(key)
  end

  @impl true
  def put(key, value) do
    map = Process.get(@dict_key, %{}) |> Map.put(key, value)
    Process.put(@dict_key, map)
    :ok
  end

  @impl true
  def delete(key) do
    map = Process.get(@dict_key, %{}) |> Map.delete(key)
    Process.put(@dict_key, map)
    :ok
  end
end
Elixir logo

3.6. Documentación

defmodule User do
  @moduledoc "Defines the User structure."

  defstruct [:id, :username, :first_name, :last_name]

  @typedoc "An User object."
  @type t :: %__MODULE__{
               id: pos_integer,
               username: String.t(),
               first_name: String.t(),
               last_name: String.t()
             }

  @typedoc "An User DTO map."
  @type dto :: %{String.t() => any}

  @doc "Converts an User object to a DTO map."
  def to_dto(%User{} = user) do
    %{
      "username" => user.username,
      "firstName" => user.first_name,
      "lastName" => user.last_name
    }
  end
end
Elixir logo

4. Metaprogramación

Elixir logo

4.1. Macros

Permiten generar código de forma dinámica
defmodule MyMacros do
  defmacro if(condition, opts) do
    do_block = opts[:do]
    else_block = opts[:else]

    quote do
      case unquote(condition) do
        x when x in [false, nil] -> unquote(else_block)
        _ -> unquote(do_block)
      end
    end
  end

  defmacro unless(condition, opts) do
    quote do
      if !unquote(condition), unquote(opts)
    end
  end

  defmacro defdebug(call, opts) do
    block = Keyword.fetch!(opts, :do)
    block =
      quote do
        IO.inspect(binding())
        unquote(block)
      end

    opts = Keyword.put(opts, :do, block)

    quote do
      def unquote(call), unquote(opts)
    end
  end
end
my_macros.ex

4.1 Macros

iex> require MyMacros
MyMacros
iex> MyMacros.if 2 < 3, do: :lesser, else: :greater_or_equal
:lesser
iex> fruits = ["apple", "orange", "pineapple"]
["apple", "orange", "pineapple"]
iex> MyMacros.if "guava" not in fruits do
...>   :ok
...> else
...>   :error
...> end
:ok
iex> errors = ["username is empty", "password is empty"]
["username is empty", "password is empty"]
iex> MyMacros.unless Enum.empty?(errors) do
...>   IO.puts("there are errors!")
...>   Enum.each(errors, &IO.puts/1)
...>   :error
...> end
there are errors!
username is empty
password is empty
:error
iex> defmodule A do
...>   import MyMacros
...>   defdebug sum(a, b), do: a + b
...> end
{:module, A, <<...>>, ...}
iex> A.sum(19, 23)
[a: 8, b: 7]
15
Elixir logo

4.2. use

defmodule MyModuleTemplate do
  defmacro __using__(opts) do
    import_opts = Keyword.get(opts, :import_opts, [])

    quote do
      import unquote(__MODULE__), unquote(import_opts)

      require MyMacros

      alias A.B.C
      alias Arithmetic.Integers, as: IntOperations
    end
  end

  def first({x, _}), do: x
  def second({_, x}), do: x
end

4.2 use

defmodule UsingExample do
  # becomes MyModuleTemplate.__using__([])
  use MyModuleTemplate

  def keys(map) when is_map(map) do
    Enum.map(map, &first/1)
  end

  def values(map) when is_map(map) do
    Enum.map(map, &second/1)
  end
end

4.2 use

defmodule UsingExampleExpanded do
  import MyModuleTemplate, []

  require MyMacros

  alias A.B.C
  alias Arithmetic.Integers, as: IntOperations

  def keys(map) when is_map(map) do
    Enum.map(map, &first/1)
  end

  def values(map) when is_map(map) do
    Enum.map(map, &second/1)
  end
end
Elixir logo

4.3. Protocolos

A protocol specifies an API that should be defined by its implementations.

defprotocol Area do
  @doc "Calculates the area of a shape."
  @spec area(any) :: number
  def area(shape)
end

defmodule Rectangle do
  defstruct [:width, :height]

  defimpl Area do
    def area(%Rectangle{width: width, height: height}) do
      width * height
    end
  end
end

defmodule Circle do
  defstruct [:radius]

  defimpl Area do
    def area(%Circle{radius: radius}) do
      :math.pi() * radius * radius
    end
  end
end

defmodule Triangle do
  defstruct [:a, :b, :c]

  defimpl Area do
    def area(%Triangle{a: a, b: b, c: c}) do
      s = (a + b + c) / 2
      :math.sqrt(s * (s - a) * (s - b) * (s - c))
    end
  end
end

defimpl Area, for: Map do
  def area(%{__shape__: struct} = shape) do
    Map.delete(shape, :__shape__)
    |> then(&struct!(struct, &1))
    |> Area.area()
  end
end

defimpl Area, for: Tuple do
  def area(tuple) do
    struct = elem(tuple, 0)
    struct.__info__(:struct)
    |> Enum.map(& &1.field)
    |> Enum.zip(Tuple.delete_at(tuple, 0) |> Tuple.to_list())
    |> then(&struct!(struct, &1))
    |> Area.area()
  end
end
geometry.ex

4.3 Protocolos

iex> Area.area(%Rectangle{width: 5, height: 3})
15
iex> Area.area(%Circle{radius: 1})
3.141592653589793
iex> Area.area(%Triangle{a: 3, b: 4, c: 5})
6.0
iex> Area.area(%{__shape__: Rectangle, width: 1, height: 7})
7
iex> Area.area({Triangle, 3, 4, 5})
6.0
Elixir logo

4.3.1. Ejemplo: Poison, librería JSON

defmodule User do
  defstruct [:id, :username, :email, :password]

  defimpl Poison.Encoder do
    # encodes ignoring id and password values
    def encode(%User{} = user, options) do
      Map.from_struct(user)
      |> Map.drop([:id, :password])
      |> Poison.Encoder.encode(options)
    end
  end
end
Elixir logo

4.3.2. Derivación

defprotocol Indexable do
  @spec indices(any) :: [any]
  def indices(object)
end

defimpl Indexable, for: Tuple do
  def indices(tuple) when is_tuple(tuple) do
    max_index = tuple_size(tuple) - 1
    0..max_index |> Enum.to_list()
  end
end

defimpl Indexable, for: List do
  def indices(list) when is_list(list) do
    indices(list, 0)
  end

  defp indices([], _), do: []
  defp indices([_ | t], i), do: [i | indices(t, i + 1)]
end

defimpl Indexable, for: Map do
  def indices(map) when is_map(map) do
    Map.keys(map)
  end
end

defimpl Indexable, for: Any do
  defmacro __deriving__(module, struct, _options) do
    fields = Map.from_struct(struct) |> Map.keys()

    quote do
      defimpl Indexable, for: unquote(module) do
        @fields unquote(fields)

        def indices(_), do: @fields
      end
    end
  end

  def indices(_), do: []
end

defmodule Point do
  @derive [Indexable]
  defstruct [:x, :y]
end
indexable.ex

4.3.2 Derivación

iex> Indexable.indices({:a, :b, :c, :d})
[0, 1, 2, 3]
iex> Indexable.indices([1, 8, 27, 64, 125])
[0, 1, 2, 3, 4]
iex> Indexable.indices(%{x: 1, y: 0, z: 0})
[:x, :y, :z]
iex> Indexable.indices(%Point{x: 0, y: 1})
[:x, :y]
Elixir logo

5. Ecosistema

Elixir logo

5.1. Aplicaciones estándar

El repositorio de Elixir se compone de varias aplicaciones y módulos BEAM, escritos en Erlang y en Elixir, los cuales siempre están disponibles en una instalación del lenguaje.

Elixir logo

5.1.1. Elixir

Compilador y librería estándar, escritos en Erlang y Elixir, respectivamente. Muchas funciones de la librería estándar hacen uso de BIFs (built-in functions) de otros módulos estándar de Erlang/OTP. El compilador utiliza yecc, generador de parsers estándar de Erlang/OTP.

Documentación en hexdocs

Elixir logo

5.1.2. ExUnit

Framework de pruebas unitarias que nos permite estructurar, definir y ejecutar casos de prueba utilizando un DSL, implementado usando macros. Algunas funcionalidades que implementa el framework son:

  • Setup y cleanup
  • Aserciones
  • State compartido entre pruebas
  • Templates (mediante use)
  • Generación de casos de prueba a partir de documentación de módulos y funciones
  • Ejecución concurrente
Elixir logo

5.1.3. IEx

Shell interactivo de Elixir, análogo al módulo shell de Erlang. Incluye el módulo IEx.Helpers que incluye funciones de utilidad y es importado automáticamente al iniciar el shell. También incluye el módulo IEx.Pry que permite detener la ejecución de un programa para depurarlo, iniciando un shell interactivo en el contexto en que se detiene el programa; puede invocarse con el macro Kernel.dbg.

Elixir logo

5.1.4. Logger

Aplicación para logging implementada internamente usando el módulo logger de Erlang, con algunas adiciones, como formatos y mensajes distintos a los incluidos en Erlang/OTP.

Elixir logo

5.1.5. Mix

Herramienta de compilación para el manejo de proyectos mediante CLI. Nos permite realizar tareas de creación, compilación, configuración, ejecución, testing y manejo de dependencias en proyectos Elixir. Es extensible, por lo que librerías externas también pueden definir y documentar tareas usando la API de Mix, y que se pueden invocar y consultar usando la línea de comandos de Mix. La configuración de proyectos, incluyendo sus dependencias, se define en un módulo Elixir en el archivo mix.exs.

Elixir logo

5.2. Clean architecture Bancolombia

Proyecto open source publicado de Bancolombia que permite generar una estructura de proyecto Elixir que sigue los lineamientos de arquitectura limpia de esta organización. Está basado en un proyecto similar de Bancolombia para el lenguaje de programación Java, que es distribuido como un plugin Gradle.

Define un conjunto de tareas Mix que generan módulos Elixir para diferentes componentes de la arquitectura hexagonal, que son: modelos, casos de uso, adaptadores y entry points.

Elixir logo

5.2.1. Diagrama de la arquitectura

Fuente
Elixir logo

5.2.2. Modelo

Fuente
Elixir logo

5.2.3. Comportamiento

Fuente
Elixir logo

5.2.4. Caso de uso

Fuente
Elixir logo

5.2.5. Adaptador (Ecto)

Fuente
Elixir logo

5.2.6. Entry point (REST)

Fuente
Elixir logo

5.2.7. Instalación

$ mix archive.install hex elixir_structure_manager
Elixir logo

5.2.8. Crear un proyecto

$ mix ca.new.structure my_ca_app
Elixir logo

5.2.9. Tareas de Mix

$ mix help --search ca.
mix ca.apply.config  # Applies project configuration
mix ca.new.da        # Creates a new driven adapter
mix ca.new.ep        # Creates a new entry point
mix ca.new.model     # Creates a new model with empty properties
mix ca.new.structure # Creates a new clean architecture application.
mix ca.new.usecase   # Creates a new usecase
$ mix help ca.new.structure
...

Creates a new driven adapter for the clean architecture project mix ca.new.da
--type driven_adapter_name

Type param options:

  • generic
  • redis
  • asynceventbus

...
Elixir logo

5.2.10. Enlaces de interés