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
__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.
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.
Repara na grafia britânica de "behaviours".
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
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
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.
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.
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.
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]}
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}
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}
Inscreve-te no Exercism para aprenderes e dominares Elixir com 58 conceitos168 exercícios, e mentoria humana real, tudo grátis.