EMZETT.
Login

Kurz: Ein Struct ist eine Map mit festen Feldern und einem Modulnamen als Typ:

Teil des Kurses Elixir

Kapitel 5 von 8 im Kurs Elixir (Abschnitt „Fortgeschritten“). Mit Fortschritt, Quiz und Zertifikat auf der Lernseite.

Structs

Ein Struct ist eine Map mit festen Feldern und einem Modulnamen als Typ:

defmodule Person do
  @enforce_keys [:name]
  defstruct [:name, alter: 0, hobbys: []]
 
  def neu(name, alter), do: %Person{name: name, alter: alter}
 
  def vorstellen(%Person{name: n, alter: a}) when a >= 18, do: "#{n} (#{a}, volljährig)"
  def vorstellen(%Person{name: n, alter: a}), do: "#{n} (#{a})"
 
  def geburtstag(%Person{} = p), do: %{p | alter: p.alter + 1}
end
 
defmodule Demo1 do
  def run do
    mia = Person.neu("Mia", 17)
    IO.inspect(mia)
    IO.puts(Person.vorstellen(mia))
    aelter = Person.geburtstag(mia)
    IO.puts(Person.vorstellen(aelter))
    IO.inspect(%Person{name: "Tom"})
    IO.inspect(mia.name)
    IO.inspect(is_map(mia))
    IO.inspect(mia.__struct__)
    IO.inspect(Map.from_struct(mia))
    IO.inspect(match?(%Person{alter: 17}, mia))
 
    try do
      struct!(Person, alter: 5)
    rescue
      e in ArgumentError -> IO.puts("Pflichtfeld fehlt: #{Exception.message(e)}")
    end
  end
end
 
Demo1.run()

Ausgabe:

%Person{name: "Mia", alter: 17, hobbys: []}
Mia (17)
Mia (18, volljährig)
%Person{name: "Tom", alter: 0, hobbys: []}
"Mia"
true
Person
%{alter: 17, hobbys: [], name: "Mia"}
true
Pflichtfeld fehlt: the following keys must also be given when building struct Person: [:name]

Structs unterstützen Pattern Matching (%Person{name: n}) und das Update %{p | alter: 18} (nur existierende Felder).

Protokolle: Polymorphie nach Datentyp

Ein Protokoll definiert Funktionen, die für verschiedene Typen unterschiedlich umgesetzt werden:

defprotocol Beschreibbar do
  @doc "Gibt eine Beschreibung zurück"
  def beschreibe(wert)
end
 
defimpl Beschreibbar, for: Integer do
  def beschreibe(n), do: "Ganzzahl #{n}"
end
 
defimpl Beschreibbar, for: BitString do
  def beschreibe(s), do: "Text mit #{String.length(s)} Zeichen"
end
 
defimpl Beschreibbar, for: List do
  def beschreibe(l), do: "Liste mit #{length(l)} Elementen"
end
 
defmodule Hund, do: defstruct([:name])
 
defimpl Beschreibbar, for: Hund do
  def beschreibe(%Hund{name: n}), do: "Hund #{n}"
end
 
defmodule Demo2 do
  def run do
    for w <- [42, "Hallo", [1, 2], %Hund{name: "Rex"}] do
      IO.puts(Beschreibbar.beschreibe(w))
    end
 
  end
end
 
Demo2.run()
 
# Eingebaute Protokolle: String.Chars (to_string), Inspect, Enumerable
defmodule Punkt do
  defstruct [:x, :y]
end
defimpl String.Chars, for: Punkt do
  def to_string(%Punkt{x: x, y: y}), do: "(#{x}|#{y})"
end
defimpl Inspect, for: Punkt do
  def inspect(%Punkt{x: x, y: y}, _opts), do: "#Punkt<#{x},#{y}>"
end
defmodule Demo3 do
  def run do
    IO.puts("Punkt: #{%Punkt{x: 1, y: 2}}")
    IO.inspect(%Punkt{x: 3, y: 4})
  end
end
 
Demo3.run()

Ausgabe:

Ganzzahl 42
Text mit 5 Zeichen
Liste mit 2 Elementen
Hund Rex
Punkt: (1|2)
#Punkt<3,4>

Behaviours: Verträge für Module

