Track
/
Elixir
Elixir
/
Esercizi
/
Punti danzanti
Punti danzanti

Punti danzanti

Esercizio di apprendimento

Introduzione

Use

La macro use ci permette di estendere rapidamente il nostro modulo con funzionalità fornite da un altro modulo. Quando usiamo use su un modulo, quel modulo può iniettare codice nel nostro: può ad esempio definire funzioni, fare import o alias di altri moduli, oppure impostare attributi di modulo.

Se hai mai dato un'occhiata ai file di test di alcuni esercizi di Elixir qui su Exercism, avrai probabilmente notato che iniziano tutti con use ExUnit.Case. Questa singola riga di codice è ciò che rende disponibili le macro test e assert nel modulo di 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

Cosa succede esattamente quando usi use su un modulo è stabilito dalla macro __using__/1 di quel modulo. Prende un argomento, una lista di parole chiave con le opzioni, e restituisce un'espressione quotata. Il codice di questa espressione quotata viene inserito nel nostro modulo quando chiamiamo 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

Le opzioni possono essere passate come secondo argomento quando si chiama use, ad esempio use ExUnit.Case, async: true. Quando non vengono indicate esplicitamente, il loro valore predefinito è una lista vuota.

Behaviours

I behaviour ci permettono di definire interfacce (insiemi di funzioni e macro) in un modulo behaviour che possono poi essere implementate da diversi moduli callback. Grazie all'interfaccia condivisa, quei moduli callback possono essere usati in modo intercambiabile.

Note

Nota la grafia britannica di «behaviours».

Definire un behaviour

Per definire un behaviour, dobbiamo creare un nuovo modulo e specificare una lista di funzioni che fanno parte dell'interfaccia desiderata. Ogni funzione deve essere definita usando l'attributo di modulo @callback. La sintassi è identica a quella di una typespec di funzione (@spec). Dobbiamo specificare un nome di funzione, una lista di tipi di argomento e tutti i possibili tipi restituiti.

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

Implementare un behaviour

Per aggiungere un behaviour esistente al nostro modulo (creare un modulo callback) usiamo l'attributo di modulo @behaviour. Il suo valore deve essere il nome del modulo behaviour che stiamo aggiungendo.

Poi dobbiamo definire tutte le funzioni (i callback) richieste da quel modulo behaviour. Se stiamo implementando il behaviour di qualcun altro, come i behaviour integrati di Elixir Access o GenServer, troviamo l'elenco di tutti i callback del behaviour nella documentazione su hexdocs.pm.

Un modulo callback non è limitato a implementare solo le funzioni che fanno parte del suo behaviour. È anche possibile che un singolo modulo implementi più behaviour.

Per indicare da quale behaviour proviene ogni funzione, dobbiamo usare l'attributo di modulo @impl prima di ogni funzione. Il suo valore deve essere il nome del modulo behaviour che definisce questo 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

Implementazioni predefinite dei callback

Quando definiamo un behaviour, è possibile fornire un'implementazione predefinita di un callback. Questa implementazione va definita nell'espressione quotata della macro __using__/1. Per permettere a chi usa il modulo behaviour di sovrascrivere l'implementazione predefinita, chiama la macro defoverridable/1 dopo l'implementazione della funzione. Accetta una lista di parole chiave con i nomi delle funzioni come chiavi e le arità delle funzioni come valori.

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

Tieni presente che definire funzioni dentro __using__/1 è sconsigliato per qualsiasi scopo diverso dalla definizione di implementazioni predefinite dei callback, ma puoi sempre definire funzioni in un altro modulo e importarle nella macro __using__/1.

Istruzioni

Il tuo amico, un artista in erba, ti ha contattato con un'idea per un progetto. Uniamo la sua creatività visiva alla tua competenza tecnica. È il momento di cimentarci con l'arte generativa!

I vincoli aiutano la creatività e accorciano le scadenze: avete così deciso di limitare il vostro capolavoro a una sola forma, il cerchio. Però ci saranno molti cerchi. E potranno muoversi! Lo chiamerai... puntini danzanti.

Il tuo amico vorrà sicuramente ideare nuovi ed elaborati movimenti per i puntini, perciò inizierai a programmare creando un'architettura che ti permetterà di definire facilmente nuove animazioni in seguito.

1. Definire il comportamento dell'animazione

Ogni modulo di animazione deve implementare due callback: init/1 e handle_frame/3. Definiscile nel modulo Animation.

Definisci la callback init/1. Deve accettare un argomento di tipo opts e restituire una tupla {:ok, opts} oppure una tupla {:error, error}. Le implementazioni di questa callback controlleranno se le opzioni date sono valide per questo particolare tipo di animazione.

Definisci la callback handle_frame/3. Deve accettare tre argomenti: il puntino, un numero di frame e le opzioni. Deve restituire sempre un puntino. Le implementazioni di questa callback modificheranno gli attributi del puntino in base al numero di frame corrente e alle opzioni dell'animazione.

2. Fornire un'implementazione predefinita della callback init/1

Il comportamento Animation dovrebbe essere facile da incorporare in altri moduli chiamando use DancingDots.Animation.

Per farlo, implementa la macro __using__ nel modulo Animation in modo che imposti il modulo Animation come comportamento dell'altro modulo. Deve anche fornire un'implementazione predefinita della callback init/1. L'implementazione predefinita di init/1 deve restituire le opzioni date invariate.

defmodule MyCustomAnimation do
  use DancingDots.Animation
end

MyCustomAnimation.init(some_option: true)
# => {:ok, [some_option: true]}

3. Implementare l'animazione Flicker

Usa il comportamento Animation per implementare un'animazione a sfarfallio.

Deve usare la callback init/1 predefinita perché non accetta opzioni.

Implementa la callback handle_frame/3, che gestisce un singolo frame. Se il numero di frame è un multiplo di quattro, la funzione deve restituire il puntino con metà della sua opacità originale. Negli altri frame, deve restituire il puntino invariato.

I frame si contano da 1. Il puntino passato a handle_frame/3 è sempre il puntino nel suo stato originale, non nello stato del frame precedente.

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. Implementare l'animazione Zoom

Usa il comportamento Animation per implementare un'animazione di zoom.

Questa animazione accetta un'opzione: velocity. velocity può essere un numero qualsiasi. Se è negativa, il puntino viene rimpicciolito invece che ingrandito.

Implementa la callback init/1. Deve verificare che le opzioni passate siano una keyword list con una chiave :velocity. Il valore di velocity deve essere un numero. Se non è un numero, restituisci l'errore "The :velocity option is required, and its value must be a number. Got: #{inspect(velocity)}".

Implementa la callback handle_frame/3. Deve restituire il puntino con il raggio aumentato di (il numero di frame corrente meno uno) per velocity.

I frame si contano da 1. Il puntino passato a handle_frame/3 è sempre il puntino nel suo stato originale, non nello stato del frame precedente.

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}
Modifica tramite GitHub Il link si apre in una nuova finestra o scheda
Elixir Exercism

Vuoi iniziare Punti danzanti?

Iscriviti a Exercism per imparare e padroneggiare Elixir con 58 concetti168 esercizi e il mentoring di persone reali, tutto gratis.