# Was ich an Elixir gesucht habe

URL: https://www.mikebild.dev/de/blog/was-ich-an-elixir-gesucht-habe/

Eine neue Programmiersprache probiert man aus, weil einem die Syntax gefällt – so lautet die übliche Begründung. Kürzere Schreibweisen, elegantere Lambdas, weniger Zeremonie. Diese Begründung halte ich für falsch herum. An Syntax gewöhnt man sich in zwei Wochen, sie ist der austauschbarste Teil einer Sprache. Interessant wird es dort, wo eine Sprache anders *ausführt*: wie sie Nebenläufigkeit organisiert, wem Zustand gehört und was passiert, wenn etwas kaputtgeht.

Genau deshalb liegt seit letztem Herbst ein kleines Elixir-Repository auf meinem GitHub-Konto. Nicht, weil mir das Ruby-artige Gewand der Sprache gefallen hätte – das war mir anfangs beinahe egal –, sondern weil unter diesem Gewand die Erlang-VM arbeitet. Und die beantwortet die drei Fragen von oben radikal anders, als ich es aus meinem Alltag mit JavaScript und C# kenne: Prozesse statt Threads, Nachrichten statt geteiltem Zustand, „let it crash" statt defensivem try/catch an jeder Ecke.

## Ein Blick über den Zaun, mit Zeitkolorit

Der Zeitpunkt für diesen Blick über den Zaun ist günstig. Im Februar hat Facebook WhatsApp für eine Summe gekauft, über die noch immer alle reden – und plötzlich interessiert sich auch die Management-Etage dafür, dass ein erstaunlich kleines Team dort mehrere hundert Millionen Nutzer bedient. Das Backend: Erlang. Die Zahlen, die seit Jahren aus dieser Ecke kursieren – Millionen gleichzeitiger Verbindungen auf einer einzelnen Maschine –, klangen für mich lange wie Folklore. Jetzt schaut man genauer hin, ich auch.

Elixir setzt auf genau dieser Maschine auf. José Valim, vielen aus dem Rails-Umfeld bekannt, baut damit eine Sprache, die auf der Erlang-VM läuft, Erlang-Module direkt aufrufen kann und die Werkzeuge der Plattform erbt. Die Sprache ist noch jung: aktuell Version 0.13, auf dem Weg zu 1.0, mit gelegentlichen Brüchen zwischen den Releases. Dave Thomas schreibt bei den Pragmatic Programmers an „Programming Elixir", bislang als Beta zu haben. Wer heute Elixir anfasst, fasst also bewusst etwas Unfertiges an – die Plattform darunter ist dagegen seit über zwanzig Jahren im Ernstfall erprobt.

## Ein Repository voller Fragen

