Tracks
/
Elixir
Elixir
/
Ejercicios
/
Puntos danzantes
Puntos danzantes

Puntos danzantes

Ejercicio de aprendizaje

Introducción

Uso

La macro use nos permite extender rápidamente nuestro módulo con funcionalidad que proporciona 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 miraste los archivos de prueba de algunos de los ejercicios de Elixir aquí en Exercism, lo más probable es que hayas notado que todos comienzan con use ExUnit.Case. Esta sola línea de código es lo que hace que las macros test y assert estén disponibles en el módulo de prueba.

defmodule LasagnaTest do
  use ExUnit.Case

  test "expected minutes in oven" do
    assert Lasagna.expected_minutes_in_oven() === 40
  end
end

Macro __using__/1

Lo que sucede 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 dan de forma explícita, su valor predeterminado es una lista vacía.

Comportamientos

Los comportamientos nos permiten definir interfaces (conjuntos de funciones y macros) en un módulo de comportamiento que luego pueden implementar diferentes módulos de callback. Gracias a la interfaz compartida, esos módulos de callback se pueden usar de manera intercambiable.

Note

Fíjate en la ortografía británica de «behaviours».

Definir comportamientos

Para definir un comportamiento, necesitamos crear un módulo nuevo y especificar una lista de funciones que forman parte de la interfaz deseada. Cada función se debe definir con el atributo de módulo @callback. La sintaxis es idéntica a la de una especificación de tipo de función (@spec). Necesitamos especificar un nombre de función, una lista de tipos de argumentos y todos los tipos de retorno posibles.

defmodule Countable do
  @callback count(collection :: any) :: pos_integer
end

Implementar comportamientos

Para agregar un comportamiento 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 comportamiento que estamos agregando.

Luego, debemos definir todas las funciones (callbacks) que requiere ese módulo de comportamiento. Si estamos implementando el comportamiento de otra persona, como los comportamientos integrados de Elixir Access o GenServer, encontraríamos la lista de todos los callbacks del comportamiento en la documentación en hexdocs.pm.

Un módulo de callback no está limitado a implementar solo las funciones que forman parte de su comportamiento. También es posible que un solo módulo implemente varios comportamientos.

Para marcar qué función proviene de qué comportamiento, debemos usar el atributo de módulo @impl antes de cada función. Su valor debe ser el nombre del módulo de comportamiento 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

Implementaciones de callback predeterminadas

Al definir un comportamiento, es posible proporcionar una implementación predeterminada de un callback. Esta implementación se debe definir en la expresión citada de la macro __using__/1. Para que los usuarios del módulo de comportamiento puedan sobrescribir la implementación predeterminada, 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 funciones como claves y aridades de funciones 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 se recomienda para ningún otro propósito que no sea definir implementaciones de callback predeterminadas, pero siempre puedes definir funciones en otro módulo e importarlas en la macro __using__/1.

Instrucciones

Tu amigo, un artista en ciernes, 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 incursionar en el arte generativo!

Las restricciones ayudan a la creatividad y acortan los plazos de los proyectos, así que ambos acordaron limitar su obra maestra a una sola forma: el círculo. Pero habrá muchos círculos. ¡Y pueden moverse! Lo llamarás... puntos bailarines.

Tu amigo sin duda querrá idear nuevos movimientos elaborados para los puntos, así que empezarás a programar creando una arquitectura que te permita definir nuevas animaciones fácilmente más adelante.

1. Define el comportamiento de la animación

Cada módulo de animación necesita 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 verificarán si las opciones dadas son válidas para este tipo particular 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.

2. Proporciona una implementación predeterminada del callback init/1

El comportamiento Animation debería ser fácil de incorporar en otros módulos llamando a use DancingDots.Animation.

Para lograrlo, implementa la macro __using__ en el módulo Animation de modo 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]}

3. Implementa la animación 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 se encarga de un solo 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 siempre es 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}

4. Implementa la animación Zoom

Usa el comportamiento Animation para implementar una animación de acercamiento.

Esta animación recibe una sola 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 aumentado 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 siempre es 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}
Editar en GitHub El enlace se abre en una ventana o una pestaña nuevas
Elixir Exercism

¿Todo listo para empezar Puntos danzantes?

Regístrate en Exercism para aprender y dominar Elixir con 58 conceptos168 ejercicios y mentoría humana real, todo gratis.