Rutas
/
Elixir
Elixir
/
Ejercicios
/
Puntos bailarines
Puntos bailarines

Puntos bailarines

Ejercicio de aprendizaje

Introducción

Use

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

La macro __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.

Behaviours

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.

Note

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

Definición 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

Implementación de behaviours

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

Implementaciones de callback por defecto

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.

Instrucciones

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.

1. Define el comportamiento de la animación

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.

2. Proporciona una implementación predeterminada del callback 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]}

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 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}

4. Implementa la animación 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}
Editar en GitHub El enlace se abre en una ventana o pestaña nueva
Elixir Exercism

¿Listo para empezar Puntos bailarines?

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