Taller de Elixir #13 – Structs

Aprendimos sobre mapas:

iex> map = %{a: 1, b: 2}
%{a: 1, b: 2}
iex> map[:a]
1
iex> %{map | a: 3}
%{a: 3, b: 2}

Las estructuras son extensiones construidas sobre mapas que proporcionan comprobaciones en tiempo de compilación y valores predeterminados.

Definiendo structs

Para definir una estructura, se utiliza la construcción defstruct:

iex> defmodule User do
...>   defstruct name: "John", age: 27
...> end

La lista de palabras clave utilizada con defstruct define qué campos tendrá la estructura junto con sus valores predeterminados.

Las estructuras toman el nombre del módulo en el que están definidas. En el ejemplo anterior, definimos una estructura llamada User.

Ahora podemos crear estructuras User utilizando una sintaxis similar a la utilizada para crear mapas (si ha definido la estructura en un archivo separado, puede compilar el archivo dentro de IEx antes de continuar ejecutando c “file.exs”. Ten en cuenta es posible que reciba un error que indique “the struct was not yet defined” si prueba el siguiente ejemplo en un archivo directamente debido a cuando se resuelven las definiciones):

iex> %User{}
%User{age: 27, name: "John"}
iex> %User{name: "Jane"}
%User{age: 27, name: "Jane"}

Las estructuras proporcionan garantías en tiempo de compilación de que solo los campos (y todos ellos) definidos a través de defstruct podrán existir en una estructura:

iex> %User{oops: :field}
** (KeyError) key :oops not found in: %User{age: 27, name: "John"}

Acceder y actualizar structs

Cuando hablamos de mapas, mostramos cómo podemos acceder y actualizar los campos de un mapa. Las mismas técnicas (y la misma sintaxis) también se aplican a las estructuras:

iex> john = %User{}
%User{age: 27, name: "John"}
iex> john.name
"John"
iex> jane = %{john | name: "Jane"}
%User{age: 27, name: "Jane"}
iex> %{jane | oops: :field}
** (KeyError) key :oops not found in: %User{age: 27, name: "Jane"}

Cuando se usa la sintaxis de actualización, la VM es consciente de que no se agregarán nuevas claves a la estructura, lo que permite que los mapas debajo compartan su estructura en la memoria. En el ejemplo anterior, tanto John como Jane comparten la misma estructura clave en la memoria.

Las estructuras también se pueden usar en la coincidencia de patrones, tanto para la coincidencia en el valor de claves específicas como para garantizar que el valor de coincidencia sea una estructura del mismo tipo que el valor coincidente.

iex> %User{name: name} = john
%User{age: 27, name: "John"}
iex> name
"John"
iex> %User{} = %{}
** (MatchError) no match of right hand side value: %{}

Structs son mapas desnudos debajo

En el ejemplo anterior, la coincidencia de patrones funciona porque debajo de las estructuras hay mapas desnudos con un conjunto fijo de campos. Como mapas, las estructuras almacenan un campo “especial” llamado __struct__ que contiene el nombre de la estructura:

iex> is_map(john)
true
iex> john.__struct__
User

Tenga en cuenta que nos referimos a las estructuras como mapas desnudos porque ninguno de los protocolos implementados para los mapas están disponibles para estructuras. Por ejemplo, no puede enumerar ni acceder a una estructura:

iex> john = %User{}
%User{age: 27, name: "John"}
iex> john[:name]
** (UndefinedFunctionError) function User.fetch/2 is undefined (User does not implement the Access behaviour)
             User.fetch(%User{age: 27, name: "John"}, :name)
iex> Enum.each john, fn({field, value}) -> IO.puts(value) end
** (Protocol.UndefinedError) protocol Enumerable not implemented for %User{age: 27, name: "John"}

Sin embargo, dado que las estructuras son solo mapas, funcionan con las funciones del módulo Map:

iex> jane = Map.put(%User{}, :name, "Jane")
%User{age: 27, name: "Jane"}
iex> Map.merge(jane, %User{name: "John"})
%User{age: 27, name: "John"}
iex> Map.keys(jane)
[:__struct__, :age, :name]

Valores predeterminados y claves requeridas

Si no especifica un valor de clave predeterminado al definir una estructura, se supondrá nil:

iex> defmodule Product do
...>   defstruct [:name]
...> end
iex> %Product{}
%Product{name: nil}

Puede definir una estructura que combine ambos campos con valores predeterminados explícitos y valores nil implícitos. En este caso, primero debe especificar los campos que están implícitamente predeterminados en nil:

iex> defmodule User do
...>   defstruct [:email, name: "John", age: 27]
...> end
iex> %User{}
%User{age: 27, email: nil, name: "John"}

Hacerlo en orden inverso generará un error de sintaxis:

iex> defmodule User do                          
...>   defstruct [name: "John", age: 27, :email]
...> end
** (SyntaxError) iex:107: syntax error before: email

También puede exigir que ciertas claves tengan que especificarse al crear la estructura:

iex> defmodule Car do
...>   @enforce_keys [:make]
...>   defstruct [:model, :make]
...> end
iex> %Car{}
** (ArgumentError) the following keys must also be given when building struct Car: [:make]
    expanding struct: Car.__struct__/1

Taller de Elixir #12 – Atributos del módulo

Los atributos del módulo en Elixir tienen tres propósitos

  • Sirven para anotar el módulo, a menudo con información para ser utilizada por el usuario o la VM.
  • Trabajan como constantes.
  • Funcionan como un módulo de almacenamiento temporal para ser utilizado durante la compilación.

Veamos cada caso, uno por uno.

Anotaciones

Elixir trae el concepto de atributos de módulo de Erlang. Por ejemplo:

defmodule MyServer do
  @vsn 2
end

En el ejemplo anterior, estamos configurando explícitamente el atributo de versión para ese módulo. @vsn es utilizado por el mecanismo de recarga de código en Erlang VM para verificar si un módulo se ha actualizado o no. Si no se especifica ninguna versión, la versión se establece en la suma de comprobación MD5 de las funciones del módulo.

Elixir tiene un puñado de atributos reservados. Aquí hay algunos de ellos, los más utilizados:

  • @moduledoc: proporciona documentación para el módulo actual.
  • @doc: proporciona documentación para la función o macro que sigue al atributo.
  • @behaviour: (observe la ortografía británica) utilizada para especificar un OTP o un comportamiento definido por el usuario.
  • @before_compile: proporciona un enlace que se invocará antes de compilar el módulo. Esto hace posible inyectar funciones dentro del módulo exactamente antes de la compilación.

@moduledoc y @doc son los atributos más utilizados. Elixir trata la documentación como de primera clase y proporciona muchas funciones para acceder a la documentación. Puede leer más en la documentación en Elixir.

Volvamos al módulo Math definido en los capítulos anteriores, agreguemos documentación y guárdelo en el archivo math.ex:

defmodule Math do
  @moduledoc """
  Provides math-related functions.

  ## Examples

      iex> Math.sum(1, 2)
      3

  """

  @doc """
  Calculates the sum of two numbers.
  """
  def sum(a, b), do: a + b
end

Elixir promueve el uso de Markdown con heredocs para escribir documentación legible. Los heredocs son cadenas de varias líneas, comienzan y terminan con comillas dobles triples, manteniendo el formato del texto interno. Podemos acceder a la documentación de cualquier módulo compilado directamente desde IEx:

$ elixirc math.ex
$ iex
iex> h Math # Access the docs for the module Math
...
iex> h Math.sum # Access the docs for the sum function
...

