Composición
Sabemos por experiencia que es incontrolable tener todas nuestras funciones en el mismo archivo y alcance. En esta sección cubriremos cómo agrupar funciones y definir un mapa especializado conocido como estructura (struct), con el propósito de organizar nuestro código de manera eficiente.
Modulos
Los módulos son la mejor manera de organizar funciones en un namespace. En adición a las funciones agrupativas, los módulos nos permiten definir funciones nombradas y privadas, las cuales cubrimos en la lección pasada.
Démosle un vistazo a un ejemplo básico:
defmodule Example do
  def greeting(name) do
    "Hello #{name}."
  end
end
iex> Example.greeting "Sean"
"Hello Sean."Es posible anidar módulos en Elixir, permitiéndonos ser explícitos nombrando nuestra funcionalidad.
defmodule Example.Greetings do
  def morning(name) do
    "Good morning #{name}."
  end
  def evening(name) do
    "Good night #{name}."
  end
end
iex> Example.Greetings.morning "Sean"
"Good morning Sean."Atributos de un Módulo
Los atributos de un módulo son comúnmente usados como constantes en Elixir. Démosle un vistazo al siguiente ejemplo:
defmodule Example do
  @greeting "Hello"
  def greeting(name) do
    ~s(#{@greeting} #{name}.)
  end
endEs importante destacar que hay atributos reservados en Elixir. Los tres más comunes son:
- 
moduledoc— Documenta el módulo actual.
- 
doc— Documentación para funciones y macros.
- 
behaviour— Usa OTP o comportamiento definido por el usuario.
Estructuras
Las estructuras son mapas especiales con un conjunto definido de claves y valores por defecto. Deben ser definidas dentro de un módulo, y tomarán su nombre. Es común que una estructura sea definida únicamente dentro de un módulo.
Para definir una estructura utilizamos defstruct junto con una lista de claves y valores por defecto:
defmodule Example.User do
  defstruct name: "Sean", roles: []
endAhora, creemos estructuras:
iex> %Example.User{}
%Example.User<name: "Sean", roles: [], ...>
iex> %Example.User{name: "Steve"}
%Example.User<name: "Steve", roles: [], ...>
iex> %Example.User{name: "Steve", roles: [:manager]}
%Example.User<name: "Steve", roles: [:manager]>Podemos actualizar una estructura justo como lo hacemos con un mapa:
iex> steve = %Example.User{name: "Steve"}
%Example.User<name: "Steve", roles: [...], ...>
iex> sean = %{steve | name: "Sean"}
%Example.User<name: "Sean", roles: [...], ...>Algo muy importante es que podemos hacer coincidencia entre estructuras y mapas:
iex> %{name: "Sean"} = sean
%Example.User<name: "Sean", roles: [...], ...>
A partir de Elixir 1.8 las estructuras incluyen la inspección personalizada.
Para entender que significa esto y cómo debemos usarlo, debemos inspeccionar el mapa sean:
iex> inspect(sean)
"%Example.User<name: \"Sean\", roles: [...], ...>"
Todos nuestros campos están presentes, lo que esta bien para este ejemplo, pero ¿qué pasaría si tuviéramos un campo protegido que no quisiéramos incluir?
¡La nueva característica @derive nos permite lograr esto!
Actualicemos nuestro ejemplo para que roles ya no sea incluido en la salida:
defmodule Example.User do
  @derive {Inspect, only: [:name]}
  defstruct name: nil, roles: []
end
Nota: también podríamos usar @derive {Inspect, except: [:roles]}, son equivalentes.
Con nuestro módulo actualizado, echemos un vistazo a lo que sucede en iex:
iex> sean = %Example.User<name: "Sean", roles: [...], ...>
%Example.User<name: "Sean", ...>
iex> inspect(sean)
"%Example.User<name: \"Sean\", ...>"
Los roles son excluidos de la salida.
Composición
Ahora que sabemos cómo crear módulos y estructuras, aprendamos a agregarle funciones existentes a través de la composición. Elixir nos proporciona una variedad de formas diferentes de interactuar con otros módulos.
alias
Nos permite darle un alias a los módulos, que son usados frecuentemente en Elixir.
defmodule Sayings.Greetings do
  def basic(name), do: "Hi, #{name}"
end
defmodule Example do
  alias Sayings.Greetings
  def greeting(name), do: Greetings.basic(name)
end
# Without alias
defmodule Example do
  def greeting(name), do: Sayings.Greetings.basic(name)
end
Si hay un conflicto entre dos alias o quieres que los alias tomen un nombre diferente, podemos utilizar la opción :as
defmodule Example do
  alias Sayings.Greetings, as: Hi
  def print_message(name), do: Hi.basic(name)
endEs posible dar múltiples alias a un módulo a la vez:
defmodule Example do
  alias Sayings.{Greetings, Farewells}
endimport
Si queremos importar las funciones y macros de un módulo, más que sólo darle un alias, podemos utilizar import/:
iex> last([1, 2, 3])
** (CompileError) iex:9: undefined function last/1
iex> import List
nil
iex> last([1, 2, 3])
3Filtrado
Por defecto, todas las funciones y macros son importadas, pero podemos filtarlas utilizando las opciones :only y :except
Empecemos por importar únicamente la función last/1
iex> import List, only: [last: 1]
iex> first([1, 2, 3])
** (CompileError) iex:13: undefined function first/1
iex> last([1, 2, 3])
3
Si importamos todo excepto last/1 e intentamos utilizar la misma función:
iex> import List, except: [last: 1]
nil
iex> first([1, 2, 3])
1
iex> last([1, 2, 3])
** (CompileError) iex:3: undefined function last/1
En adición a los pares nombre/aridad, hay dos átomos especiales, :functions y :macros, las cuales importan únicamente funciones y macros, respectivamente:
import List, only: :functions
import List, only: :macrosrequire
Aunque require/2 no es usado frecuentemente, es bastante importante. Haciendo require de un módulo asegura que está compilado y cargado. Esto es muy útil cuando necesitamos acceso a las macros de un módulo:
defmodule Example do
  require SuperMacros
  SuperMacros.do_stuff()
endSi intentamos hacer un llamado a una macro que no está cargada aún, Elixir lanzará un error.
use
Con el macro use podemos habilitar otro módulo para modificar la definición de nuestro módulo actual.
Cuando llamamos use en nuestro código, en realidad estamos invocando el callback __using__/1 definido por el módulo utilizado.
El resultado de __using__/1 se convierte en parte de la definición de nuestro módulo.
Para comprender mejor cómo funciona esto, veamos un ejemplo simple:
defmodule Hello do
  defmacro __using__(_opts) do
    quote do
      def hello(name), do: "Hi, #{name}"
    end
  end
end
Aquí hemos creado un módulo Hello que define el callback __using__/1 dentro del cual definimos una función hello/1.
Vamos a crear un nuevo módulo para que podamos probar nuestro nuevo código:
defmodule Example do
  use Hello
end
Si probamos nuestro código en IEx, veremos que hello/1 esta disponible en el módulo Example:
iex> Example.hello("Sean")
"Hi, Sean"
Aquí podemos ver que use invocó el callback __using__/1 del módulo Hello que a su ves agrego el código resultante a nuestro módulo.
Ahora que hemos demostrado un ejemplo básico, actualicemos nuestro código para ver cómo __using__/1 admite opciones.
Haremos esto agregando la opción greeting:
defmodule Hello do
  defmacro __using__(opts) do
    greeting = Keyword.get(opts, :greeting, "Hi")
    quote do
      def hello(name), do: unquote(greeting) <> ", " <> name
    end
  end
end
Actualicemos nuestro módulo Example para incluir la opción greeting recién creada:
defmodule Example do
  use Hello, greeting: "Hola"
end
Si lo probamos en IEx, deberiamos ver que el saludo cambio:
iex> Example.hello("Sean")
"Hola, Sean"
Estos son ejemplos simples para mostrar como funciona use, pero es una herramienta increíblemente poderosa en las herramientas de Elixir.
A medida que continúe aprendiendo acerca de Elixir, este atento a use, un ejemplo que seguramente vera es use ExUnit.Case, async: true.
Nota: quote, alias, use, require son macros utilizadas cuando trabajamos con metaprogramming.
¿Encontraste un error o quieres contribuir a la lección? ¡Edita esta lección en GitHub!