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

Pontos dançantes

Exercício de aprendizagem

Introdução

Uso

O macro use permite estender rapidamente nosso módulo com funcionalidades fornecidas por outro módulo. Quando usamos use em um módulo, esse módulo pode injetar código no nosso: ele pode, por exemplo, definir funções, fazer import ou alias de outros módulos, ou definir atributos de módulo.

Se você já olhou os arquivos de teste de alguns dos exercícios de Elixir aqui no Exercism, provavelmente notou que todos começam com use ExUnit.Case. É essa única linha de código que disponibiliza os macros test e assert 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

O macro __using__/1

O que exatamente acontece quando você usa use em um módulo é ditado pelo macro __using__/1 desse módulo. Ele recebe um argumento, uma lista de palavras-chave com opções, e retorna uma quoted expression. O código dessa quoted expression é inserido no nosso módulo quando chamamos 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 passadas como segundo argumento ao chamar use, por exemplo use ExUnit.Case, async: true. Quando não são passadas explicitamente, o padrão é uma lista vazia.

Behaviours

Behaviours permitem definir interfaces (conjuntos de funções e macros) em um módulo de behaviour que depois podem ser implementadas por diferentes módulos de callback. Graças à interface compartilhada, esses módulos de callback podem ser usados de forma intercambiável.

Note

Repare na grafia britânica de "behaviours".

Definindo behaviours

Para definir um behaviour, precisamos criar um novo módulo e especificar uma lista de funções que fazem parte da interface desejada. Cada função precisa ser definida usando o atributo de módulo @callback. A sintaxe é idêntica à de uma typespec de função (@spec). Precisamos especificar o nome da função, uma lista de tipos de argumento e todos os tipos de retorno possíveis.

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

Implementando behaviours

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

Depois, precisamos definir todas as funções (callbacks) exigidas por esse módulo de behaviour. Se estivermos implementando o behaviour de outra pessoa, como os behaviours Access ou GenServer nativos do Elixir, vamos encontrar a lista de todos os callbacks desse behaviour na documentação em hexdocs.pm.

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

Para marcar de qual behaviour vem cada função, devemos usar o atributo de módulo @impl antes de cada função. Seu valor deve ser o nome do módulo de behaviour que define esse 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 padrão de callbacks

Ao definir um behaviour, é possível fornecer uma implementação padrão de um callback. Essa implementação deve ser definida na quoted expression do macro __using__/1. Para permitir que quem usa o módulo de behaviour substitua a implementação padrão, chame o macro defoverridable/1 depois da implementação da função. Ele aceita uma lista de palavras-chave com nomes de funções como chaves e aridades de função 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

Vale notar que definir funções dentro de __using__/1 não é recomendado para nenhum outro propósito além de definir implementações padrão de callbacks, mas você sempre pode definir funções em outro módulo e importá-las no macro __using__/1.

Instruções

Seu amigo, um aspirante a artista, entrou em contato com você com uma ideia de projeto. Vamos combinar a criatividade visual dele com o seu conhecimento técnico. É hora de se aventurar na arte generativa!

Restrições ajudam a criatividade e encurtam os prazos dos projetos, então vocês dois concordaram em limitar sua obra-prima a uma única forma: o círculo. Mas vão ser muitos círculos. E eles podem se movimentar! Você vai chamar isso de... pontos dançantes.

Seu amigo com certeza vai querer criar novos movimentos elaborados para os pontos, então você vai começar a programar criando uma arquitetura que permita definir novas animações com facilidade mais tarde.

1. Defina o comportamento da animação

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

Defina o callback init/1. Ele deve receber um argumento do tipo opts e retornar uma tupla {:ok, opts} ou uma tupla {:error, error}. As implementações desse callback vão verificar se as opções fornecidas são válidas para esse tipo específico de animação.

Defina o callback handle_frame/3. Ele deve receber três argumentos: o ponto, um número de quadro e as opções. Ele deve sempre retornar um ponto. As implementações desse callback vão modificar os atributos do ponto com base no número do quadro atual e nas opções da animação.

2. Forneça uma implementação padrão do callback init/1

O comportamento Animation deve ser fácil de incorporar em outros módulos chamando use DancingDots.Animation.

Para que isso aconteça, implemente a macro __using__ no módulo Animation de forma que ele defina o módulo Animation como o comportamento do outro módulo. Ele também deve fornecer uma implementação padrão do callback init/1. A implementação padrão de init/1 deve retornar as opções fornecidas sem alterações.

defmodule MyCustomAnimation do
  use DancingDots.Animation
end

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

3. Implemente a animação Flicker

Use o comportamento Animation para implementar uma animação cintilante.

Ela deve usar o callback init/1 padrão porque não recebe nenhuma opção.

Implemente o callback handle_frame/3, que lida com um único quadro. Se o número do quadro for múltiplo de quatro, a função deve retornar o ponto com metade da sua opacidade original. Nos outros quadros, ela deve retornar o ponto sem alterações.

Os quadros são contados a partir de 1. O ponto passado para handle_frame/3 é sempre o ponto em seu estado original, não no estado do quadro 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. Implemente a animação Zoom

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

Essa animação recebe uma opção: a velocidade. A velocidade pode ser qualquer número. Se for negativa, o ponto sofre zoom out em vez de zoom in.

Implemente o callback init/1. Ele deve validar que as opções passadas são uma keyword list com uma chave :velocity. O valor da velocidade deve ser um número. Se não for um número, retorne o erro "The :velocity option is required, and its value must be a number. Got: #{inspect(velocity)}".

Implemente o callback handle_frame/3. Ele deve retornar o ponto com seu raio aumentado pelo número do quadro atual, menos um, vezes a velocidade.

Os quadros são contados a partir de 1. O ponto passado para handle_frame/3 é sempre o ponto em seu estado original, não no estado do quadro 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 O link abre em uma nova janela ou aba
Elixir Exercism

Tudo pronto para começar Pontos dançantes?

Crie sua conta no Exercism para aprender e dominar Elixir com 58 conceitos168 exercícios e mentoria humana de verdade, tudo de graça.