También tenemos una herramienta llamada ExDoc que se utiliza para generar páginas HTML a partir de la documentación.

Puede consultar los documentos de Módulo para obtener una lista completa de los atributos admitidos. Elixir también usa atributos para definir typepecs.

Esta sección cubre los atributos integrados. Sin embargo, los desarrolladores también pueden usar los atributos o extenderlos las bibliotecas para admitir un comportamiento personalizado.

Constantes

Los desarrolladores de Elixir a menudo usan atributos de módulo cuando desean hacer que un valor sea más visible o reutilizable:

defmodule MyServer do
  @initial_state %{host: "127.0.0.1", port: 3456}
  IO.inspect @initial_state
end

Intentar acceder a un atributo que no se definió imprimirá una advertencia:

defmodule MyServer do
  @unknown
end
warning: undefined module attribute @unknown, please remove access to @unknown or explicitly set it before access

Los atributos también se pueden leer dentro de las funciones:

defmodule MyServer do
  @my_data 14
  def first_data, do: @my_data
  @my_data 13
  def second_data, do: @my_data
end

MyServer.first_data #=> 14
MyServer.second_data #=> 13

Cada vez que se lee un atributo dentro de una función, se toma una instantánea de su valor actual. En otras palabras, el valor se lee en tiempo de compilación y no en tiempo de ejecución. Como veremos, esto también hace que los atributos sean útiles como almacenamiento durante la compilación del módulo.

Normalmente, repetir un atributo del módulo hará que su valor se reasigne, pero hay circunstancias en las que es posible que desee configurar el atributo del módulo para que se acumulen sus valores:

defmodule Foo do
  Module.register_attribute __MODULE__, :param, accumulate: true

  @param :foo
  @param :bar     
  # here @param == [:foo, :bar]
end

Se pueden invocar funciones al definir un atributo del módulo:

defmodule MyApp.Notification do
  @service Application.get_env(:my_app, :email_service)
  @message Application.get_env(:my_app, :welcome_email)
  def welcome(email), do: @service.send_welcome_message(email, @message)
end

Sin embargo, tenga cuidado: las funciones definidas en el mismo módulo que el atributo en sí no se pueden invocar porque aún no se han compilado cuando se está definiendo el atributo.

Al definir un atributo, no deje un salto de línea entre el nombre del atributo y su valor.

Almacenamiento temporal

Uno de los proyectos en la organización Elixir es el proyecto Plug, que pretende ser una base común para construir bibliotecas web y marcos en Elixir.

La biblioteca Plug permite a los desarrolladores definir sus propios plugins que se pueden ejecutar en un servidor web:

defmodule MyPlug do
  use Plug.Builder

  plug :set_header
  plug :send_ok

  def set_header(conn, _opts) do
    put_resp_header(conn, "x-header", "set")
  end

  def send_ok(conn, _opts) do
    send_resp(conn, 200, "ok")
  end
end

IO.puts "Running MyPlug with Cowboy on http://localhost:4000"
Plug.Adapters.Cowboy.http MyPlug, []

En el ejemplo anterior, hemos utilizado la macro plug/1 para conectar funciones que se invocarán cuando haya una solicitud web. Internamente, cada vez que llama a plug/1, la biblioteca Plug almacena el argumento dado en un atributo @plugs. Justo antes de compilar el módulo, Plug ejecuta una devolución de llamada que define una función (call/2) que maneja las solicitudes HTTP. Esta función ejecutará todos los enchufes dentro de @plugs en orden.

Para comprender el código subyacente, necesitaríamos macros, por lo que volveremos a visitar este patrón en la guía de metaprogramación. Sin embargo, el enfoque aquí está en cómo usar los atributos del módulo como almacenamiento permite a los desarrolladores crear DSL.

Otro ejemplo proviene del marco ExUnit que utiliza los atributos del módulo como anotación y almacenamiento:

defmodule MyTest do
  use ExUnit.Case

  @tag :external
  test "contacts external service" do
    # ...
  end
end

Las etiquetas en ExUnit se utilizan para anotar pruebas. Las etiquetas se pueden usar más tarde para filtrar las pruebas. Por ejemplo, puede evitar ejecutar pruebas externas en su máquina porque son lentas y dependen de otros servicios, mientras que aún pueden habilitarse en su sistema de compilación.

Esperamos que esta sección arroje algo de luz sobre cómo Elixir admite la metaprogramación y cómo los atributos del módulo juegan un papel importante al hacerlo.

Taller de Elixir #11 – alias, require, and import

Para facilitar la reutilización del software, Elixir proporciona tres directivas (alias, requerir e importar) más una macro llamada uso resumida a continuación:

# Alias del módulo para que se pueda llamar como Bar en lugar de Foo.Bar
alias Foo.Bar, as: Bar

# Requiere que el módulo use sus macros
require Foo

# Importa funciones de Foo para poder llamarlas sin el prefijo `Foo.`
import Foo

# Invoca el código personalizado definido en Foo como un punto de extensión
use Foo

alias

Permite configurar alias para cualquier nombre de módulo dado.

Imagine que un módulo utiliza una lista especializada implementada en Math.List. La directiva alias permite hacer referencia a Math.List como List dentro de la definición del módulo:

defmodule Stats do
  alias Math.List, as: List
end

Es igual que:

defmodule Stats do
  alias Math.List
end

Ten en cuenta que el alias tiene un ámbito léxico, lo que le permite establecer alias dentro de funciones específicas:

defmodule Math do
  def plus(a, b) do
    alias Math.List
    # ...
  end

  def minus(a, b) do
    # ...
  end
end

En el ejemplo anterior, estamos invocando el alias dentro de la función plus/2, el alias será válido solo dentro de la función plus/2. minus/2 no se verá afectado en absoluto.

require

Elixir proporciona macros como mecanismo para la metaprogramación (escribir código que genera código). Las macros se expanden en tiempo de compilación.

Las funciones públicas en los módulos están disponibles globalmente, pero para usar macros, debe optar por solicitar el módulo en el que están definidas.

iex> Integer.is_odd(3)
** (CompileError) iex:1: you must require Integer before invoking the macro Integer.is_odd/1
    (elixir) src/elixir_dispatch.erl:97: :elixir_dispatch.dispatch_require/6
iex> require Integer
Integer
iex> Integer.is_odd(3)
true

En Elixir, Integer.is_odd/1 se define como una macro para que pueda usarse como guard. Esto significa que, para invocar Integer.is_odd/1, primero necesitamos el módulo Integer.

Tenga en cuenta que, al igual que la directiva alias, require también tiene un alcance léxico. Hablaremos más sobre macros en un capítulo posterior.

import

Usamos import siempre que deseamos acceder a funciones o macros de otros módulos sin usar el nombre completo. Tenga en cuenta que solo podemos importar funciones públicas, ya que las funciones privadas nunca son accesibles externamente.

Por ejemplo, si queremos usar la función duplicate/2 del módulo List varias veces, podemos importarla:

iex> import List, only: [duplicate: 2]
List
iex> duplicate :ok, 3
[:ok, :ok, :ok]

Importamos solo la función duplicate/2 de List. Aunque: solo es opcional, se recomienda su uso para evitar importar todas las funciones de un módulo dado dentro del alcance actual. :except también podría darse como una opción para importar todo en un módulo, excepto una lista de funciones.

Ten en cuenta que la importación también tiene un ámbito léxico. Esto significa que podemos importar macros o funciones específicas dentro de las definiciones de funciones:

