Tracks
/
Elixir
Elixir
/
Übungen
/
Tanzende Punkte
Tanzende Punkte

Tanzende Punkte

Lernübung

Einführung

Verwendung

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-Makro

Was 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

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.

Note

Beachte die britische Schreibweise von „behaviours“.

Behaviours definieren

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

Behaviours implementieren

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

Standardimplementierungen für Callbacks

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.

Anleitung

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.

1. Definiere das Verhalten der Animation

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.

2. Stelle eine Standardimplementierung für den Callback init/1 bereit

Das 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]}

3. Implementiere die Animation 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}

4. Implementiere die Animation 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}
Über GitHub bearbeiten Der Link öffnet sich in einem neuen Fenster oder Tab
Elixir Exercism

Bereit, mit Tanzende Punkte zu starten?

Melde dich bei Exercism an, um Elixir mit 58 Konzepte168 Übungen und echtem menschlichen Mentoring zu lernen und zu meistern, alles kostenlos.