Mein Einstieg war kein Projekt, sondern eine Sammlung kleiner Versuche: [github.com/MikeBild/tryelixir](https://github.com/MikeBild/tryelixir), begonnen im Oktober, gewachsen in Abendstunden. Kein README, keine Architektur, nur Ordner – jeder Ordner eine Frage, die ich der Sprache stellen wollte:

- `actors` – wie fühlen sich `spawn` und `receive` an, das Handwerkszeug der Prozesse?
- `bankAccount` – kann ein Prozess allein den Zustand eines Kontos verwalten, ganz ohne Lock?
- `cowboy` – wie spricht man HTTP auf der Erlang-VM, mit dem Webserver Cowboy?
- `kataTennis` und `kataTennisFSM` – die bekannte Tennis-Kata, einmal klassisch, einmal als endlicher Automat
- `leveldb` – wie bindet man eine Bibliothek wie LevelDB an?

Mehr behaupte ich über das Repository nicht, denn mehr ist es nicht: ein Experimentierkasten. Aber die Konten-Übung darin taugt gut, um zu zeigen, was mich am Ausführungsmodell gepackt hat.

## Prozesse statt Threads

Zunächst der Ausgangspunkt. Ein Bankkonto mit nebenläufigen Zugriffen sieht in meinem C#-Alltag ungefähr so aus – geteilter Zustand, geschützt durch ein Lock:

```csharp
public class BankAccount
{
    private readonly object _sync = new object();
    private decimal _balance;

    public void Withdraw(decimal amount)
    {
        lock (_sync)
        {
            if (_balance < amount)
                throw new InvalidOperationException("insufficient funds");
            _balance -= amount;
        }
    }
}
```

Das funktioniert – solange jeder Zugriff durch dasselbe Lock geht, niemand zwei Locks in unterschiedlicher Reihenfolge nimmt und keine Exception den Zustand halb verändert zurücklässt. In Code-Reviews sind das genau die Stellen, an denen ich am längsten hängen bleibe, und die Fehler, die dort durchrutschen, zeigen sich erst unter Last. In JavaScript stellt sich die Frage anders, aber nicht besser: Ein einzelner Thread mit Callbacks schützt zwar vor Race Conditions im Speicher, dafür blockiert eine einzige teure Berechnung alles andere, und ein nicht abgefangener Fehler reißt den ganzen Node-Prozess mit.

Die Erlang-VM organisiert das Problem grundlegend anders. Ihre Prozesse sind keine Betriebssystem-Threads, sondern winzige, von der VM verwaltete Einheiten: wenige Kilobyte beim Start, jeder mit eigenem Heap und eigener Speicherbereinigung, präemptiv geplant. Hunderttausende davon auf einer Maschine sind normal, nicht heldenhaft. Vor allem aber: Es gibt keinen geteilten Speicher zwischen ihnen. Was ein Prozess weiß, weiß nur er.

Dasselbe Konto in Elixir ist ein Prozess, der seinen Kontostand als Argument einer Endlosschleife mit sich trägt:

```elixir
defmodule BankAccount do
  def start(initial_balance) do
    spawn(fn -> loop(initial_balance) end)
  end

  defp loop(balance) do
    receive do
      {:deposit, amount} ->
        loop(balance + amount)
      {:withdraw, amount, caller} when amount <= balance ->
        send(caller, {:ok, balance - amount})
        loop(balance - amount)
      {:withdraw, _amount, caller} ->
        send(caller, {:error, :insufficient_funds})
        loop(balance)
      {:balance, caller} ->
        send(caller, {:balance, balance})
        loop(balance)
    end
  end
end
```

Kein Lock, kein `synchronized`, keine `volatile`-Grübelei. Der Kontostand kann gar nicht von zwei Seiten gleichzeitig verändert werden, weil ihn nur ein einziger Prozess besitzt und die Nachrichten aus seiner Mailbox der Reihe nach abarbeitet. Nebenläufigkeit entsteht nicht durch mehr Threads am selben Speicher, sondern durch mehr Prozesse mit je eigenem Zustand – ein Prozess pro Konto, nicht ein Lock pro Konto.

```mermaid
graph LR
  subgraph SHARED["Threads und geteilter Zustand"]
    T1["Thread 1"] --> L["Lock"]
    T2["Thread 2"] --> L
    L --> MEM["gemeinsamer Speicher<br/>balance"]
  end
  subgraph MSG["Prozesse und Nachrichten"]
    P1["Prozess A"] -- "{:withdraw, 30}" --> MB["Mailbox"]
    P2["Prozess B"] -- "{:deposit, 50}" --> MB
    MB --> ACC["BankAccount-Prozess<br/>eigener Zustand: balance"]
  end
```

## Nachrichten statt Aufrufe

Von außen betrachtet ist so ein Konto kein Objekt mit Methoden, sondern eine Adresse, an die man Nachrichten schickt:

```elixir
account = BankAccount.start(100)

send(account, {:deposit, 50})
send(account, {:withdraw, 200, self()})

receive do
  {:ok, new_balance} ->
    IO.puts "withdrawal ok, new balance: #{new_balance}"
  {:error, reason} ->
    IO.puts "withdrawal failed: #{reason}"
end
```

Der Unterschied zu einem Methodenaufruf ist größer, als er aussieht. Ein Aufruf ist synchron, kostenlos im Denken und teuer in den Folgen: Der Aufrufer hängt am Gelingen des Aufgerufenen, teilt sich mit ihm Stack und Schicksal. Eine Nachricht ist asynchron und entkoppelt – ob und wann eine Antwort kommt, ist eine eigene Entscheidung, die im Code sichtbar wird. Beim Umstieg fühlt sich das zunächst umständlich an. Mir ist dabei aber aufgefallen, wie viele Entwurfsentscheidungen ein Methodenaufruf stillschweigend trifft, über die ich vorher nie nachgedacht habe.

Wer nicht jedes Protokoll von Hand strickt, greift zur OTP-Bibliothek der Plattform: `gen_server` gießt genau dieses Muster – Zustand in einer Schleife, synchrone und asynchrone Nachrichten, geordnete Start- und Stopp-Semantik – in eine wiederverwendbare Form mit Callbacks. Die Konten-Übung von Hand zu schreiben war trotzdem der richtige Weg: Erst wenn man die `receive`-Schleife einmal selbst gebaut hat, versteht man, was OTP einem abnimmt.

## „Let it crash" – und wer räumt dann auf?

Der Teil, der mein Denken am stärksten herausfordert, ist der Umgang mit Fehlern. Mein gelernter Reflex aus Jahren mit C# und JavaScript: Fehler dürfen nicht passieren, also verteidige ich mich – null-Prüfungen, try/catch um jede Schicht, Fallback-Werte, Logging und weiterwursteln. Das Ergebnis kenne ich aus etlichen Codebasen, auch eigenen: Der Fehler wird nicht behoben, sondern verschmiert. Irgendwo fängt ein catch-Block zu viel, das Programm läuft mit halb kaputtem Zustand weiter, und der eigentliche Knall kommt Stunden später an einer Stelle, die mit der Ursache nichts mehr zu tun hat.

Die Erlang-Welt zieht daraus einen Schluss, der zunächst fahrlässig klingt: Lass den Prozess abstürzen. Kein defensives Abfangen im Prozess selbst – wenn eine Nachricht nicht zum erwarteten Muster passt oder eine Berechnung scheitert, stirbt der Prozess. Möglich ist das nur wegen der Isolation: Ein sterbender Prozess reißt keinen geteilten Speicher mit, es gibt keinen halb veränderten Zustand, den er hinterlassen könnte. Aufgeräumt wird an anderer Stelle – von einem Supervisor, einem Prozess, dessen einzige Aufgabe darin besteht, andere Prozesse zu beobachten und nach einem Absturz mit frischem, definiertem Zustand neu zu starten. Supervisors überwachen auch andere Supervisors, daraus entstehen Bäume:

```mermaid
graph TD
  APP["Application"] --> SUP["Supervisor<br/>Strategie: one_for_one"]
  SUP --> A1["BankAccount-Prozess<br/>Konto 1001"]
  SUP --> A2["BankAccount-Prozess<br/>Konto 1002"]
  SUP --> A3["BankAccount-Prozess<br/>Konto 1003"]
  A2 -. "stürzt ab" .-> SUP
  SUP -. "startet neu,<br/>frischer Zustand" .-> A2
```

Fehlerbehandlung wird damit von einer Codestil-Frage zu einer Architektur-Frage. Nicht mehr: Wo fange ich die Exception? Sondern: Welche Einheit darf sterben, und wer startet sie in welchem Zustand neu? Die Antwort steht nicht verstreut in tausend catch-Blöcken, sondern an einer Stelle im Supervision-Baum. Joe Armstrong hat diese Idee in seiner Dissertation ausformuliert – zuverlässige Systeme *trotz* fehlerhafter Software, nicht durch fehlerfreie Software. Beim Lesen musste ich mehrfach an Produktionsvorfälle denken, bei denen uns genau das gefehlt hat: nicht weniger Fehler, sondern ein definierter Weg zurück in einen bekannten Zustand.

## Die Syntax als Zugabe

Und dann gefällt mir die Syntax am Ende doch – aber als Zugabe, nicht als Grund. Drei Dinge stechen heraus.

Pattern Matching ist oben schon zu sehen: Die `receive`-Zweige sind keine if-Kaskade über einen Nachrichtentyp, sondern Muster mit Bedingungen, und die Struktur der Daten dokumentiert sich genau an der Stelle, an der sie verarbeitet wird. Dass `=` in Elixir kein Zuweisungs-, sondern ein Match-Operator ist, hat mich anfangs stolpern lassen und dann überzeugt.

Daten sind unveränderlich. Es gibt kein `balance -= amount`, es gibt nur einen neuen Wert, der in den nächsten Schleifendurchlauf wandert. Was in C# eine Disziplinfrage bleibt – bitte keine geteilten Objekte verändern –, ist hier schlicht nicht ausdrückbar. Ganze Fehlerklassen aus meinen Reviews existieren in dieser Sprache nicht.

Der Pipe-Operator `|>` schließlich macht Transformationsketten lesbar, ohne Zwischenvariablen und ohne von innen nach außen geschachtelte Aufrufe:

```elixir
"transactions.csv"
|> File.read!
|> String.split("\n")
|> Enum.map(&String.strip/1)
|> Enum.reject(&(&1 == ""))
|> Enum.map(&String.split(&1, ";"))
```

Jede Zeile eine Stufe, die Daten fließen von oben nach unten. Die ES5-Entsprechung mit `for`-Schleife und Hilfs-Arrays schreibe ich seit Jahren, ohne darüber nachzudenken – erst die Gegenüberstellung hat mir gezeigt, wie viel Rauschen darin steckt.

## Was in meinen Alltag zurückwandert

Elixir wird auf absehbare Zeit nicht meine Produktionssprache. Die Projekte, in denen ich stecke, sind .NET- und Node-Landschaften, und eine Pre-1.0-Sprache schleppe ich dort nicht hinein. Trotzdem war das Experiment kein Selbstzweck, denn einige Denkmuster nehme ich mit über den Zaun zurück:

- Kleine, absturzbereite Einheiten. Ein Node-Dienst, der in Sekunden startet und den ein Prozess-Wächter nach dem Absturz einfach neu hochzieht, ist dem großen Prozess überlegen, der jeden Fehler überleben muss. Schneller Neustart mit frischem Zustand schlägt heroisches Weiterlaufen.
- Nachrichten statt Aufrufe an den Grenzen. Wo zwei Teile eines Systems unabhängig voneinander leben und sterben sollen, gehört eine Queue dazwischen, kein direkter synchroner Aufruf – die Entkopplung, die Erlang-Prozessen von Haus aus mitgegeben ist, muss ich mir dort mit Infrastruktur erkaufen.
- Zustand hat einen Besitzer. Auch ohne Erlang-Prozesse kann ein Modul der einzige Ort sein, an dem ein Stück Zustand verändert wird, während alle anderen nur über definierte Nachrichten oder Kommandos mit ihm reden.
- Unveränderliche Daten, wo die Sprache es hergibt – und sei es nur als Konvention im Team.

Genauso wichtig ist, was *nicht* mitkommt. Einen Supervisor in C# oder JavaScript nachzubauen ist verlockend und führt in die Irre: Ohne isolierte Prozesse mit eigenem Heap ist „let it crash" keine Strategie, sondern Leichtsinn – der abstürzende Thread hinterlässt eben doch geteilten Zustand, und der Neustart beginnt nicht bei null. Das Modell trägt als Ganzes oder gar nicht. OTP in einer anderen Laufzeitumgebung zu imitieren hieße, die Fassade ohne das Fundament zu kopieren.

## Unterm Strich

Gesucht habe ich an Elixir keine schönere Sprache, sondern ein anderes Verhältnis zu den zwei Dingen, die mir in Projekten die meiste Arbeit machen: Zustand und Fehler. Gefunden habe ich ein Ausführungsmodell, das beide Probleme nicht besser löst, sondern anders stellt – Zustand hat genau einen Besitzer, Fehler haben einen definierten Bereich, in dem sie sterben dürfen. Dass darüber eine angenehme, junge Sprache liegt, macht das Experimentieren leichter; getragen wird alles von der Erlang-VM darunter.

Das kleine Repository bleibt liegen und wächst weiter, wenn die nächste Frage auftaucht. Und Elixir selbst beobachte ich auf seinem Weg zu 1.0 – nicht, weil ich morgen ein Konto-System darauf bauen werde, sondern weil mir der Blick über diesen Zaun mehr über meine eigenen Werkzeuge beigebracht hat als so manches Framework-Release diesseits davon.

## Weiterführende Quellen

- [github.com/MikeBild/tryelixir](https://github.com/MikeBild/tryelixir) – meine Elixir-Experimente
- [elixir-lang.org: Getting Started](https://elixir-lang.org/getting-started/introduction.html) – offizieller Einstieg in die Sprache
- [Erlang/OTP Design Principles](https://www.erlang.org/doc/design_principles/des_princ.html) – Supervision Trees und Behaviours in der offiziellen Erlang-Dokumentation
- [Joe Armstrong: Making reliable distributed systems in the presence of software errors](https://erlang.org/download/armstrong_thesis_2003.pdf) – die Dissertation hinter „let it crash"