defmodule Math do
  def some_function do
    import List, only: [duplicate: 2]
    duplicate(:ok, 10)
  end
end

En el ejemplo anterior, el List.duplicate/2 importado solo es visible dentro de esa función específica. duplicate/2 no estará disponible en ninguna otra función en ese módulo (o cualquier otro módulo para el caso).

Tenga en cuenta que la importación de un módulo lo requiere automáticamente.

use

La macro de use con frecuencia es un punto de extensión. Esto significa que, cuando usa un módulo FooBar, permite que ese módulo inyecte cualquier código en el módulo actual, como importarse a sí mismo u otros módulos, definir nuevas funciones, establecer un estado del módulo, etc.

Por ejemplo, para escribir pruebas usando el marco ExUnit, un desarrollador debe usar el módulo ExUnit.Case:

defmodule AssertionTest do
  use ExUnit.Case, async: true

  test "always pass" do
    assert true
  end
end

Detrás de escena, el uso requiere el módulo dado y luego llama a la devolución de llamada __using__/1, lo que permite que el módulo inyecte algo de código en el contexto actual. Algunos módulos (por ejemplo, el ExUnit.Case anterior, pero también Supervisor y GenServer) usan este mecanismo para llenar su módulo con un comportamiento básico, que su módulo está destinado a anular o completar.

En general, el siguiente módulo:

se compila en

defmodule Example do
  require Feature
  Feature.__using__(option: :value)
end

Como el uso permite que se ejecute cualquier código, no podemos conocer realmente los efectos secundarios del uso de un módulo sin leer su documentación. Por esta razón, a menudo se prefiere la importación y el alias, ya que su semántica está definida por el lenguaje.

Entendiendo los alias

Un alias en Elixir es un identificador en mayúscula (como String, Keyword, etc.) que se convierte en un átomo durante la compilación. Por ejemplo, el alias String se traduce por defecto al átomo: “Elixir.String”:

iex> is_atom(String)
true
iex> to_string(String)
"Elixir.String"
iex> :"Elixir.String" == String
true

Al usar la directiva alias/2, estamos cambiando el átomo al que se expande el alias.

Los alias se expanden a átomos porque en los módulos Erlang VM (y, en consecuencia, Elixir) siempre están representados por átomos. Por ejemplo, ese es el mecanismo que usamos para llamar a los módulos de Erlang:

iex> :lists.flatten([1, [2], 3])
[1, 2, 3]

Anidamiento de módulos

Fíjate en el siguiente ejemplo:

defmodule Foo do
  defmodule Bar do
  end
end

El ejemplo anterior definirá dos módulos: Foo y Foo.Bar. Se puede accedeFrom Elixir v1.2, it is possible to alias, import or require multiple modules at once. This is particularly useful once we start nesting modules, which is very common when building Elixir applications. For example, imagine you have an application where all modules are nested under MyApp, you can alias the modules MyApp.Foo, MyApp.Bar and MyApp.Baz at once as follows:r al segundo como Bar dentro de Foo siempre que estén en el mismo ámbito léxico. El código anterior es exactamente el mismo que:

defmodule Elixir.Foo do
  defmodule Elixir.Foo.Bar do
  end
  alias Elixir.Foo.Bar, as: Bar
end

O que:

defmodule Elixir.Foo do
  alias Elixir.Foo.Bar, as: Bar
end

defmodule Elixir.Foo.Bar do
end

Multi alias/import/require/use

Desde Elixir v1.2, es posible crear alias, importar o requerir múltiples módulos a la vez. Esto es particularmente útil una vez que comenzamos a anidar módulos, lo cual es muy común al construir aplicaciones Elixir. Por ejemplo, imagine que tiene una aplicación en la que todos los módulos están anidados en MyApp, puede crear un alias de los módulos MyApp.Foo, MyApp.Bar y MyApp.Baz de la siguiente manera:

alias MyApp.{Foo, Bar, Baz}

Taller de Elixir #10 – Enumerables y Streams

Enumerables

Elixir proporciona el concepto de enumerables y el módulo Enum para trabajar con ellos. Ya hemos aprendido dos enumerables: listas y mapas.

iex> Enum.map([1, 2, 3], fn x -> x * 2 end)
[2, 4, 6]
iex> Enum.map(%{1 => 2, 3 => 4}, fn {k, v} -> k * v end)
[2, 12]

El módulo Enum proporciona una amplia gama de funciones para transformar, ordenar, agrupar, filtrar y recuperar elementos de enumerables. Es uno de los módulos que los desarrolladores usan con frecuencia en su código Elixir.

Elixir también proporciona rangos:

iex> Enum.map(1..3, fn x -> x * 2 end)
[2, 4, 6]
iex> Enum.reduce(1..3, 0, &+/2)
6

Las funciones en el módulo Enum se limitan a, como su nombre lo indica, enumerar valores en estructuras de datos. Para operaciones específicas, como insertar y actualizar elementos particulares, es posible que deba buscar módulos específicos para el tipo de datos. Por ejemplo, si desea insertar un elemento en una posición dada en una lista, debe usar la función List.insert_at/3 del módulo List, ya que tendría poco sentido insertar un valor en, por ejemplo, un rango.

Decimos que las funciones en el módulo Enum son polimórficas porque pueden trabajar con diversos tipos de datos. En particular, las funciones en el módulo Enum pueden funcionar con cualquier tipo de datos que implemente el protocolo Enumerable.

El operador pipe

En el ejemplo siguiente tiene una pipeline de operaciones. Comenzamos con un rango y luego multiplicamos cada elemento en el rango por 3. Esta primera operación ahora creará y devolverá una lista con 100_000 elementos. Luego mantenemos todos los elementos impares de la lista, generando una nueva lista, ahora con 50_000 elementos, y luego sumamos todas las entradas.

iex> total_sum = 1..100_000 |> Enum.map(&(&1 * 3)) |> Enum.filter(odd?) |> Enum.sum
7500000000

El símbolo |> utilizado en el fragmento de arriba es el operador de tubería: toma la salida de la expresión en su lado izquierdo y la pasa como el primer argumento para la llamada a la función en su lado derecho. Es similar al Unix | operador. Su propósito es resaltar los datos que están siendo transformados por una serie de funciones. Para ver cómo puede hacer que el código sea más limpio, eche un vistazo al ejemplo anterior reescrito sin usar el operador |>:

iex> Enum.sum(Enum.filter(Enum.map(1..100_000, &(&1 * 3)), odd?))
7500000000

Puedes encontrar más sobre el operador de tubería en su documentación.

Streams

Como alternativa a Enum, Elixir proporciona el módulo Stream que admite operaciones diferidas:

iex> 1..100_000 |> Stream.map(&(&1 * 3)) |> Stream.filter(odd?) |> Enum.sum
7500000000

Los flujos son enumerables perezosos y componibles.

En el ejemplo anterior, 1..100_000 |> Stream.map (& (& 1 * 3)) devuelve un tipo de datos, un flujo real, que representa el cálculo del mapa en el rango 1..100_000:

iex> 1..100_000 |> Stream.map(&(&1 * 3))
#Stream<[enum: 1..100000, funs: [#Function<34.16982430/1 in Stream.map/2>]]>

Además, son componibles porque podemos canalizar muchas operaciones de flujo:

1..100_000 |> Stream.map(&(&1 * 3)) |> Stream.filter(odd?)
#Stream<[enum: 1..100000, funs: [...]]>

