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()%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()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)}")
endjson-ish: {"alter": 17, "name": "Mia"}
csv: 17,MiaFehlerbehandlung
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))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
defstructfü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
Aufgabe
Definiere ein Struct Produkt mit Name und Preis und ein Protokoll Preis mit brutto/1.