Do you want to pick up from where you left of?
Take me there

Module

Wie wir aus Erfahrung wissen ist es unschön alle unsere Funktionen in der gleichen Datei und im gleichen scope zu haben. In dieser Lektion werden wir uns ansehen, wie man Funktionen aufteilt und definieren eine spezielle map, genauer ein struct, um unseren Code effektiver zu strukturieren.

Module

Module sind der beste Weg Funktionen in einem namespace zu gruppieren. Zusätzlich zum Gruppieren von Funktionen erlauben sie uns benannte und private Funktionen zu definieren, welche wir im vorherigen Kapitel behandelt haben. Lass uns ein einfaches Beispiel ansehen:

defmodule Example do
  def greeting(name) do
    "Hello #{name}."
  end
end

iex> Example.greeting "Sean"
"Hello Sean."

Es ist möglich Module in Elixir zu verschachteln, was dir erlaubt den namespace weiter zu konkretisieren:

defmodule Example.Greetings do
  def morning(name) do
    "Good morning #{name}."
  end

  def evening(name) do
    "Good night #{name}."
  end
end

iex> Example.Greetings.morning "Sean"
"Good morning Sean."

Modulattribute

Modulattribute werden meist als Konstanten in Elixir benutzt. Lass uns den Blick auf ein simples Beispiel werfen:

defmodule Example do
  @greeting "Hello"

  def greeting(name) do
    ~s(#{@greeting} #{name}.)
  end
end

Es ist wichtig festzuhalten, dass es reservierte Attribute in Elixir gibt. Die drei häufigsten sind:

Structs

Structs sind besondere maps, mit einer Menge keys und Defaultwerten. Ein struct muss innerhalb eines Modules definiert werden, woher es den Namen bezieht. Es ist für ein struct üblich, das einzige Ding in einem Modul zu sein. Um ein struct zu definieren benutzen wir defstruct zusammen mit einer keyword list an Feldern und Defaultwerten:

defmodule Example.User do
  defstruct name: "Sean", roles: []
end

Lass uns ein paar structs erstellen:

iex> %Example.User{}
%Example.User<name: "Sean", roles: [], ...>

iex> %Example.User{name: "Steve"}
%Example.User<name: "Steve", roles: [], ...>

iex> %Example.User{name: "Steve", roles: [:manager]}
%Example.User<name: "Steve", roles: [:manager]>

Wir können ein struct genauso wie eine map aktualisieren:

iex> steve = %Example.User{name: "Steve"}
%Example.User<name: "Steve", roles: [...], ...>
iex> sean = %{steve | name: "Sean"}
%Example.User<name: "Sean", roles: [...], ...>

Und das Wichtigste: Man kann sie gegen maps matchen:

iex> %{name: "Sean"} = sean
%Example.User<name: "Sean", roles: [...], ...>

Komposition

Da wir nun wissen wie man Module und structs erstellt, lass uns lernen wie man existierende Funktionalität hinzufügt mit der Hilfe von Komposition. Elixir erlaubt uns mit einer breiten Anzahl verschiedener Wege mit anderen Modulen zu interagieren.

alias

Erlaubt uns Modulnamen mit einem Alias anzusprechen; wird ziemlich oft in Elixir benutzt:

defmodule Sayings.Greetings do
  def basic(name), do: "Hi, #{name}"
end

defmodule Example do
  alias Sayings.Greetings

  def greeting(name), do: Greetings.basic(name)
end

# Ohne Alias

defmodule Example do
  def greeting(name), do: Sayings.Greetings.basic(name)
end

Falls es einen Konflikt zwischen zwei Aliasen gibt oder wir einfach nur einen anderen Alias vergeben wollen, können wir die :as-Option nutzen:

defmodule Example do
  alias Sayings.Greetings, as: Hi

  def print_message(name), do: Hi.basic(name)
end

Es ist sogar möglich mehrere Module auf einmal zu aliasen:

defmodule Example do
  alias Sayings.{Greetings, Farewells}
end

import

Falls wir Funktionen und Makros statt eines Moduls importieren wollen, können wir import/ benutzen:

iex> last([1, 2, 3])
** (CompileError) iex:9: undefined function last/1
iex> import List
nil
iex> last([1, 2, 3])
3

Filtern

Standardmäßig werden alle Funktionen und Makros importiert, aber wir können diese mit den :only- und :except-Optionen filtern.

Um bestimmte Funktionen und Makros zu importieren müssen wir :only und :except die Name/Arity-Paare angeben. Lass uns mit dem Import der last/1-Funktion beginnen:

iex> import List, only: [last: 1]
iex> first([1, 2, 3])
** (CompileError) iex:13: undefined function first/1
iex> last([1, 2, 3])
3

Falls wir alles außer last/1 importieren und die gleichen Funktionen wie gerade eben aufrufen wollen:

iex> import List, except: [last: 1]
nil
iex> first([1, 2, 3])
1
iex> last([1, 2, 3])
** (CompileError) iex:3: undefined function last/1

Neben den Name/Arity-Paaren gibt es noch zwei besondere atoms: :functions und :macros, welche nur Funktionen bzw. Makros importieren:

import List, only: :functions
import List, only: :macros

require

Obwohl weniger gebräuchlich, ist require/2 dennoch wichtig. Ein Modul zu benötigen sichert ab, dass es kompiliert und geladen ist. Das ist am nützlichsten, wenn wir auf das Makro eines Moduls zugreifen wollen:

defmodule Example do
  require SuperMacros

  SuperMacros.do_stuff()
end

Falls wir versuchen auf ein Makro zuzugreifen, dass noch nicht geladen ist, wirft Elixir einen Fehler.

use

Das use-Makro ruft ein spezielles Macro auf - __using__/1 - vom spezifizierten Modul. Hier ist ein Beispiel:

# lib/use_import_require/use_me.ex
defmodule UseImportRequire.UseMe do
  defmacro __using__(_) do
    quote do
      def use_test do
        IO.puts("use_test")
      end
    end
  end
end

und wir fügen diese Zeile UseImportRequire hinzu:

use UseImportRequire.UseMe

UseImportRequire.UseMe zu benutzen definiert eine use_test/0-Funktion durch den Aufruf des __using__/1-Makros.

Das ist alles, was use macht. Dennoch ist es wiederum für das __using__-Makro üblich alias, require oder import aufzurufen. Das erstellt dann daraus Aliase oder Imports im benutzenden Modul. Das erlaubt dem Modul eine Richtlinie zu definieren, wie seine Funktionen und Makros referenziert werden sollen. Das kann ziemlich flexibel sein, insofern, dass __using__/1 Referenzen auf andere Module aufsetzt, besonders Untermodule.

Das Phoenix Framework benutzt use und __using__/1, um den Gebrauch von sich wiederholenden Aliasen und Import-Aufrufen in benutzerdefinierten Modulen zu reduzieren.

Hier ist ein schönes und kurzes Beispiel aus dem Ecto.Migration-Modul:

defmacro __using__(_) do
  quote location: :keep do
    import Ecto.Migration
    @disable_ddl_transaction false
    @before_compile Ecto.Migration
  end
end

Das Ecto.Migration.__using__/1-Makro inkludiert einen import-Aufruf, so dass wenn du use Ecto.Migration aufrufst, auch import Ecto.Migration aufrufst.

Nochmal zur Wiederholung: Das use-Makro ruft einfach nur das __using__/1-Makro auf dem angegebenen Modul auf. Um wirklich zu verstehen, was es tut, musst du das __using__/1-Makro lesen.

Notiz: quote, require, use und alias sind Makros, die wir bei der Metaprogrammierung brauchen.

Caught a mistake or want to contribute to the lesson? Edit this lesson on GitHub!