En lugar de generar listas intermedias, las secuencias crean una serie de cálculos que se invocan solo cuando pasamos la secuencia subyacente al módulo Enum. Las secuencias son útiles cuando se trabaja con colecciones grandes, posiblemente infinitas.

iex> stream = Stream.cycle([1, 2, 3])
#Function<15.16982430/2 in Stream.unfold/2>
iex> Enum.take(stream, 10)
[1, 2, 3, 1, 2, 3, 1, 2, 3, 1]

Por otro lado, Stream.unfold/2 puede usarse para generar valores a partir de un valor inicial dado:

iex> stream = Stream.unfold("hełło", &String.next_codepoint/1)
#Function<39.75994740/2 in Stream.unfold/2>
iex> Enum.take(stream, 3)
["h", "e", "ł"]

Otra función interesante es Stream.resource/3, que se puede utilizar para envolver los recursos, garantizando que se abran justo antes de la enumeración y se cierren después, incluso en caso de fallas. Por ejemplo, File.stream!/1 se basa en Stream.resource/3 para transmitir archivos:

iex> stream = File.stream!("path/to/file")
%File.Stream{
  line_or_bytes: :line,
  modes: [:raw, :read_ahead, :binary],
  path: "path/to/file",
  raw: true
}
iex> Enum.take(stream, 10)

El ejemplo anterior buscará las primeras 10 líneas del archivo que ha seleccionado. Esto significa que las transmisiones pueden ser muy útiles para manejar archivos grandes o incluso recursos lentos como los recursos de red.

La cantidad de funcionalidad en los módulos Enum y Stream puede ser desalentadora al principio, pero se familiarizará con ellos caso por caso. En particular, enfóquese primero en el módulo Enum y solo muévase a Stream para los escenarios particulares donde se requiere pereza, ya sea para manejar recursos lentos o grandes colecciones, posiblemente infinitas.

Taller de Elixir #9 – Recursividad

Bucles a través de la recursividad

Debido a la inmutabilidad, los bucles en Elixir (como en cualquier lenguaje de programación funcional) se escriben de manera diferente a los lenguajes imperativos. Por ejemplo, en un lenguaje imperativo como C, uno escribiría:

for(i = 0; i < sizeof(array); i++) {
  array[i] = array[i] * 2;
}

En el ejemplo anterior, estamos mutando tanto la matriz como la variable i. La mutación no es posible en Elixir. En cambio, los lenguajes funcionales dependen de la recursividad: una función se llama de forma recursiva hasta que se alcanza una condición que detiene la acción recursiva. No hay datos mutados en este proceso. Considere el siguiente ejemplo que imprime una cadena un número arbitrario de veces:

defmodule Recursion do
  def print_multiple_times(msg, n) when n <= 1 do
    IO.puts msg
  end

  def print_multiple_times(msg, n) do
    IO.puts msg
    print_multiple_times(msg, n - 1)
  end
end

Recursion.print_multiple_times("Hello!", 3)
# Hello!
# Hello!
# Hello!

Similar al case, una función puede tener muchas cláusulas. Una cláusula particular se ejecuta cuando los argumentos pasados a la función coinciden con los patrones de argumento de la cláusula y su guard se evalúa como true.

Cuando print_multiple_times/2 se llama inicialmente en el ejemplo anterior, el argumento n es igual a 3.

La primera cláusula tiene un guard que dice “use esta definición si y solo si n es menor o igual que 1”. Como este no es el caso, Elixir pasa a la siguiente definición de la cláusula.

La segunda definición coincide con el patrón y no tiene guard, por lo que se ejecutará. Primero imprime nuestro mensaje y luego se llama a sí mismo pasando n – 1 como segundo argumento.

Nuestro smg se imprime y print_multiple_times/2 se llama de nuevo, esta vez con el segundo argumento establecido en 1. Debido a que n ahora se establece en 1, el guard en nuestra primera definición de print_multiple_times/2 se evalúa como true, y ejecutamos esta definición particular. El mensaje se imprime y no queda nada para ejecutar.

Definimos print_multiple_times/2 para que, sin importar qué número se pase como segundo argumento, desencadene nuestra primera definición (conocida como caso base) o desencadene nuestra segunda definición, lo que garantizará que estemos exactamente un paso más cerca del caso base.

Reducir y mapear algoritmos

Veamos ahora cómo podemos usar el poder de la recursividad para sumar una lista de números:

defmodule Math do
  def sum_list([head | tail], accumulator) do
    sum_list(tail, head + accumulator)
  end

  def sum_list([], accumulator) do
    accumulator
  end
end

IO.puts Math.sum_list([1, 2, 3], 0) #=> 6

Invocamos sum_list con la lista [1, 2, 3] y el valor inicial 0 como argumentos. Intentaremos cada cláusula hasta encontrar una que coincida de acuerdo con las reglas de coincidencia de patrones. En este caso, la lista [1, 2, 3] coincide con [head | tail] que une head a 1 y tail a [2, 3]. Y accumulator se establece en 0.

Luego, agregamos el encabezado de la lista al head + accumulator y llamamos a sum_list nuevamente, recursivamente, pasando el final de la lista como su primer argumento. La cola volverá a coincidir [head | tail] hasta que la lista esté vacía, como se ve a continuación:

sum_list [1, 2, 3], 0
sum_list [2, 3], 1
sum_list [3], 3
sum_list [], 6

Cuando la lista está vacía, coincidirá con la cláusula final que devuelve el resultado final de 6.

El proceso de tomar una lista y reducirla a un valor se conoce como algoritmo de reducción y es fundamental para la programación funcional.

¿Qué sucede si en cambio queremos duplicar todos los valores de nuestra lista?

defmodule Math do
  def double_each([head | tail]) do
    [head * 2 | double_each(tail)]
  end

  def double_each([]) do
    []
  end
end
$ iex math.exs
iex> Math.double_each([1, 2, 3]) #=> [2, 4, 6]

Aquí hemos utilizado la recursividad para recorrer una lista, duplicar cada elemento y devolver una nueva lista. El proceso de tomar una lista y mapearla se conoce como algoritmo de mapa.

La recursividad y la optimización de la cola son una parte importante de Elixir y se usan comúnmente para crear bucles. Sin embargo, cuando programe en Elixir, rara vez usará la recursividad como se describe arriba para manipular las listas.

El módulo Enum, que veremos en el próximo capítulo, ya ofrece muchas comodidades para trabajar con listas. Por ejemplo, los ejemplos anteriores podrían escribirse como:

iex> Enum.reduce([1, 2, 3], 0, fn(x, acc) -> x + acc end)
6
iex> Enum.map([1, 2, 3], fn(x) -> x * 2 end)
[2, 4, 6]

O usando la sintaxis de captura:

iex> Enum.reduce([1, 2, 3], 0, &+/2)
6
iex> Enum.map([1, 2, 3], &(&1 * 2))
[2, 4, 6]

Taller de Elixir #8 – Módulos y funciones

En Elixir agrupamos varias funciones en módulos. Ya hemos usado muchos módulos diferentes en los capítulos anteriores, como el módulo de cadena:

iex> String.length("hello")
5

Para crear nuestros propios módulos en Elixir, utilizamos la macro defmodule. Usamos la macro def para definir funciones en ese módulo:

iex> defmodule Math do
...>   def sum(a, b) do
...>     a + b
...>   end
...> end

iex> Math.sum(1, 2)
3

Compilación

Ya es hora de que aprendamos cómo compilar el código de Elixir y también cómo ejecutar los scripts de Elixir.

