Das use-Makro ermöglicht es uns, unser Modul schnell um Funktionalität zu erweitern, die ein anderes Modul bereitstellt. Wenn wir ein Modul mit use einbinden, kann dieses Modul Code in unser Modul einfügen - es kann zum Beispiel Funktionen definieren, andere Module mit import oder alias einbinden oder Modulattribute setzen.
Wenn du jemals einen Blick in die Testdateien einiger Elixir-Übungen hier auf Exercism geworfen hast, ist dir wahrscheinlich aufgefallen, dass sie alle mit use ExUnit.Case beginnen. Diese einzige Codezeile macht die Makros test und assert im Testmodul verfügbar.
defmodule LasagnaTest do
use ExUnit.Case
test "expected minutes in oven" do
assert Lasagna.expected_minutes_in_oven() === 40
end
end
__using__/1-MakroWas genau passiert, wenn du ein Modul mit use einbindest, bestimmt das __using__/1-Makro dieses Moduls. Es nimmt ein Argument entgegen, eine Keyword-Liste mit Optionen, und gibt einen quotierten Ausdruck zurück. Der Code in diesem quotierten Ausdruck wird in unser Modul eingefügt, wenn wir use aufrufen.
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
Die Optionen können beim Aufruf von use als zweites Argument angegeben werden, z. B. use ExUnit.Case, async: true. Werden sie nicht explizit angegeben, ist ihr Standardwert eine leere Liste.
Behaviours ermöglichen es uns, Schnittstellen (Mengen von Funktionen und Makros) in einem Behaviour-Modul zu definieren, die später von verschiedenen Callback-Modulen implementiert werden können. Dank der gemeinsamen Schnittstelle können diese Callback-Module austauschbar verwendet werden.
Beachte die britische Schreibweise von „behaviours“.
Um ein Behaviour zu definieren, müssen wir ein neues Modul erstellen und eine Liste von Funktionen angeben, die Teil der gewünschten Schnittstelle sind. Jede Funktion muss mit dem Modulattribut @callback definiert werden. Die Syntax ist identisch mit einer Funktionstypspezifikation (@spec). Wir müssen einen Funktionsnamen, eine Liste von Argumenttypen und alle möglichen Rückgabetypen angeben.
defmodule Countable do
@callback count(collection :: any) :: pos_integer
end
Um ein vorhandenes Behaviour zu unserem Modul hinzuzufügen (ein Callback-Modul zu erstellen), verwenden wir das Modulattribut @behaviour. Sein Wert sollte der Name des Behaviour-Moduls sein, das wir hinzufügen.
Dann müssen wir alle Funktionen (Callbacks) definieren, die von diesem Behaviour-Modul benötigt werden. Wenn wir das Behaviour von jemand anderem implementieren, wie die eingebauten Behaviours Access oder GenServer von Elixir, finden wir die Liste aller Callbacks des Behaviours in der Dokumentation auf hexdocs.pm.
Ein Callback-Modul ist nicht darauf beschränkt, nur die Funktionen zu implementieren, die Teil seines Behaviours sind. Es ist auch möglich, dass ein einzelnes Modul mehrere Behaviours implementiert.
Um zu kennzeichnen, welche Funktion von welchem Behaviour stammt, sollten wir vor jeder Funktion das Modulattribut @impl verwenden. Sein Wert sollte der Name des Behaviour-Moduls sein, das diesen Callback definiert.
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
Beim Definieren eines Behaviours ist es möglich, eine Standardimplementierung eines Callbacks bereitzustellen. Diese Implementierung sollte im quotierten Ausdruck des __using__/1-Makros definiert werden. Damit Nutzer des Behaviour-Moduls die Standardimplementierung überschreiben können, rufe das Makro defoverridable/1 nach der Funktionsimplementierung auf. Es akzeptiert eine Keyword-Liste mit Funktionsnamen als Schlüssel und Funktionsaritäten als Werte.
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
Beachte, dass das Definieren von Funktionen innerhalb von __using__/1 für jeden anderen Zweck als das Definieren von Standard-Callback-Implementierungen nicht empfohlen wird, aber du kannst Funktionen jederzeit in einem anderen Modul definieren und sie im __using__/1-Makro importieren.
Dein Freund, ein angehender Künstler, hat sich mit einer Projektidee bei dir gemeldet. Kombinieren wir seine visuelle Kreativität mit deiner technischen Expertise. Zeit, dich in generativer Kunst zu versuchen!
Einschränkungen fördern die Kreativität und verkürzen Projektfristen, deshalb habt ihr beide vereinbart, euer Meisterwerk auf eine einzige Form zu beschränken: den Kreis. Aber es wird viele Kreise geben. Und sie können sich bewegen! Ihr nennt es ... Dancing Dots.
Dein Freund wird auf jeden Fall neue, ausgefallene Bewegungen für die Punkte entwickeln wollen, also beginnst du mit dem Programmieren, indem du eine Architektur erstellst, mit der du später leicht neue Animationen definieren kannst.
Jedes Animationsmodul muss zwei Callbacks implementieren: init/1 und handle_frame/3. Definiere sie im Modul Animation.
Definiere den Callback init/1. Er sollte ein Argument vom Typ opts entgegennehmen und entweder ein {:ok, opts}-Tupel oder ein {:error, error}-Tupel zurückgeben. Implementierungen dieses Callbacks prüfen, ob die übergebenen Optionen für diese spezielle Art von Animation gültig sind.
Definiere den Callback handle_frame/3. Er sollte drei Argumente entgegennehmen: den Punkt, eine Frame-Nummer und die Optionen. Er sollte immer einen Punkt zurückgeben. Implementierungen dieses Callbacks ändern die Attribute des Punktes basierend auf der aktuellen Frame-Nummer und den Optionen der Animation.
init/1 bereitDas Verhalten Animation sollte sich leicht in andere Module einbinden lassen, indem du use DancingDots.Animation aufrufst.
Implementiere dazu das Makro __using__ im Modul Animation, sodass es das Modul Animation als Verhalten des anderen Moduls festlegt. Es sollte außerdem eine Standardimplementierung des Callbacks init/1 bereitstellen. Die Standardimplementierung von init/1 sollte die übergebenen Optionen unverändert zurückgeben.
defmodule MyCustomAnimation do
use DancingDots.Animation
end
MyCustomAnimation.init(some_option: true)
# => {:ok, [some_option: true]}
Flicker
Nutze das Verhalten Animation, um eine flackernde Animation zu implementieren.
Sie sollte den Standard-Callback init/1 verwenden, weil sie keine Optionen entgegennimmt.
Implementiere den Callback handle_frame/3, der einen einzelnen Frame verarbeitet. Wenn die Frame-Nummer ein Vielfaches von vier ist, sollte die Funktion den Punkt mit der Hälfte seiner ursprünglichen Deckkraft zurückgeben. In allen anderen Frames sollte sie den Punkt unverändert zurückgeben.
Die Frames werden ab 1 gezählt. Der an handle_frame/3 übergebene Punkt ist immer der Punkt in seinem ursprünglichen Zustand, nicht in dem Zustand aus dem vorherigen Frame.
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
Nutze das Verhalten Animation, um eine zoomeinde Animation zu implementieren.
Diese Animation nimmt eine Option entgegen: die Geschwindigkeit. Die Geschwindigkeit kann eine beliebige Zahl sein. Ist sie negativ, wird der Punkt herausgezoomt statt hineingezoomt.
Implementiere den Callback init/1. Er sollte prüfen, dass die übergebenen Optionen eine Keyword-Liste mit dem Schlüssel :velocity sind. Der Wert der Geschwindigkeit muss eine Zahl sein. Wenn er keine Zahl ist, gib den Fehler "The :velocity option is required, and its value must be a number. Got: #{inspect(velocity)}" zurück.
Implementiere den Callback handle_frame/3. Er sollte den Punkt mit einem Radius zurückgeben, der um die aktuelle Frame-Nummer minus eins, multipliziert mit der Geschwindigkeit, vergrößert wurde.
Die Frames werden ab 1 gezählt. Der an handle_frame/3 übergebene Punkt ist immer der Punkt in seinem ursprünglichen Zustand, nicht in dem Zustand aus dem vorherigen Frame.
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}
Melde dich bei Exercism an, um Elixir mit 58 Konzepte168 Übungen und echtem menschlichen Mentoring zu lernen und zu meistern, alles kostenlos.