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
__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 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.
Repare na grafia britânica de "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
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
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.
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.
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.
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]}
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}
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}
Crie sua conta no Exercism para aprender e dominar Elixir com 58 conceitos168 exercícios e mentoria humana de verdade, tudo de graça.