La mayoría de las veces es conveniente escribir módulos en archivos para que puedan compilarse y reutilizarse. Supongamos que tenemos un archivo llamado math.ex con los siguientes contenidos:

defmodule Math do
  def sum(a, b) do
    a + b
  end
end

Este archivo se puede compilar usando elixirc:

$ elixirc math.ex

Esto generará un archivo llamado Elixir.Math.beam que contiene el código de bytes para el módulo definido. Si iniciamos iex nuevamente, la definición de nuestro módulo estará disponible (siempre que iex se inicie en el mismo directorio en el que se encuentra el archivo de código de bytes):

iex> Math.sum(1, 2)
3

Los proyectos de elixir generalmente se organizan en tres directorios:

  • ebin – contiene el bytecode compilado
  • lib: contiene código de elixir (generalmente archivos .ex)
  • prueba: contiene pruebas (generalmente archivos .exs)

Cuando trabaje en proyectos reales, la herramienta de compilación llamada mix será responsable de compilar y configurar las rutas adecuadas para usted. Con fines de aprendizaje, Elixir también es compatible con un modo de script que es más flexible y no genera ningún artefacto compilado.

Modo Scripted

Además de la extensión de archivo Elixir .ex, Elixir también admite archivos .exs para secuencias de comandos. Elixir trata ambos archivos exactamente de la misma manera, la única diferencia está en la intención. Los archivos .ex están destinados a ser compilados mientras que los archivos .exs se utilizan para la creación de scripts. Cuando se ejecutan, ambas extensiones compilan y cargan sus módulos en la memoria, aunque solo los archivos .ex escriben su código de bytes en el disco en el formato de archivos .beam.

Por ejemplo, podemos crear un archivo llamado math.exs:

defmodule Math do
  def sum(a, b) do
    a + b
  end
end

IO.puts Math.sum(1, 2)

Y ejecutarlo como:

$ elixir math.exs

El archivo se compilará en la memoria y se ejecutará, imprimiendo “3” como resultado. No se creará ningún archivo de código de bytes.

Funciones nombradas

Dentro de un módulo, podemos definir funciones con def/2 y funciones privadas con defp/2. Una función definida con def/2 puede invocarse desde otros módulos, mientras que una función privada solo puede invocarse localmente.

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)

Las declaraciones de funciones también admiten guards y múltiples cláusulas. Si una función tiene varias cláusulas, Elixir probará cada cláusula hasta que encuentre una que coincida. Aquí tenemos una implementación de una función que verifica si el número dado es cero o no:

defmodule Math do
  def zero?(0) do
    true
  end

  def zero?(x) when is_integer(x) do
    false
  end
endSiiiii......imágenes cuanto menos, impactantes

IO.puts Math.zero?(0)         #=> true
IO.puts Math.zero?(1)         #=> false
IO.puts Math.zero?([1, 2, 3]) #=> ** (FunctionClauseError)
IO.puts Math.zero?(0.0)       #=> ** (FunctionClauseError)

¿El signo de interrogación final en cero? significa que esta función devuelve un valor booleano. Ver convenciones de nomenclatura.

Dar un argumento que no coincide con ninguna de las cláusulas genera un error.

Similar a construcciones como if, las funciones con nombre admiten la sintaxis de bloque do: y do/end, como aprendimos, do/end es una sintaxis conveniente para el formato de lista de palabras clave. Por ejemplo, podemos editar math.exs para que se vea así:

defmodule Math do
  def zero?(0), do: true
  def zero?(x) when is_integer(x), do: false
end

Y proporcionará el mismo comportamiento. Puede usar do: para líneas simples, pero siempre use do/end para funciones que abarcan varias líneas.

Captura de funciones

Hemos estado usando el nombre/aridad de notación para referirnos a funciones. Sucede que esta notación se puede utilizar para recuperar una función con nombre como un tipo de función. Inicie iex, ejecutando el archivo math.exs definido anteriormente:

$ iex math.exs
iex> Math.zero?(0)
true
iex> fun = &Math.zero?/1
&Math.zero?/1
iex> is_function(fun)
true
iex> fun.(0)
true

Recuerde que Elixir hace una distinción entre funciones anónimas y funciones con nombre, donde las primeras deben invocarse con un punto (.) Entre el nombre de la variable y los paréntesis. El operador de captura cierra esta brecha al permitir que las funciones con nombre se asignen a variables y se pasen como argumentos de la misma manera que asignamos, invocamos y pasamos funciones anónimas.

Las funciones locales o importadas, como is_function/1, se pueden capturar sin el módulo:

iex> &is_function/1
&:erlang.is_function/1
iex> (&is_function/1).(fun)
true

hay que tener en cuenta que la sintaxis de captura también se puede utilizar como acceso directo para crear funciones:

iex> fun = &(&1 + 1)
#Function<6.71889879/1 in :erl_eval.expr/5>
iex> fun.(1)
2

iex> fun2 = &"Good #{&1}"
#Function<6.127694169/1 in :erl_eval.expr/5>
iex)> fun2.("morning")
"Good morning"

El &1 representa el primer argumento pasado a la función. &(&1 + 1) arriba es exactamente lo mismo que fn x -> x + 1 final. La sintaxis anterior es útil para definiciones breves de funciones.

Si desea capturar una función desde un módulo, puede hacer &Module.function():

iex> fun = &List.flatten(&1, &2)
&List.flatten/2
iex> fun.([1, [[2], 3]], [4, 5])
[1, 2, 3, 4, 5]

&List.flatten(&1, &2) es lo mismo que escribir fn(list, tail) -> List.flatten (list, tail) final, que en este caso es equivalente a &List.flatten/2. Puede leer más sobre el operador de captura y en la documentación Kernel.SpecialForms.

Argumentos por defecto

Las funciones con nombre en Elixir también admiten argumentos predeterminados:

defmodule Concat do
  def join(a, b, sep \\ " ") do
    a <> sep <> b
  end
end

IO.puts Concat.join("Hello", "world")      #=> Hello world
IO.puts Concat.join("Hello", "world", "_") #=> Hello_world

Se permite que cualquier expresión sirva como valor predeterminado, pero no se evaluará durante la definición de la función. Cada vez que se invoca la función y se debe utilizar cualquiera de sus valores predeterminados, se evaluará la expresión para ese valor predeterminado:

defmodule DefaultTest do
  def dowork(x \\ "hello") do
    x
  end
end
iex> DefaultTest.dowork
"hello"
iex> DefaultTest.dowork 123
123
iex> DefaultTest.dowork
"hello"

Si una función con valores predeterminados tiene varias cláusulas, es necesario crear un encabezado de función (sin un cuerpo real) para declarar valores predeterminados:

defmodule Concat do
  def join(a, b \\ nil, sep \\ " ")

  def join(a, b, _sep) when is_nil(b) do
    a
  end

  def join(a, b, sep) do
    a <> sep <> b
  end
end

IO.puts Concat.join("Hello", "world")      #=> Hello world
IO.puts Concat.join("Hello", "world", "_") #=> Hello_world
IO.puts Concat.join("Hello")               #=> Hello

El guión bajo inicial en _sep significa que la variable se ignorará en esta función. Ver convenciones de nomenclatura.

Cuando se usan valores predeterminados, se debe tener cuidado para evitar la superposición de definiciones de funciones. Considere el siguiente ejemplo:

defmodule Concat do
  def join(a, b) do
    IO.puts "***First join"
    a <> b
  end

  def join(a, b, sep \\ " ") do
    IO.puts "***Second join"
    a <> sep <> b
  end
