Entwicklung & Code
Im Test: Elixirs 1.20 neues intelligentes Typsystem
Eine Funktion erwartet eine Zahl, aber irgendwo im Projekt wird stattdessen eine Zeichenkette übergeben. Der Compiler sagt nichts, die Tests decken den Fall nicht ab, und der Fehler tritt erst auf, wenn ein Kunde in der Produktion genau diesen Codepfad auslöst. Wer mit dynamisch getypten Sprachen arbeitet, kennt diese Klasse von Bugs: Typen werden erst zur Laufzeit geprüft, der Compiler trifft über sie von sich aus keine Annahmen.
Weiterlesen nach der Anzeige
Python, Ruby und JavaScript funktionieren nach diesem Prinzip, die funktionale Programmiersprache Elixir ebenfalls. Der Komfort: Code bleibt knapp und lässt sich leicht umbauen. Der Preis: Fehler werden erst sichtbar, wenn das Programm auf den unerwarteten Wert trifft.
Ein Beispiel zeigt, was sich bei Elixir mit dem am 3. Juni 2026 erschienenen Release 1.20 ändert. Die folgende Funktion nimmt eine Map entgegen, Elixirs Schlüssel-Wert-Struktur (geschrieben %{schlüssel: wert}, vergleichbar einem Dictionary in Python oder einem Hash in Ruby), und wandelt das darin enthaltene Feld age in eine Zeichenkette um:
defmodule Report do
def user_age_to_string(user) do
Integer.to_string(user.age)
end
end
# An anderer Stelle im Projekt:
Report.user_age_to_string(%{age: "42"})
Integer.to_string/1 ist dabei Elixirs übliche Schreibweise für eine Funktion: Modul, Funktionsname und nach dem Schrägstrich die Stelligkeit (Arity), also die Zahl der Argumente. Bis einschließlich Version 1.19 schwieg der Compiler zu diesem Code; dass age eine Zeichenkette statt einer Zahl enthält, fiel erst zur Laufzeit auf. Ab 1.20 erscheint eine Warnung beim Übersetzen:
warning: incompatible types given to Report.user_age_to_string/1:
Report.user_age_to_string(%{age: "42"})
given types:
%{age: binary()}
but expected one of:
%{..., age: integer()}
Zwei Notationen darin sind erklärungsbedürftig (Typnamen tragen in Elixir stets leere Klammern). binary() ist Elixirs Typ für Zeichenketten, denn Strings sind in Elixir intern UTF-8-kodierte Binärdaten. Und die Ellipse in %{..., age: integer()} steht für beliebige weitere Schlüssel: Gemeint ist jede Map, die mindestens den Schlüssel age mit einem integer(), einer ganzen Zahl, enthält.
Niemand hat dem Compiler mitgeteilt, welchen Typ user.age haben soll. Er hat es selbst erschlossen: Integer.to_string/1 verlangt eine ganze Zahl, also muss das Feld age eine ganze Zahl enthalten, also muss jeder Aufrufer eine Map mit genau diesem Merkmal übergeben. Die Schlussfolgerung klingt trivial, aber sie steckt in keiner Typannotation, also keinem expliziten Typhinweis im Quelltext, wie ihn typisierte Sprachen verlangen (name: string in TypeScript, String name in Java). In Elixir 1.20 übernimmt der Compiler diese Arbeit von selbst.
Weiterlesen nach der Anzeige
Das dahinterstehende Typsystem, das seit Version 1.17 schrittweise in die Sprache einzog, analysiert inzwischen jedes Sprachkonstrukt. Es erkennt nicht nur fehlerhafte Argumente wie im Beispiel oben, sondern verfolgt auch, wie sich das Wissen über einen Wert durch mehrere Fallunterscheidungen hindurch verfeinert, und deckt dabei toten Code und widersprüchliche Zugriffe auf. Was abstrakt klingt, markiert einen Wendepunkt in der Geschichte von Elixir.
Sämtliche Vorteile des neuen Typsystems gibt es automatisch und ohne eigenes Zutun. An der Arbeitsweise muss sich nichts ändern, bestehender Code kompiliert unverändert weiter, niemand muss Typannotationen schreiben oder neue Syntax lernen; die zusätzliche Fehlersuche läuft bei jedem Übersetzen einfach mit.
- Erste Version: 2012
- Autor: José Valim
- Laufzeit: BEAM (Erlang/OTP)
- Paradigma: funktional, dynamisch, ab 1.17 gradual typisiert (typisierter und untypisierter Code koexistieren)
- Typische Einsatzfelder: Web-Backends (Phoenix, LiveView), Embedded (Nerves), maschinelles Lernen (Nx), verteilte Systeme
- Paketverwaltung: Hex, Build-Tool Mix
- Version zum Artikel: 1.20.0, Release 3. Juni 2026, benötigt OTP 27+, kompatibel bis OTP 29
Elixir kurz erklärt
Elixir ist eine funktionale, dynamisch getypte Sprache, die der Brasilianer José Valim 2012 veröffentlichte. Sie läuft auf der BEAM, der virtuellen Maschine von Erlang, und erbt von dort leichtgewichtige Prozesse im Millionenbereich (von der VM verwaltet, keine Betriebssystemprozesse) sowie eine ausfallsichere Laufzeit durch Supervisoren: überwachende Prozesse, die abgestürzte Teile des Systems automatisch neu starten.
Bekannt ist Elixir vor allem durch das Web-Framework Phoenix, das Embedded-Framework Nerves (Firmware-Images für Geräte vom Raspberry Pi bis zur Industriesteuerung) und Nx, das Elixir in die Welt des maschinellen Lernens bringt. Allen gemein ist ein Programmierstil, den eine kleine Funktion illustriert; sie übersetzt das Ergebnis einer Operation in einen lesbaren Satz:
defmodule Ergebnis do
def beschreibe({:ok, wert}), do: "Erfolg: #{wert}"
def beschreibe({:error, :timeout}), do: "Zeitüberschreitung"
def beschreibe({:error, grund}), do: "Fehler: #{grund}"
def beschreibe(:pending), do: "läuft noch"
end
Vier Klauseln, vier Fälle. Eine Funktion darf in Elixir aus mehreren Definitionen gleichen Namens bestehen, den Klauseln; welche läuft, entscheidet kein if oder switch, sondern die Form des Arguments. Diesen Musterabgleich nennt man Pattern Matching. Die erste Klausel greift für einen Tupel, eine geordnete Sammlung fester Länge in geschweiften Klammern, dessen erstes Element das Atom :ok ist; den zweiten Wert bindet sie an die Variable wert und fügt ihn per #{...} in den Antwortsatz ein, Elixirs String-Interpolation, vergleichbar mit f-Strings in Python. Atome wie :ok oder :pending sind benannte Konstanten, deren Wert ihr eigener Name ist, vergleichbar mit Symbols in Ruby. Die zweite Klausel fängt den speziellen Fehlerfall :timeout ab, die dritte jeden anderen Fehler, die vierte das schlichte Atom :pending. Das do: ist die einzeilige Kurzform des do ... end-Blocks aus dem ersten Beispiel.
Zwei Bausteine gehören noch zum Rüstzeug. Erstens: Tupel wie {:ok, wert} und {:error, grund} heißen Result-Tupel; fast jede Elixir-Bibliothek meldet Erfolg oder Misserfolg in dieser Form. Zweitens lassen sich Klauseln durch Guards verfeineren, Prüfbedingungen hinter dem Schlüsselwort when: def halbiere(x) when is_integer(x) greift nur für ganze Zahlen. Patterns, Klauseln und Guards ersetzen in Elixir einen großen Teil des klassischen Kontrollflusses. Und genau dieser Stil galt lange als Hindernis für ein klassisches Typsystem.
Vom dynamischen Liebling zum typisierten Werkzeug
Die Idee, Elixir ein Typsystem zu verpassen, ist alt. Bereits der aus der Erlang-Welt stammende Dialyzer, den Kostis Sagonas an der Universität Uppsala mitentwickelt hat, kann Elixir-Code analysieren. Seine Stärke, das sogenannte Success Typing, liegt darin, nur solche Fehler zu melden, die sich aus dem Code unzweifelhaft ergeben. Sein Potenzial entfaltet er allerdings erst, wenn Entwicklerinnen und Entwickler ihre Funktionen mit @spec-Annotationen versehen: von Hand geschriebene Typsignaturen, die zusätzlich zum eigentlichen Code gepflegt werden wollen und mit ihm auseinanderdriften können. Dazu kommen langsame Analysen, sperrige Integration und schwer entzifferbare Meldungen. In die breite Masse der Elixir-Entwickler hat es Dialyzer deshalb nie geschafft.
José Valim kündigte 2022 in seiner Keynote auf der ElixirConf EU an, dass die Sprache ein eigenes Typsystem bekommen werde, finanziert über ein Promotionsstipendium des französischen Forschungszentrums CNRS und des Unternehmens Remote. Die wissenschaftliche Grundlage lieferte der Informatiker Giuseppe Castagna (CNRS, Paris) mit seiner Arbeit zu set-theoretischen Typen, einem Formalismus, der Typen als Mengen begreift. Castagnas Doktorand Guillaume Duboc übernahm den Großteil der Implementierung, unterstützt durch Valim selbst; die Designprinzipien dokumentieren die drei in einem gemeinsamen Paper. Für eine Sprache, in der Pattern Matching ohnehin um die Form von Werten kreist, passte dieser Ansatz besser als klassische Typsysteme nach dem Vorbild von ML oder Haskell.
Die Entwicklung verlief in vier Etappen (Abbildung 1). Elixir 1.17 legte im Juni 2024 den Grundstein: erste Typen, abgeleitet aus den Patterns einer Funktion, erste Warnungen bei offensichtlichen Widersprüchen. Elixir 1.18 brachte im Dezember 2024 das Typchecking von Funktionsaufrufen, dazu eingebaute JSON-Unterstützung und Schnittstellen für die offizielle Language-Server-Initiative. Elixir 1.19 dehnte die Prüfungen im Oktober 2025 auf Protokolle und anonyme Funktionen aus und machte das Übersetzen großer Projekte bis zu viermal schneller. Version 1.20 schließt den Kreis: Der Compiler inferiert jetzt die Typen sämtlicher Sprachkonstrukte.
Zeitstrahl des Elixir-Typsystems (1.17 bis 1.20): Vier Minor-Versionen, ein roter Faden: Typinformation fließt in immer mehr Sprachkonstrukte ein. Mit 1.20 erreicht die Inferenz den Alltag (Abb. 1).
Den Rollout hat das Kernteam in einem öffentlichen Phasenplan organisiert, der die Inferenz in drei Phasen durch die Release Candidates trug (siehe Kasten). Alle drei stecken im stabilen Release, das Anfang Juni kurz nach dem ursprünglich angepeilten Mai-Termin erschien.
Lesen Sie auch