Der Wechsel zu Elixir
(leemeichin.com)- Ein Ruby-Backend-Entwickler wechselte zu einem Unternehmen, das Elixir als Hauptsprache verwendet, und konnte sich trotz fehlender Elixir-/Erlang-Erfahrung dank der von Ruby beeinflussten Syntax vergleichsweise schnell einarbeiten
- Elixir bietet wie Ruby syntaktischen Spaß und DSL-Freundlichkeit, reduziert aber mit einem unveränderlichen funktionalen Modell ohne Klassen, Instanzen und Vererbung den Aufwand für Zustandsverwaltung
- Während Ruby-DSLs darauf setzen, zur Laufzeit Objekte und Methoden dynamisch zu definieren, erzeugen Elixir-DSLs mit Compile-Time-Makros Laufzeitcode und machen Fehler früher sichtbar
- Die Pipeline-Syntax, der
with-Ausdruck und Result-Tupel in der Form{:ok, result}sind nützlich, um den regulären Ablauf von der Behandlung behebbarer Fehler zu trennen - Im Vergleich zu dem in Ruby verbreiteten Ansatz mit Ausnahmen,
nil/falseund internen Fehlerzuständen von Objekten wirkt Elixirs explizite Rückgabe vonok/errorbefriedigender, und auch nach drei Monaten Nutzung macht das Schreiben damit noch Spaß
Wie sich jemand mit Ruby-Erfahrung an Elixir gewöhnt hat
- Im neuen Job wird Elixir als Hauptsprache im Backend verwendet; vor dem Start gab es keine Erfahrung mit dem Schreiben von Elixir- oder Erlang-Code
- Erlang wirkte wegen etwas Erfahrung mit Prolog nicht völlig fremd, und Elixir ließ sich dank seiner stark von Ruby beeinflussten Syntax schnell lernen
- Es gibt zwar noch keine tiefe Expertise in Best Practices, Architektur oder eher low-level Erlang-Konzepten, doch das hat den Einstieg in Elixir nicht verhindert
- Elixir wird als Sprache wahrgenommen, die den Programmier-Spaß von Ruby beibehält und zugleich typische zustandsbezogene Fallstricke aus OOP-Sprachen reduziert
- keine Klassen, Instanzen oder Vererbung
- basiert auf Unveränderlichkeit und funktionalem Stil
- nicht an ein statisches Typsystem gebunden
- Code mit veränderlichem Zustand ist deutlich schwerer wartbar, und es ist nicht immer klar, ob ein Zustand lokal in einer Funktion, in einer Instanz oder in einer Klasse liegt
- In Ruby ist alles ein Objekt, daher kann jedes Objekt Zustand besitzen; beim Versuch, intuitive DSLs zu bauen, vermischen sich leicht Zustände auf Funktions-, Instanz- und Klassenebene
- Rubys Eigenclass ist ein Konzept ähnlich einer Klasse der Klasse, und wenn eine Instanz ihre Eigenclass verändert, kann das auch den Zustand anderer Instanzen beeinflussen
Der Unterschied zwischen DSLs und Compile-Time-Makros
- Auch Elixir unterstützt wie Ruby DSLs und hat eine vertraute Syntax, aber der Kernunterschied liegt darin, wann die DSL erzeugt wird
- Elixir-DSLs sind Compile-Time-Makros und erzeugen Laufzeitcode
- Ruby-DSLs beruhen darauf, die Laufzeitumgebung zu verändern und Objekte sowie Methoden dynamisch zu definieren
- Wenn Elixir-Makros falsch verwendet werden, entstehen Fehler in der Bibliothek; wenn der erzeugte Makro-Code fehlerhaft ist, führt das zu Compiler-Fehlern
- Da solche Fehler zur Compile-Time sichtbar werden, können sich Tests stärker auf die eigentliche Anwendungslogik konzentrieren
Der Ablauf mit Pipelines und with
- Der Pipeline-Operator
|>in Elixir ist nützlich, aber auch die Threading-Operatoren in Clojure haben Vorteile- Clojures
->setzt wie Elixirs|>das Ergebnis des vorherigen Ausdrucks als erstes Argument der nächsten Funktion ein - Clojures
->>setzt das vorherige Ergebnis als letztes Argument der nächsten Funktion ein - Dieser Unterschied wirkt klein, ist aber nützlich, wenn man interoperablen Code ohne anonyme Funktionen schreiben will
- Clojures
- Elixirs
with-Ausdruck erinnert anletin Haskell oder Lisp und verhält sich, als wäre einotherwiseeingebaut- Komplexe Funktionen lassen sich in einen normalen Pfad oben und eine Fehlerbehandlung unten aufteilen
- Wenn kein Pattern Matching auf
{:ok, result}möglich ist, kann das als behebbarer Fehler behandelt werden, was gut zu Result-Tupeln passt
Vorteile expliziter Fehlerrückgaben
- In Ruby ist es üblich, Ausnahmen für den Kontrollfluss zu verwenden, und statt expliziter Fehlertypen stützt man sich oft auf erfolgreiche Ergebnisse,
nil,falseoder interne Fehlerzustände von Objekten- Zum Beispiel gibt
model.update(params)bei einem Fehlschlagfalsezurück und speichert den Grund inmodel.errors - Das führt wieder zum Problem des veränderlichen Zustands
- Zum Beispiel gibt
- Ein Ansatz wie bei
Result-/Either-Monaden, bei demok/erroroderleft/rightexplizit als Rückgabewerte verwendet werden, wirkt befriedigender als Ruby-typischer Ausnahmefluss - Auch nach drei Monaten mit Elixir bleibt es eine Sprache, die das von Ruby vermittelte Gefühl fortsetzt: „Programmieren macht Spaß“
1 Kommentare
Hacker-News-Kommentare
In den letzten Wochen schaue ich fast jeden Tag Elixir-YouTube-Videos
Vor Kurzem scheint es eine Elixir-Konferenz gegeben zu haben, und nachdem ich ein paar davon gesehen hatte, empfiehlt mir YouTube ständig Elixir-Content
Ich mag die Ideen hinter Erlang, aber als ich früher Dinge wie den ejabberd-Chatserver betreut habe, fand ich es zu eigenartig
Jedes Mal, wenn ich Videos von Joe Armstrong sehe, denke ich, dass er ein unterschätztes Genie war, und isolierte Prozesse mit Message Passing, also das Actor Model, wirken wie ein guter Kandidat für die Zukunft verteilter Programmierung
Beeindruckend fand ich auch, dass in Crockfords Video zu seiner neuen Sprache Misty https://www.youtube.com/watch?v=R2idkNdKqpQ die Fragen ebenfalls in Richtung Elixir/Erlang gingen
Allerdings bin ich angesichts der „Magie“ von LiveView und LiveBook noch nicht zu 100 % überzeugt. Dinge, die zu gut aussehen, hatten meist irgendwo einen Haken, und ungewohnte Begriffe wie GenServer sowie plattformspezifisches Wissen fühlen sich an wie ein Einwegportal; ich weiß nicht, ob dieses Wissen anderswohin übertragbar ist
Da Go ebenfalls eine ziemlich gute Concurrency-Story hat, bergen Sprache und Plattform einer kleinen Community Risiken bei Recruiting, Bibliotheken und Material. Trotzdem ist wirklich beeindruckend, was die Elixir-Community erreicht hat, und ich bin gespannt, wie die Umsetzung des Typsystems aussehen wird
sendfunktioniertGenServer ist eine natürliche Verallgemeinerung eines Musters, das man unzählige Male selbst schreiben würde, und Wissen über Actor lässt sich auch anderswo anwenden
Auch für Go gibt es Bibliotheken, die eine Actor-Abstraktion implementieren, und die Mailbox eines Prozesses kann man sich wie eine Message Queue ähnlich Go-Channels vorstellen. Der Unterschied liegt eher darin, wie der Interpreter und das Abgeben von Prozessorzeit funktionieren
LiveView dagegen wirkt tatsächlich magisch. Da greifen viele bewegliche Teile ineinander, und es ist das Ergebnis von Arbeit, die in verschiedenen Communities über die letzten zehn Jahre entstanden ist; es gibt also viele Abstraktionen, aber die Ideen selbst sind ausgereift
Dinge wie Riak waren ihrer Zeit voraus und hatten im Grunde schon die Idee, dass man robuste verteilte Systeme so bauen kann, wie man einen GenServer baut
Sowohl Chris als auch José tun viel dafür, die Sichtbarkeit außerhalb der frühen Elixir-Kreise zu erhöhen, und auch Leute in der Community machen das auf ihrer jeweiligen Ebene
Recruiting ist nicht so kompliziert, wie man denkt. Es gibt ziemlich viele Menschen, die mit dieser Sprache arbeiten wollen, und gute Entwickler holen recht schnell auf
Trotzdem muss man den Aspekt als Nischensprache in den Griff bekommen. Für die Sprache selbst ist das ein Risiko, kurzfristig kann es aber auch belohnt werden, in einer Nischentechnologie versiert zu sein
Bei den meisten Dingen kann man sehen und verstehen, was sie tun, und den Code lesen. Bei JavaScript ist das oft nicht der Fall
Dass Recruiting und das Finden von Tutorials schwierig sind, stimmt. Zum Beispiel ist es viel schwieriger als bei JavaScript, jemanden zu finden, der genau das, was ich mit Elixir vorhabe, schon einmal gemacht hat
Bibliotheken waren kein großes Problem, es gibt ziemlich viele. Bei APIs gibt es manchmal Client-Bibliotheken für JavaScript oder Python, aber keine für Elixir; normalerweise ist es jedoch nicht besonders schwer, eine API nur mit einem HTTP-Client zu nutzen
Dass ich jetzt Node.js nutze, liegt nicht daran, dass ich es möchte, sondern daran, dass ich es muss. Nachdem ich es ausprobiert habe, denke ich, dass ich niemals ein neues Projekt mit Node.js beginnen würde
Die Werkzeuge von Node.js für Concurrency Control, Skalierbarkeit sowie Reliability und Observability liegen weit hinter dem BEAM-Ökosystem zurück
Wenn Leute Elixir für zu gut halten, um es zu glauben, liegt das meiner Meinung nach manchmal daran, dass sie zu sehr an Plattformen gewöhnt sind, die nicht auf Resilienz ausgelegt sind
Bei LiveBook ist es, abgesehen von den genauen Details der Diff-Berechnung oder Datenkompression, ziemlich leicht, ein korrektes mentales Modell zu entwickeln. Wenn es dringend ist, kann man sich auch die WebSocket-Nachrichten ansehen und recht gut nachvollziehen, was passiert
LiveView ist erstaunlich intuitiv
Der große Reiz von Elixir/Erlang besteht darin, dass man im Kontext eines Webservers Background Jobs wirklich einfach laufen lassen kann und sich keine Sorgen machen muss, dass blockierende Ein-/Ausgabe den ganzen Server anhält
Bei meinem früheren Arbeitgeber mussten wir in einem Webhook-Handler viele HTTP-Requests absetzen; wenn davon genug gleichzeitig passierten, waren alle Betriebssystemprozesse beschäftigt und die ganze Site fiel aus
In solchen Momenten dachte ich schmerzlich: Mit Elixir hätte man einfach einen Task gestartet und weitergemacht
Die Idee, Objekte zwischen asynchronen Tokio-Prozessen zu teilen, fühlte sich im Vergleich zu Elixirs First-Class-Funktionalität schwierig an. Nicht, dass es unmöglich oder grundsätzlich schwer wäre, aber korrekt hinzubekommen schien es schwieriger
Background Jobs möchte ich auf anderer Rechenkapazität als HTTP-Requests laufen lassen. Das Nutzungsmuster der Ressourcen ist anders, und ich möchte State oder eine Queue davor haben, um Retries, Fehlerbehandlung und Backpressure explizit zu machen
Natürlich wäre es schön, wenn es diese Komplexität nicht gäbe und dasselbe Framework alles übernehmen würde, aber ich glaube nicht, dass eine Programmiersprache oder Runtime allein so weit kommt. Monitoring und Betriebsprozesse müssen Teil der Lösung sein
Sasa Jurics Vortrag The Soul of Erlang and Elixir zeigt sehr gut, wie mächtig diese Sprache sein kann
https://www.youtube.com/watch?v=JvBT4XBdoUE&t=4
Ich empfehle ihn sehr allen, die zwar schon „let it fail“ oder „BEAM macht Concurrency richtig“ gehört haben, aber noch kein Gefühl dafür haben. Ein wirklich großartiger Vortrag
Ich programmiere seit 1991 jeden Tag mit Erlang und hatte vor, das auch weiterhin zu tun.
In der Zwischenzeit habe ich auch Haskell, Rust, Elixir usw. ausprobiert. Starke Typisierung ist sehr wertvoll, aber am Ende läuft es darauf hinaus, womit man am meisten Spaß hat.
Erlang ist die Programmiersprache, die mir die größte Freude bereitet, und ich plane, mindestens bis zu meiner Pensionierung in ein paar Jahren weiter an Erlang zu hacken.
Ich kenne Elixir seit etwas mehr als einem Jahr, und das Lernen sowie das Einbauen in verschiedene Komponenten hat ziemlich viel Spaß gemacht.
Der größte Reiz kommt daher, dass BEAM/OTP allein schon so vieles mitbringt.
Jemand aus dem Team, mit dem ich gearbeitet habe, sagte einmal: „BEAM/OTP ist wie k8s ohne die komplizierten Teile.“
Ich habe einige Jahre lang Elixir gelernt, aber durch die kürzlich von mir entworfene OpenShift-, also k8s-Migration, wurde dieser Vorteil ein Stück weit verwässert.
Ohne k8s wäre es ein deutlich attraktiveres Angebot gewesen. Stattdessen haben wir unseren bestehenden Entwicklungs-Stack beibehalten und das, was wir mit OTP gemacht hätten, mit k8s-Konzepten erreicht. Die Trade-offs waren natürlich andere.
„Elixir gibt einem eine unterhaltsame Sprache wie Ruby, nimmt einem aber die zustandsbezogenen Tretminen objektorientierter Sprachen weg. Keine Klassen, keine Instanzen, keine Vererbung; es ist immutable und funktional und bindet einen nicht an ein statisches Typsystem.“
Das will ich, aber ich hätte auch gern Typen. Bei großen Codebasen bin ich überzeugt, dass starke Typisierung der richtige Weg ist, weil sie viel Magie sichtbar macht und das Nachvollziehen erleichtert.
Dank Pattern Matching/Unification in Funktionen weiß man ziemlich gut, welche Form Daten haben, wenn sie hin und her gehen. Wenn man trotzdem unsicher ist, gibt es Dialyzer.
Dialyzer ist ein statisches Analysewerkzeug für Erlang/Elixir. Es ist in der Standard-Erlang-Distribution enthalten und steht für „DIscrepancy AnaLYZer for ERlang programs“.
Es findet Inkonsistenzen wie Typfehler, toten Code und unnötige Tests in einem einzelnen Erlang-Modul oder in Bündeln von Anwendungen. Es leitet Typen auf Basis korrekter Programme ab; Typannotationen sind nicht zwingend erforderlich, liefern aber bessere Warnungen, wenn sie vorhanden sind.
https://fly.io/phoenix-files/adding-dialyzer-without-the-pai...
https://elixir-lang.org/blog/2023/06/22/type-system-updates-...
GenServer sind eher wie Objekte und interagieren über Nachrichten, statt einfach nur Structs mit zugehörigen Funktionen zu sein.
Ich hätte gern eine Basisschablone für die GenServer im Projekt, mit gemeinsamer Registrierung, Speichereinstellungen, Logging usw., die ich dann für drei Varianten spezifischer Aufgaben spezialisieren kann.
Elixir füllt diese Lücke mit Makro-Codegenerierung, aber das ist nicht besonders schön. Meine GenServer sind voller trivialer Wiederholungen.
Die Aussage „In Ruby ist es üblich, Exceptions für Control Flow zu verwenden“ halte ich schlicht für falsch.
Das Beispiel nach diesem Absatz ist die
update-Methode von Rails, aber die in Standard-Rails-Beispielen und Generatoren verwendete Form ist dieupdate-Version ohne Exceptions.Bibliotheken oder Anwendungen einer gewissen Größe gehen, sobald sie komplex genug werden, zu aussagekräftigen Fehlerklassen über. Zustandsbasierte Fehlerbehandlung wie in ActiveRecord ist nämlich schmerzhaft.
Da Ruby inzwischen in gewissem Umfang Pattern Matching hat, sehe ich darin einen Ersatz für Exceptions als Control Flow. Statt eine Exception zu werfen, kann man die Klasse selbst zurückgeben und darauf matchen.
Viele Gems verwenden Exceptions für Control Flow, und auch in Apps und Bibliotheken, die ich geschrieben oder gewartet habe, war das üblich.
Rails hat viele
bang!-Methoden, die bei Fehlern Exceptions werfen. Aber generell verwendet Ruby, ähnlich wie Python, tatsächlich häufig Exceptions für Control Flow.Ich komme von F# und habe Elixir nur ein wenig ausprobiert, aber die Sprache wirkt leider etwas verwirrend
Zum Beispiel zeigen Funktionssignaturen nur den Namen und die Anzahl der Parameter, aber keine Typen. In großen Libraries ist es daher ziemlich schwer, im Kopf nachzuvollziehen, was wohin gehört
Dass man falsche Parameter übergeben hat, merkt man oft erst, nachdem man den Code ausgeführt und debuggt hat. In einer streng typisierten Sprache wie F# wird so etwas schon beim Coden direkt abgefangen, was viel Zeit und Aufwand spart
Deshalb frage ich mich, was abgesehen vom Erlang-Umfeld der Hauptreiz von Elixir ist
https://elixir-lang.org/blog/2023/09/20/strong-arrows-gradua...
Derzeit ist es tatsächlich eine dynamische Sprache, daher bekommt man von der Sprache selbst nicht viele Typinformationen. Eine statisch typisierte Sprache ist sie noch nicht
Was Elixir für mich aber wirklich gegenüber anderen Sprachen, die ich verwendet habe, nach vorn bringt, ist BEAM/OTP
Meiner Erfahrung nach sind viele Funktionen, die man in anderen Sprachen als Libraries dazuholen müsste, standardmäßig enthalten. Solche Libraries erhöhen die Komplexität stark und haben manchmal auch eine steile Lernkurve. Async-Funktionen sind ein gutes Beispiel
Wenn man Elixir eine Weile nutzt und diese eingebauten Werkzeuge und Abstraktionen kennenlernt, versteht man meiner Ansicht nach die Stärke des gesamten Ökosystems
Der Hauptreiz ist vermutlich, dass sie mit einer Ruby-artigen Syntax auf BEAM läuft. BEAM halte ich für einen ziemlich großen Vorteil
Mit den Typen von F# lässt es sich nicht gut vergleichen, aber typespec kann man verwenden: https://hexdocs.pm/elixir/1.15.7/typespecs.html
Zum Beispiel gibt es mit Axon, Nx und BumbleBee einen Flow, in dem Machine Learning nativ integriert ist und sogar GPU-Kompilierung möglich ist: https://www.youtube.com/watch?v=HK38-HIK6NA
Die Community treibt diesen Bereich tatsächlich stark voran
Auch die Funktionsweise von LiveView ist attraktiv. Allerdings gibt es in .NET Blazor
Elixir kann in vielen Kontexten genutzt werden, etwa mit Nerves im Embedded-Bereich oder für Scripting. Soweit ich weiß, kann man F# ebenfalls für Scripting nutzen
Insgesamt sind BEAM und die damit verbundenen Strukturen die Stärken, und der Paketmanager Hex ist wirklich gut. Natürlich hat .NET ebenfalls gute Tools
Mit typespec, Pattern Matching und Guards kann man sich behelfen, aber es ist ziemlich umständlich und zumindest im Community-getriebenen JetBrains-Plugin nicht besonders IDE-/IntelliSense-freundlich
Es wäre gut, das nicht am Anfang zu schreiben, sondern nach zwei Jahren Nutzung
Neues wirkt am Anfang immer wie eine makellose, glänzende Universallösung
Erste Eindrücke haben ebenfalls ihren Platz, und genau das liefere ich. Zielgruppe sind nicht Elixir-/Erlang-Experten, sondern Ruby-Experten
Elixir war damals wie heute ein Juwel. Nicht perfekt, aber erstaunlich robust
Ich nutze Ruby seit etwa 2007
Jedes Mal, wenn ich auf das nächste „coole“ Ding umsteigen wollte, bin ich am Ende zu Ruby zurückgekehrt, weil ich Ruby mag, damit am schnellsten arbeiten kann, und das war alles
In letzter Zeit sehe ich aber deutlich mehr über Elixir, und Look and Feel gefallen mir. Ich starte bald ein neues Side Project und sollte es wohl einmal ausprobieren