end

Si guardamos el código anterior en un archivo llamado “concat.ex” y lo compilamos, Elixir emitirá la siguiente advertencia:

warning: this clause cannot match because a previous clause at line 2 always matches

El compilador nos dice que invocar la función de combinación con dos argumentos siempre elegirá la primera definición de combinación, mientras que la segunda solo se invocará cuando se pasen tres argumentos:

$ iex concat.ex
iex> Concat.join "Hello", "world"
***First join
"Helloworld"
iex> Concat.join "Hello", "world", "_"
***Second join
"Hello_world"

Taller de Elixir #7 – Lista de palabras claves y mapas

Aún no hemos visto ninguna estructura de datos asociativos, es decir, estructuras de datos que puedan asociar un cierto valor (o valores múltiples) a una clave. Diferentes idiomas llaman a estos diferentes nombres como diccionarios, hashes, matrices asociativas, etc.

Tenemos dos estructuras de datos asociativas principales: listas de palabras clave y mapas.

Lista de palabras claves (Keyword lists)

En Elixir, cuando tenemos una lista de tuplas y el primer elemento de la tupla (es decir, la clave) es un átomo, lo llamamos una lista de palabras clave:

iex> list = [{:a, 1}, {:b, 2}]
[a: 1, b: 2]
iex> list == [a: 1, b: 2]
true

Como puede ver arriba, Elixir admite una sintaxis especial para definir tales listas: [clave: valor]. Debajo se asigna a la misma lista de tuplas que la anterior. Como las listas de palabras clave son listas, podemos usar todas las operaciones disponibles para las listas. Por ejemplo, podemos usar ++ para agregar nuevos valores a una lista de palabras clave:

iex> list ++ [c: 3]
[a: 1, b: 2, c: 3]
iex> [a: 0] ++ list
[a: 0, a: 1, b: 2]

Ten en cuenta que los valores agregados al frente son los obtenidos en la búsqueda:

iex> new_list = [a: 0] ++ list
[a: 0, a: 1, b: 2]
iex> new_list[:a]
0

Las listas de palabras clave son importantes porque tienen tres características especiales:

  • Las llaves deben ser átomos.
  • Las claves están ordenadas, según lo especificado por el desarrollador.
  • Las llaves se pueden dar más de una vez.

Por ejemplo, la biblioteca Ecto hace uso de estas características para proporcionar un DSL elegante para escribir consultas en la base de datos:

query = from w in Weather,
      where: w.prcp > 0,
      where: w.temp < 20,
     select: w

Estas características son las que llevaron a las listas de palabras clave a ser el mecanismo predeterminado para pasar opciones a funciones en Elixir. La macro if/2, mencionamos que la siguiente sintaxis es compatible:

iex> if false, do: :this, else: :that
:that

¡Los pares do: y else: forman una lista de palabras clave! De hecho, la llamada anterior es equivalente a:

iex> if(false, [do: :this, else: :that])
:that

Y tambien es lo mismo que:

iex> if(false, [{:do, :this}, {:else, :that}])
:that

En general, cuando la lista de palabras clave es el último argumento de una función, los corchetes son opcionales.

Aunque podemos hacer coincidir patrones en listas de palabras clave, rara vez se hace en la práctica, ya que la coincidencia de patrones en listas requiere la cantidad de elementos y su orden para que coincidan:

iex> [a: a] = [a: 1]
[a: 1]
iex> a
1
iex> [a: a] = [a: 1, b: 2]
** (MatchError) no match of right hand side value: [a: 1, b: 2]
iex> [b: b, a: a] = [a: 1, b: 2]
** (MatchError) no match of right hand side value: [a: 1, b: 2]

Para manipular las listas de palabras clave, Elixir proporciona el módulo de palabras clave. Sin embargo, recuerde que las listas de palabras clave son simplemente listas y, como tales, proporcionan las mismas características de rendimiento lineal que las listas. Cuanto más larga sea la lista, más tiempo llevará encontrar una clave, contar la cantidad de elementos, etc. Por esta razón, las listas de palabras clave se utilizan en Elixir principalmente para pasar valores opcionales. Si necesita almacenar muchos artículos o garantizar asociados de una clave con un valor máximo, debe usar mapas en su lugar.

Mapas

Recomiendo la lectura de este articulo. Cuando termines este tema. Ya que se explica la sintaxis especial que trae Elixir.

Siempre que necesite un almacén de valores clave, los mapas son la estructura de datos “go to” en Elixir. Se crea un mapa utilizando la sintaxis %{}:

iex> map = %{:a => 1, 2 => :b}
%{2 => :b, :a => 1}
iex> map[:a]
1
iex> map[2]
:b
iex> map[:c]
nil

En comparación con las listas de palabras clave, hay dos diferencias:

  • Los mapas permiten cualquier valor como clave.
  • Las claves del mapas no siguen ningún pedido.

A diferencia de las listas de palabras clave, los mapas son muy útiles con la coincidencia de patrones. Cuando se usa un mapa en un patrón, siempre coincidirá en un subconjunto del valor dado:

iex> %{} = %{:a => 1, 2 => :b}
%{2 => :b, :a => 1}
iex> %{:a => a} = %{:a => 1, 2 => :b}
%{2 => :b, :a => 1}
iex> a
1
iex> %{:c => c} = %{:a => 1, 2 => :b}
** (MatchError) no match of right hand side value: %{2 => :b, :a => 1}

Como se muestra arriba, un mapa coincide siempre y cuando las claves del patrón existan en el mapa dado. Por lo tanto, un mapa vacío coincide con todos los mapas.

Las variables se pueden usar al acceder, hacer coincidir y agregar claves de mapa:

iex> n = 1
1
iex> map = %{n => :one}
%{1 => :one}
iex> map[n]
:one
iex> %{^n => :one} = %{1 => :one, 2 => :two, 3 => :three}
%{1 => :one, 2 => :two, 3 => :three}

El módulo Map proporciona una API muy similar al módulo Keyword con funciones convenientes para manipular mapas:

iex> Map.get(%{:a => 1, 2 => :b}, :a)
1
iex> Map.put(%{:a => 1, 2 => :b}, :c, 3)
%{2 => :b, :a => 1, :c => 3}
iex> Map.to_list(%{:a => 1, 2 => :b})
[{2, :b}, {:a, 1}]

Los mapas tienen la siguiente sintaxis para actualizar el valor de una clave:

iex> map = %{:a => 1, 2 => :b}
%{2 => :b, :a => 1}

iex> %{map | 2 => "two"}
%{2 => "two", :a => 1}
iex> %{map | :c => 3}
** (KeyError) key :c not found in: %{2 => :b, :a => 1}

La sintaxis anterior requiere que exista la clave dada. No se puede usar para agregar nuevas claves. Por ejemplo, al usarlo con la tecla: c falló porque no hay: c en el mapa.

Cuando todas las claves en un mapa son átomos, puede usar la sintaxis de palabras clave por conveniencia:

iex> map = %{:a => 1, 2 => :b}
%{2 => :b, :a => 1}

iex> map.a
1
iex> map.c
** (KeyError) key :c not found in: %{2 => :b, :a => 1}

Los desarrolladores de Elixir generalmente prefieren usar la sintaxis map.field y la coincidencia de patrones en lugar de las funciones en el módulo Map cuando trabajan con mapas porque conducen a un estilo de programación asertivo. Esta publicación de blog proporciona información y ejemplos sobre cómo obtener un software más conciso y rápido escribiendo código asertivo en Elixir.

