Percursos
/
Elixir
Elixir
/
Exercícios
/
Pontos dançantes
Pontos dançantes

Pontos dançantes

Exercício de aprendizagem

Introdução

Use

A macro use permite-nos estender rapidamente o nosso módulo com funcionalidade fornecida por outro módulo. Quando usas use num módulo, esse módulo pode injetar código no nosso: pode, por exemplo, definir funções, fazer import ou alias de outros módulos, ou definir atributos do módulo.

Se alguma vez olhaste para os ficheiros de teste de alguns dos exercícios de Elixir aqui no Exercism, deves ter reparado que começam todos com use ExUnit.Case. É esta única linha de código que torna as macros test e assert disponíveis no módulo de teste.

defmodule LasagnaTest do
  use ExUnit.Case

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

A macro __using__/1

O que acontece exatamente quando usas use num módulo é ditado pela macro __using__/1 desse módulo. Esta macro recebe um argumento, uma lista de palavras-chave com opções, e devolve uma expressão citada. O código dessa expressão citada é inserido no nosso módulo quando chamas 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

As opções podem ser indicadas como segundo argumento ao chamar use, por exemplo, use ExUnit.Case, async: true. Quando não são indicadas explicitamente, o valor por omissão é uma lista vazia.

Comportamentos

Os comportamentos permitem-nos definir interfaces (conjuntos de funções e macros) num módulo de comportamento que pode depois ser implementado por diferentes módulos de callback. Graças à interface partilhada, esses módulos de callback podem ser usados de forma intercambiável.

Note

Repara na grafia britânica de "behaviours".

Definir comportamentos

Para definir um comportamento, temos de criar um novo módulo e especificar uma lista de funções que fazem parte da interface pretendida. Cada função tem de ser definida com o atributo de módulo @callback. A sintaxe é idêntica à de uma especificação de tipo de função (@spec). Temos de especificar o nome da função, uma lista de tipos de argumentos e todos os tipos que a função pode devolver.

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

Implementar comportamentos

Para adicionar um comportamento existente ao nosso módulo (criar um módulo de callback), usamos o atributo de módulo @behaviour. O seu valor deve ser o nome do módulo de comportamento que estamos a adicionar.

Depois, temos de definir todas as funções (callbacks) que esse módulo de comportamento exige. Se estivermos a implementar o comportamento de outra pessoa, como os comportamentos Access ou GenServer incluídos no Elixir, encontraremos a lista de todos os callbacks desse comportamento na documentação em hexdocs.pm.

Um módulo de callback não está limitado a implementar apenas as funções que fazem parte do seu comportamento. Também é possível que um único módulo implemente vários comportamentos.

Para assinalar que função vem de que comportamento, devemos usar o atributo de módulo @impl antes de cada função. O seu valor deve ser o nome do módulo de comportamento 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

Implementações de callback por omissão

Ao definir um comportamento, é possível fornecer uma implementação por omissão de um callback. Essa implementação deve ser definida na expressão citada da macro __using__/1. Para que quem usa o módulo de comportamento possa substituir a implementação por omissão, chama a macro defoverridable/1 depois da implementação da função. Esta macro aceita uma lista de palavras-chave em que as chaves são nomes de funções e os valores são as aridades dessas funções.

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

Nota que definir funções dentro de __using__/1 é desaconselhado para qualquer fim que não seja definir implementações de callback por omissão, mas podes sempre definir funções noutro módulo e importá-las na macro __using__/1.

Instruções

O teu amigo, um aspirante a artista, contactou-te com uma ideia de projeto. Vamos juntar a criatividade visual dele à tua experiência técnica. Está na hora de experimentares arte generativa!

As restrições ajudam a criatividade e encurtam os prazos dos projetos, por isso combinaram os dois limitar a vossa obra-prima a uma única forma: o círculo. Mas vai haver muitos círculos. E podem mover-se! Vais chamar-lhe... pontos dançantes.

O teu amigo vai certamente querer inventar novos movimentos elaborados para os pontos, por isso vais começar a programar criando uma arquitetura que te permita definir novas animações facilmente mais tarde.

1. Define o comportamento da animação

Cada módulo de animação tem de implementar dois callbacks: init/1 e handle_frame/3. Define-os no módulo Animation.

Define o callback init/1. Deve receber um argumento do tipo opts e devolver um tuplo {:ok, opts} ou um tuplo {:error, error}. As implementações deste callback verificam se as opções dadas são válidas para este tipo específico de animação.

Define o callback handle_frame/3. Deve receber três argumentos: o ponto, um número de frame e as opções. Deve devolver sempre um ponto. As implementações deste callback modificam os atributos do ponto com base no número do frame atual e nas opções da animação.

2. Fornece uma implementação predefinida do callback init/1

O comportamento Animation deve ser fácil de incorporar noutros módulos, bastando chamar use DancingDots.Animation.

Para isso, implementa a macro __using__ no módulo Animation, de modo a definir o módulo Animation como o comportamento do outro módulo. Deve também fornecer uma implementação predefinida do callback init/1. A implementação predefinida de init/1 deve devolver as opções dadas sem alterações.

defmodule MyCustomAnimation do
  use DancingDots.Animation
end

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

3. Implementa a animação Flicker

Usa o comportamento Animation para implementar uma animação intermitente.

Deve usar o callback init/1 predefinido, porque não recebe opções.

Implementa o callback handle_frame/3, que trata de um único frame. Se o número do frame for múltiplo de quatro, a função deve devolver o ponto com metade da sua opacidade original. Nos restantes frames, deve devolver o ponto inalterado.

Os frames contam-se a partir de 1. O ponto passado a handle_frame/3 está sempre no seu estado original, não no estado do frame 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 a animação Zoom

Usa o comportamento Animation para implementar uma animação de zoom.

Esta animação recebe uma opção: a velocidade. A velocidade pode ser qualquer número. Se for negativa, o ponto é reduzido em vez de ampliado.

Implementa o callback init/1. Deve validar que as opções passadas são uma lista de palavras-chave com uma chave :velocity. O valor da velocidade tem de ser um número. Se não for um número, devolve o erro "The :velocity option is required, and its value must be a number. Got: #{inspect(velocity)}".

Implementa o callback handle_frame/3. Deve devolver o ponto com o raio aumentado pelo número do frame atual, menos um, multiplicado pela velocidade.

Os frames contam-se a partir de 1. O ponto passado a handle_frame/3 está sempre no seu estado original, não no estado do frame 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 via GitHub A ligação abre numa nova janela ou separador
Elixir Exercism

Estás pronto para começar Pontos dançantes?

Inscreve-te no Exercism para aprenderes e dominares Elixir com 58 conceitos168 exercícios, e mentoria humana real, tudo grátis.