La macro use permet d'étendre rapidement notre module avec des fonctionnalités fournies par un autre module. Quand on use un module, ce module peut injecter du code dans notre module : il peut par exemple définir des fonctions, faire un import ou un alias d'autres modules, ou définir des attributs de module.
Si tu as déjà jeté un œil aux fichiers de test de certains exercices Elixir ici, sur Exercism, tu as sans doute remarqué qu'ils commencent tous par use ExUnit.Case. Cette seule ligne de code suffit à rendre les macros test et assert disponibles dans le module de test.
defmodule LasagnaTest do
use ExUnit.Case
test "expected minutes in oven" do
assert Lasagna.expected_minutes_in_oven() === 40
end
end
__using__/1
Ce qui se passe exactement quand on use un module est dicté par la macro __using__/1 de ce module. Elle prend un argument, une liste de mots-clés contenant les options, et renvoie une expression quotée. Le code de cette expression quotée est inséré dans notre module lors de l'appel à 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
Les options peuvent être passées en deuxième argument lors de l'appel à use, par exemple use ExUnit.Case, async: true. Si on ne les fournit pas explicitement, elles valent par défaut une liste vide.
Les behaviours permettent de définir des interfaces (des ensembles de fonctions et de macros) dans un module de behaviour qui peut ensuite être implémenté par différents modules de callback. Grâce à l'interface partagée, ces modules de callback sont interchangeables.
Notez l'orthographe britannique de « behaviours ».
Pour définir un behaviour, il faut créer un nouveau module et préciser la liste des fonctions qui font partie de l'interface souhaitée. Chaque fonction doit être définie à l'aide de l'attribut de module @callback. La syntaxe est identique à celle d'une spécification de type de fonction (@spec). Il faut préciser le nom de la fonction, une liste de types d'arguments et tous les types de retour possibles.
defmodule Countable do
@callback count(collection :: any) :: pos_integer
end
Pour ajouter un behaviour existant à notre module (et donc créer un module de callback), on utilise l'attribut de module @behaviour. Sa valeur doit être le nom du module de behaviour que l'on ajoute.
Ensuite, il faut définir toutes les fonctions (callbacks) exigées par ce module de behaviour. Si on implémente le behaviour de quelqu'un d'autre, comme les behaviours Access ou GenServer fournis avec Elixir, on trouve la liste de tous les callbacks de ce behaviour dans la documentation sur hexdocs.pm.
Un module de callback n'est pas limité aux fonctions qui font partie de son behaviour. Un même module peut aussi implémenter plusieurs behaviours.
Pour indiquer de quel behaviour vient chaque fonction, on utilise l'attribut de module @impl avant chaque fonction. Sa valeur doit être le nom du module de behaviour qui définit ce 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
Lorsqu'on définit un behaviour, il est possible de fournir une implémentation par défaut d'un callback. Cette implémentation doit être définie dans l'expression quotée de la macro __using__/1. Pour permettre aux utilisateurs du module de behaviour de remplacer l'implémentation par défaut, appelle la macro defoverridable/1 après l'implémentation de la fonction. Elle prend une liste de mots-clés dont les clés sont des noms de fonctions et les valeurs des arités de fonctions.
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
Note que définir des fonctions à l'intérieur de __using__/1 est déconseillé pour tout autre usage que définir des implémentations de callbacks par défaut, mais tu peux toujours définir des fonctions dans un autre module et les importer dans la macro __using__/1.
Ton ami, un artiste en herbe, t'a contacté avec une idée de projet. Combinons sa créativité visuelle et tes compétences techniques. Il est temps de t'essayer à l'art génératif !
Les contraintes stimulent la créativité et raccourcissent les délais, alors vous avez décidé tous les deux de limiter votre chef-d'œuvre à une seule forme : le cercle. Mais il y aura beaucoup de cercles. Et ils pourront se déplacer ! Tu appelleras ça... des points dansants.
Ton ami voudra sûrement imaginer de nouveaux mouvements élaborés pour les points, alors tu vas commencer par coder une architecture qui te permettra de définir facilement de nouvelles animations par la suite.
Chaque module d'animation doit implémenter deux callbacks : init/1 et handle_frame/3. Définis-les dans le module Animation.
Définis le callback init/1. Il doit prendre un argument de type opts et renvoyer soit un tuple {:ok, opts}, soit un tuple {:error, error}. Les implémentations de ce callback vérifieront si les options fournies sont valides pour ce type d'animation particulier.
Définis le callback handle_frame/3. Il doit prendre trois arguments : le point, un numéro de frame et des options. Il doit toujours renvoyer un point. Les implémentations de ce callback modifieront les attributs du point en fonction du numéro de frame courant et des options de l'animation.
init/1
Le comportement Animation doit être facile à intégrer dans d'autres modules en appelant use DancingDots.Animation.
Pour cela, implémente la macro __using__ dans le module Animation afin qu'elle définisse le module Animation comme comportement de l'autre module. Elle doit aussi fournir une implémentation par défaut du callback init/1. Cette implémentation par défaut de init/1 doit renvoyer les options fournies telles quelles.
defmodule MyCustomAnimation do
use DancingDots.Animation
end
MyCustomAnimation.init(some_option: true)
# => {:ok, [some_option: true]}
Flicker
Utilise le comportement Animation pour implémenter une animation scintillante.
Elle doit utiliser le callback init/1 par défaut, car elle ne prend aucune option.
Implémente le callback handle_frame/3, qui gère une seule frame. Si le numéro de frame est un multiple de quatre, la fonction doit renvoyer le point avec la moitié de son opacité d'origine. Pour les autres frames, elle doit renvoyer le point inchangé.
Les frames sont numérotées à partir de 1. Le point passé à handle_frame/3 est toujours le point dans son état d'origine, pas dans l'état de la frame précédente.
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
Utilise le comportement Animation pour implémenter une animation de zoom.
Cette animation prend une option : la vélocité. La vélocité peut être n'importe quel nombre. Si elle est négative, le point est dézoomé au lieu d'être zoomé.
Implémente le callback init/1. Il doit vérifier que les options passées forment une keyword list contenant la clé :velocity. La valeur de la vélocité doit être un nombre. Si ce n'en est pas un, renvoie l'erreur "The :velocity option is required, and its value must be a number. Got: #{inspect(velocity)}".
Implémente le callback handle_frame/3. Il doit renvoyer le point dont le rayon est augmenté de la vélocité multipliée par le numéro de frame courant moins un.
Les frames sont numérotées à partir de 1. Le point passé à handle_frame/3 est toujours le point dans son état d'origine, pas dans l'état de la frame précédente.
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}
Inscris-toi sur Exercism pour apprendre et maîtriser Elixir avec 58 concepts168 exercices, et un vrai mentorat humain, le tout gratuitement.