Ein Behaviour legt Funktionen fest, die ein Modul anbieten muss (wie ein Interface):

defmodule Formatter do
  @callback format(term) :: String.t()
  @callback name() :: String.t()
end
 
defmodule JsonLike do
  @behaviour Formatter
  @impl true
  def name, do: "json-ish"
  @impl true
  def format(%{} = map) do
    inhalt = map |> Enum.sort() |> Enum.map_join(", ", fn {k, v} -> "\"#{k}\": #{inspect(v)}" end)
    "{" <> inhalt <> "}"
  end
end
 
defmodule CsvLike do
  @behaviour Formatter
  @impl true
  def name, do: "csv"
  @impl true
  def format(%{} = map), do: map |> Enum.sort() |> Enum.map_join(",", fn {_, v} -> to_string(v) end)
end
 
daten = %{name: "Mia", alter: 17}
for mod <- [JsonLike, CsvLike] do
  IO.puts("#{mod.name()}: #{mod.format(daten)}")
end

Ausgabe:

json-ish: {"alter": 17, "name": "Mia"}
csv: 17,Mia

Fehlerbehandlung

Elixir bevorzugt Rückgabewerte {:ok, wert} / {:error, grund}. Exceptions sind für wirklich Unerwartetes.

defmodule Konto do
  def abheben(stand, betrag) when betrag > stand, do: {:error, :nicht_genug_guthaben}
  def abheben(stand, betrag) when betrag <= 0, do: {:error, :ungueltiger_betrag}
  def abheben(stand, betrag), do: {:ok, stand - betrag}
end
 
for betrag <- [30, 500, -1] do
  case Konto.abheben(100, betrag) do
    {:ok, neu} -> IO.puts("ok, neuer Stand #{neu}")
    {:error, grund} -> IO.puts("Fehler: #{grund}")
  end
end
 
# Exceptions
try do
  1 / 0
rescue
  e in ArithmeticError -> IO.puts("Arithmetik: #{Exception.message(e)}")
end
 
try do
  raise ArgumentError, message: "ungültig"
rescue
  e in ArgumentError -> IO.puts("Gefangen: #{e.message}")
after
  IO.puts("after läuft immer")
end
 
defmodule MeinFehler do
  defexception message: "eigener Fehler", code: 500
end
 
try do
  raise MeinFehler, code: 404
rescue
  e in MeinFehler -> IO.puts("#{e.message} (#{e.code})")
end
 
result = try do
  throw(:abbruch)
catch
  :abbruch -> "geworfen und gefangen"
end
IO.puts(result)
 
IO.inspect(Integer.parse("x"))
IO.inspect(Map.fetch(%{a: 1}, :b))
IO.inspect(Map.fetch!(%{a: 1}, :a))
IO.inspect(File.read("/gibt/es/nicht"))
IO.inspect(Enum.map(["1", "x", "3"], fn s ->
  case Integer.parse(s) do
    {n, ""} -> {:ok, n}
    _ -> {:error, s}
  end
end))

Ausgabe:

ok, neuer Stand 70
Fehler: nicht_genug_guthaben
Fehler: ungueltiger_betrag
Arithmetik: bad argument in arithmetic expression
Gefangen: ungültig
after läuft immer
eigener Fehler (404)
geworfen und gefangen
:error
:error
1
{:error, :enoent}
[ok: 1, error: "x", ok: 3]

Konvention: Funktionen mit ! am Ende (File.read!, Map.fetch!) lösen bei Fehlern eine Exception aus, die ohne ! liefern {:ok, ...}/{:error, ...}.

Merke

  • defstruct für Datentypen mit festen Feldern; Pattern Matching %Person{name: n}
  • Protokolle (defprotocol/defimpl) für Polymorphie nach Typ, Behaviours (@callback) als Modulvertrag
  • Fehler als {:ok, wert} / {:error, grund}; try/rescue/after, raise, defexception
  • !-Funktionen werfen, normale Varianten liefern Tupel

Übungsaufgabe

Definiere ein Struct Produkt mit Name und Preis und ein Protokoll Preis mit brutto/1.

Quiz zur Selbstkontrolle

Weiter im Kurs

Zurück: Kontrollfluss, Enum und Stream

Weiter: Prozesse, GenServer und OTP

Alle Kapitel: Elixir im Überblick