Estructuras de datos anidados

A menudo tendremos mapas dentro de los mapas, o incluso listas de palabras clave dentro de los mapas, y así sucesivamente. Elixir proporciona comodidades para manipular estructuras de datos anidados a través de put_in/2, update_in/2 y otras macros que brindan las mismas comodidades que encontraría en lenguajes imperativos mientras mantiene las propiedades inmutables del lenguaje.

Imagina que tienes la siguiente estructura:

iex> users = [
  john: %{name: "John", age: 27, languages: ["Erlang", "Ruby", "Elixir"]},
  mary: %{name: "Mary", age: 29, languages: ["Elixir", "F#", "Clojure"]}
]
[john: %{age: 27, languages: ["Erlang", "Ruby", "Elixir"], name: "John"},
 mary: %{age: 29, languages: ["Elixir", "F#", "Clojure"], name: "Mary"}]

Tenemos una lista de palabras clave de usuarios donde cada valor es un mapa que contiene el nombre, la edad y una lista de lenguajes de programación que le gusta a cada usuario. Si quisiéramos acceder a la edad de John, podríamos escribir:

iex> users[:john].age
27

Sucede que también podemos usar esta misma sintaxis para actualizar el valor:

users = put_in users[:john].age, 31
[john: %{age: 31, languages: ["Erlang", "Ruby", "Elixir"], name: "John"},
 mary: %{age: 29, languages: ["Elixir", "F#", "Clojure"], name: "Mary"}]

La macro update_in/2 es similar pero nos permite pasar una función que controla cómo cambia el valor. Por ejemplo, eliminemos “Clojure” de la lista de idiomas de Mary:

iex> users = update_in users[:mary].languages, fn languages -> List.delete(languages, "Clojure") end
[john: %{age: 31, languages: ["Erlang", "Ruby", "Elixir"], name: "John"},
 mary: %{age: 29, languages: ["Elixir", "F#"], name: "Mary"}]

Hay más para aprender sobre put_in/2 y update_in/2, incluido get_and_update_in/2 que nos permite extraer un valor y actualizar la estructura de datos de una vez. También hay put_in/3, update_in/3 y get_and_update_in/3 que permiten el acceso dinámico a la estructura de datos.

Esto concluye nuestra introducción a las estructuras de datos asociativas en Elixir. Descubrirás que, dadas las listas de palabras clave y los mapas, siempre tendrá la herramienta adecuada para abordar los problemas que requieren estructuras de datos asociativas en Elixir.

Taller de Elixir #6 – Case, cond e if

Las estructuras de flujo de control de flujo son case, cond e if.

Case

Case nos permite comparar un valor con muchos patrones hasta que encontremos uno coincidente:

iex> case {1, 2, 3} do
...>   {4, 5, 6} ->
...>     "This clause won't match"
...>   {1, x, 3} ->
...>     "This clause will match and bind x to 2 in this clause"
...>   _ ->
...>     "This clause would match any value"
...> end
"This clause will match and bind x to 2 in this clause"

Si desea comparar patrones con una variable existente, debe usar el operador ^:

iex> x = 1
1
iex> case 10 do
...>   ^x -> "Won't match"
...>   _ -> "Will match"
...> end
"Will match"

Las cláusulas también permiten que se especifiquen condiciones adicionales a través de guards:

iex> case {1, 2, 3} do
...>   {1, x, 3} when x > 0 ->
...>     "Will match"
...>   _ ->
...>     "Would match, if guard condition were not satisfied"
...> end
"Will match"

La primera cláusula anterior solo coincidirá cuando x sea positivo.

Tenga en cuenta que los errores en los guardias no tienen fugas, sino que simplemente hacen que el guardia falle:

iex> hd(1)
** (ArgumentError) argument error
iex> case 1 do
...>   x when hd(x) -> "Won't match"
...>   x -> "Got #{x}"
...> end
"Got 1"

Si ninguna de las cláusulas coincide, se genera un error:

iex> case :ok do
...>   :error -> "Won't match"
...> end
** (CaseClauseError) no case clause matching: :ok

Consulte la documentación completa de los guardias para obtener más información sobre los guardias, cómo se usan y qué expresiones están permitidas en ellos.

Tenga en cuenta que las funciones anónimas también pueden tener múltiples cláusulas y guardias:

iex> f = fn
...>   x, y when x > 0 -> x + y
...>   x, y -> x * y
...> end
#Function<12.71889879/2 in :erl_eval.expr/5>
iex> f.(1, 3)
4
iex> f.(-1, 3)
-3

El número de argumentos en cada cláusula de función anónima debe ser el mismo, de lo contrario se genera un error.

iex> f2 = fn
...>   x, y when x > 0 -> x + y
...>   x, y, z -> x * y + z
...> end
** (CompileError) iex:1: cannot mix clauses with different arities in anonymous functions

Cond

Case es útil cuando necesita hacer coincidir valores diferentes. Sin embargo, en muchas circunstancias, queremos verificar diferentes condiciones y encontrar la primera que no se evalúa como nula o falsa. En tales casos, uno puede usar cond:

iex> cond do
...>   2 + 2 == 5 ->
...>     "This will not be true"
...>   2 * 2 == 3 ->
...>     "Nor this"
...>   1 + 1 == 2 ->
...>     "But this will"
...> end
"But this will"

Esto es equivalente a las cláusulas else if en muchos idiomas imperativos (aunque aquí se usan con menos frecuencia).

Si todas las condiciones devuelven nulo o falso, se genera un error (CondClauseError). Por esta razón, puede ser necesario agregar una condición final, igual a verdadera, que siempre coincidirá con:

iex> cond do
...>   2 + 2 == 5 ->
...>     "This is never true"
...>   2 * 2 == 3 ->
...>     "Nor this"
...>   true ->
...>     "This is always true (equivalent to else)"
...> end
"This is always true (equivalent to else)"

Finalmente, note cond considera que cualquier valor además de nil y false es verdadero:

iex> cond do
...>   hd([1, 2, 3]) ->
...>     "1 is considered as true"
...> end
"1 is considered as true"

if y unless

Además de case y cond, también tenemos las macros if/2 y unless/2, que son útiles cuando necesita verificar una sola condición:

iex> if true do
...>   "This works!"
...> end
"This works!"
iex> unless true do
...>   "This will never be seen"
...> end
nil

Si la condición if/2 devuelve false o nil, el cuerpo dado entre do/end no se ejecuta y en su lugar devuelve nil. Lo contrario sucede con unless/2.

También admiten bloques más:

iex> if nil do
...>   "This won't be seen"
...> else
...>   "This will"
...> end
"This will"

Bloques do/end

Como hemos visto en las cuatro estructuras de control: case, cond, if y unless. En todas estaban envueltas en bloques do/end. Sucede que también podríamos escribir si es así:

iex> if true, do: 1 + 2
3

Los bloques do/end son una conveniencia sintáctica construida sobre las palabras clave. Es por eso que los bloques do/end no requieren una coma entre el argumento anterior y el bloque. Son útiles exactamente porque eliminan la verbosidad al escribir bloques de código. Estos son equivalentes:

iex> if true do
...>   a = 1 + 2
...>   a + 10
...> end
13
iex> if true, do: (
...>   a = 1 + 2
...>   a + 10
...> )
13

Las listas de palabras clave juegan un papel importante en el lenguaje y son bastante comunes en muchas funciones y macros.

