La macro use nos permite ampliar rápidamente nuestro módulo con funcionalidad que ofrece otro módulo. Cuando hacemos use de un módulo, ese módulo puede inyectar código en el nuestro; por ejemplo, puede definir funciones, hacer import o alias de otros módulos, o establecer atributos de módulo.
Si alguna vez has mirado los ficheros de test de alguno de los ejercicios de Elixir aquí en Exercism, lo más probable es que te hayas dado cuenta de que todos empiezan con use ExUnit.Case. Esta única línea de código es lo que hace que las macros test y assert estén disponibles en el módulo de test.
defmodule LasagnaTest do
use ExUnit.Case
test "expected minutes in oven" do
assert Lasagna.expected_minutes_in_oven() === 40
end
end
__using__/1
Lo que ocurre exactamente cuando haces use de un módulo lo dicta la macro __using__/1 de ese módulo. Recibe un argumento, una lista de palabras clave con opciones, y devuelve una expresión citada. El código de esta expresión citada se inserta en nuestro módulo al llamar a use.
defmodule ExUnit.Case do
defmacro __using__(opts) do
# some real-life ExUnit code omitted here
quote do
import ExUnit.Assertions
import ExUnit.Case, only: [describe: 2, test: 1, test: 2, test: 3]
end
end
end
Las opciones se pueden pasar como segundo argumento al llamar a use, por ejemplo use ExUnit.Case, async: true. Cuando no se indican explícitamente, toman por defecto el valor de una lista vacía.
Los behaviours nos permiten definir interfaces (conjuntos de funciones y macros) en un módulo de behaviour que más tarde pueden implementar distintos módulos de callback. Gracias a la interfaz compartida, esos módulos de callback se pueden usar de forma intercambiable.
Fíjate en la ortografía británica de «behaviours».
Para definir un behaviour, tenemos que crear un módulo nuevo y especificar una lista de funciones que forman parte de la interfaz deseada. Cada función debe definirse con el atributo de módulo @callback. La sintaxis es idéntica a la de un typespec de función (@spec). Tenemos que indicar el nombre de la función, una lista de tipos de argumento y todos los posibles tipos de retorno.
defmodule Countable do
@callback count(collection :: any) :: pos_integer
end
Para añadir un behaviour existente a nuestro módulo (crear un módulo de callback) usamos el atributo de módulo @behaviour. Su valor debe ser el nombre del módulo de behaviour que estamos añadiendo.
Después, tenemos que definir todas las funciones (callbacks) que requiere ese módulo de behaviour. Si implementamos el behaviour de otra persona, como los behaviours integrados de Elixir Access o GenServer, encontraremos la lista de todos los callbacks del behaviour en la documentación de hexdocs.pm.
Un módulo de callback no se limita a implementar solo las funciones que forman parte de su behaviour. También es posible que un mismo módulo implemente varios behaviours.
Para marcar qué función procede de qué behaviour, debemos usar el atributo de módulo @impl antes de cada función. Su valor debe ser el nombre del módulo de behaviour que define este callback.
defmodule BookCollection do
@behaviour Countable
defstruct [:list, :owner]
@impl Countable
def count(collection) do
Enum.count(collection.list)
end
def mark_as_read(collection, book) do
# other function unrelated to the Countable behaviour
end
end
Al definir un behaviour, es posible proporcionar una implementación por defecto de un callback. Esta implementación debe definirse en la expresión citada de la macro __using__/1. Para que los usuarios del módulo de behaviour puedan sobrescribir la implementación por defecto, llama a la macro defoverridable/1 después de la implementación de la función. Acepta una lista de palabras clave con nombres de función como claves y aridades de función como valores.
defmodule Countable do
@callback count(collection :: any) :: pos_integer
defmacro __using__(_) do
quote do
@behaviour Countable
def count(collection), do: Enum.count(collection)
defoverridable count: 1
end
end
end
Ten en cuenta que definir funciones dentro de __using__/1 no es recomendable para ningún otro propósito que no sea definir implementaciones de callback por defecto, pero siempre puedes definir funciones en otro módulo e importarlas en la macro __using__/1.
Tu amigo, un aspirante a artista, se puso en contacto contigo con una idea para un proyecto. Vamos a combinar su creatividad visual con tu experiencia técnica. ¡Es hora de experimentar con el arte generativo!
Las restricciones ayudan a la creatividad y acortan los plazos de los proyectos, así que ambos habéis acordado limitar vuestra obra maestra a una sola forma: el círculo. Pero va a haber muchos círculos. ¡Y pueden moverse! Lo llamaréis... puntos bailarines.
Tu amigo querrá sin duda idear nuevos movimientos elaborados para los puntos, así que empezarás a programar creando una arquitectura que te permita definir después nuevas animaciones con facilidad.
Cada módulo de animación debe implementar dos callbacks: init/1 y handle_frame/3. Defínelos en el módulo Animation.
Define el callback init/1. Debe recibir un argumento de tipo opts y devolver una tupla {:ok, opts} o una tupla {:error, error}. Las implementaciones de este callback comprobarán si las opciones dadas son válidas para este tipo concreto de animación.
Define el callback handle_frame/3. Debe recibir tres argumentos: el punto, un número de fotograma y las opciones. Siempre debe devolver un punto. Las implementaciones de este callback modificarán los atributos del punto según el número de fotograma actual y las opciones de la animación.
init/1
El comportamiento Animation debería ser fácil de incorporar a otros módulos llamando a use DancingDots.Animation.
Para lograrlo, implementa la macro __using__ en el módulo Animation de forma que establezca el módulo Animation como el comportamiento del otro módulo. También debe proporcionar una implementación predeterminada del callback init/1. La implementación predeterminada de init/1 debe devolver las opciones dadas sin cambios.
defmodule MyCustomAnimation do
use DancingDots.Animation
end
MyCustomAnimation.init(some_option: true)
# => {:ok, [some_option: true]}
Flicker
Usa el comportamiento Animation para implementar una animación parpadeante.
Debe usar el callback init/1 predeterminado porque no recibe ninguna opción.
Implementa el callback handle_frame/3, que gestiona un único fotograma. Si el número de fotograma es múltiplo de cuatro, la función debe devolver el punto con la mitad de su opacidad original. En los demás fotogramas, debe devolver el punto sin cambios.
Los fotogramas se cuentan a partir de 1. El punto que se pasa a handle_frame/3 es siempre el punto en su estado original, no en el estado del fotograma anterior.
dot = %DancingDots.Dot{x: 100, y: 100, radius: 24, opacity: 1}
DancingDots.Flicker.handle_frame(dot, 1, [])
# => %DancingDots.Dot{opacity: 1, radius: 24, x: 100, y: 100}
DancingDots.Flicker.handle_frame(dot, 4, [])
# => %DancingDots.Dot{opacity: 0.5, radius: 24, x: 100, y: 100}
Zoom
Usa el comportamiento Animation para implementar una animación de zoom.
Esta animación recibe una opción: la velocidad. La velocidad puede ser cualquier número. Si es negativa, el punto se aleja en lugar de acercarse.
Implementa el callback init/1. Debe validar que las opciones pasadas sean una lista de palabras clave con una clave :velocity. El valor de la velocidad debe ser un número. Si no es un número, devuelve el error "The :velocity option is required, and its value must be a number. Got: #{inspect(velocity)}".
Implementa el callback handle_frame/3. Debe devolver el punto con su radio incrementado en el número de fotograma actual menos uno, multiplicado por la velocidad.
Los fotogramas se cuentan a partir de 1. El punto que se pasa a handle_frame/3 es siempre el punto en su estado original, no en el estado del fotograma anterior.
DancingDots.Zoom.init(velocity: nil)
# => {:error, "The :velocity option is required, and its value must be a number. Got: nil"}
dot = %DancingDots.Dot{x: 100, y: 100, radius: 24, opacity: 1}
DancingDots.Zoom.handle_frame(dot, 1, velocity: 10)
# => %DancingDots.Dot{radius: 24, opacity: 1, x: 100, y: 100}
DancingDots.Zoom.handle_frame(dot, 2, velocity: 10)
# => %DancingDots.Dot{radius: 34, opacity: 1, x: 100, y: 100}
Regístrate en Exercism para aprender y dominar Elixir con 58 conceptos168 ejercicios y mentoría humana real, todo gratis.