Taller de Elixir #5 – Pattern Matching (La coincidencia de patrones)

Vamos a ver cómo el operador = en Elixir es en realidad un operador de coincidencia y cómo usarlo para establecer patrones dentro de las estructuras de datos. También, aprenderemos sobre el operador de pin ^ usado para acceder a valores previamente vinculados.

El operador match

Hemos usado el operador = un par de veces para asignar variables en Elixir:

iex> x = 1
1
iex> x
1

Pero en Elixir, el operador = en realidad se llama operador de coincidencia. Veamos por qué:

iex> x = 1
1
iex> 1 = x
1
iex> 2 = x
** (MatchError) no match of right hand side value: 1

Observe que 1 = x es una expresión válida y coincide porque los lados izquierdo y derecho son iguales a 1. Cuando los lados no coinciden, se genera un MatchError.

Una variable solo se puede asignar en el lado izquierdo del =.

iex> 1 = unknown
** (CompileError) iex:1: undefined function unknown/0

Como no hay una variable desconocida previamente definida, Elixir asumió que estaba intentando llamar a una función llamada unknown/0, pero esa función no existe.

Pattern Matching

El operador de coincidencia no solo se utiliza para comparar valores simples, sino que también es útil para desestructurar tipos de datos más complejos. Por ejemplo, podemos combinar patrones en tuplas:

iex> {a, b, c} = {:hello, "world", 42}
{:hello, "world", 42}
iex> a
:hello
iex> b
"world"

Se producirá un error de coincidencia de patrón si los lados no pueden coincidir, por ejemplo, si las tuplas tienen tamaños diferentes:

iex> {a, b, c} = {:hello, "world"}
** (MatchError) no match of right hand side value: {:hello, "world"}

Y también al comparar diferentes tipos:

iex> {a, b, c} = [:hello, "world", 42]
** (MatchError) no match of right hand side value: [:hello, "world", 42]

Más interesante aún, podemos coincidir en valores específicos. El siguiente ejemplo afirma que el lado izquierdo solo coincidirá con el lado derecho cuando el lado derecho es una tupla que comienza con el átomo :ok:

iex> {:ok, result} = {:ok, 13}
{:ok, 13}
iex> result
13

iex> {:ok, result} = {:error, :oops}
** (MatchError) no match of right hand side value: {:error, :oops}

Podemos emparejar patrones en listas:

iex> [a, b, c] = [1, 2, 3]
[1, 2, 3]
iex> a
1

Una lista también admite coincidencias en su propia cabeza y cola:

iex> [head | tail] = [1, 2, 3]
[1, 2, 3]
iex> head
1
iex> tail
[2, 3]

Similar a las funciones hd/1 y tl/1, no podemos hacer coincidir una lista vacía con un patrón de cabeza y cola:

iex> [head | tail] = []
** (MatchError) no match of right hand side value: []

La [cabeza | el formato tail] no solo se usa en la coincidencia de patrones, sino también para anteponer elementos a una lista:

iex> list = [1, 2, 3]
[1, 2, 3]
iex> [0 | list]
[0, 1, 2, 3]

La coincidencia de patrones permite a los desarrolladores desestructurar fácilmente los tipos de datos, como tuplas y listas.

El operador pin

Utilice el operador de pin ^ cuando desee comparar patrones con el valor de una variable existente en lugar de volver a vincular la variable:

iex> x = 1
1
iex> ^x = 2
** (MatchError) no match of right hand side value: 2
iex> {y, ^x} = {2, 1}
{2, 1}
iex> y
2
iex> {y, ^x} = {2, 2}
** (MatchError) no match of right hand side value: {2, 2}

Como hemos asignado el valor de 1 a la variable x, este último ejemplo también podría haberse escrito como:

iex> {y, 1} = {2, 2}
** (MatchError) no match of right hand side value: {2, 2}

Si una variable se menciona más de una vez en un patrón, todas las referencias deben unirse al mismo patrón:

iex> {x, x} = {1, 1}
{1, 1}
iex> {x, x} = {1, 2}
** (MatchError) no match of right hand side value: {1, 2}

En algunos casos, no le importa un valor particular en un patrón. Es una práctica común vincular esos valores al guión bajo, _. Por ejemplo, si solo nos importa el encabezado de la lista, podemos asignar la cola para subrayar:

iex> [head | _] = [1, 2, 3]
[1, 2, 3]
iex> head
1

La variable _ es especial porque nunca se puede leer. Si intenta leer de él da un error de compilación:

iex> _
** (CompileError) iex:1: invalid use of _. "_" represents a value to be ignored in a pattern and cannot be used in expressions

Aunque la coincidencia de patrones nos permite construir construcciones potentes, su uso es limitado. Por ejemplo, no puede realizar llamadas a funciones en el lado izquierdo de una coincidencia. El siguiente ejemplo no es válido:

iex> length([1, [2], 3]) = 3
** (CompileError) iex:1: cannot invoke remote function :erlang.length/1 inside match

Taller de Elixir #4 – Operadores Básicos

Hemos vistos que los operadores aritméticos +, -, *, / como, además de las funciones div/2 y rem/2 para la división entera y el resto.

También vimos ++ y — para manipular listas:

iex> [1, 2, 3] ++ [4, 5, 6]
[1, 2, 3, 4, 5, 6]
iex> [1, 2, 3] -- [2]
[1, 3]

La concatenación de cadenas se realiza con <>:

iex> "foo" <> "bar"
"foobar"

Aun que también es muy común el uso de:

iex> [foo, bar] = ["foo", "bar"]
["foo", "bar"]
iex> "#{foo}#{bar}"
"foobar"

Hay tres operadores booleanos: or, and y not. Estos operadores son estrictos en el sentido de que esperan algo que se evalúe como booleano (verdadero o falso) como primer argumento:

iex> true and true
true
iex> false or is_atom(:example)
true

Proporcionar un valor no booleano generará una excepción:

iex> 1 and true
** (BadBooleanError) expected a boolean on left-side of "and", got: 1

Or y and son operadores de cortocircuito. Solo ejecutan el lado derecho si el lado izquierdo no es suficiente para determinar el resultado:

iex> false and raise("This error will never be raised")
false
iex> true or raise("This error will never be raised")
true

Además de estos operadores booleanos, Elixir también proporciona ||, && y! que aceptan argumentos de cualquier tipo. Para estos operadores, todos los valores, excepto falso y nulo, se evaluarán como verdaderos:

# or
iex> 1 || true
1
iex> false || 11
11

# and
iex> nil && 13
nil
iex> true && 17
17

# !
iex> !true
false
iex> !1
false
iex> !nil
true

Como regla general, use y, o no y cuando esté esperando booleanos. Si alguno de los argumentos no es booleano, use &&, || y!

También podemos usar ==,! =, ===,! ==, <=,> =, como operadores de comparación:

iex> 1 == 1
true
iex> 1 != 2
true
iex> 1 < 2
true

La diferencia entre == y === es que este último es más estricto:

iex> 1 == 1.0
true
iex> 1 === 1.0
false

También podemos comparar dos tipos de datos diferentes:

iex> 1 < :atom
true

La razón por la que podemos comparar diferentes tipos de datos es el pragmatismo. Los algoritmos de clasificación no necesitan preocuparse por los diferentes tipos de datos para ordenar. El orden de clasificación general se define a continuación:

number < atom < reference < function < port < pid < tuple < map < list < bitstring

En realidad no necesita memorizar este pedido; es suficiente saber que existe este orden.