Weil es 2023 ist, lerne ich natürlich Common Lisp
(log.schemescape.com)- Ausgehend von der Frage nach einer Sprache, die auch in Zukunft noch nutzbar ist, ging es bei dieser Wahl weniger um Praktikabilität als darum, einen alten, aber modernen Entwicklungs-Workflow selbst auszuprobieren
- Janet ist eine kleine Skriptsprache aus der Lisp/Clojure-Familie für einen ähnlichen Einsatzbereich wie Lua und weckte Interesse wegen ihrer Standardbibliothek und makrobasierten Metaprogrammierung
- Die bei Janet attraktiv wirkenden Eigenschaften Portabilität, Einbettbarkeit und Parsing Expression Grammars passten nur begrenzt zu den aktuellen Anforderungen, und bestehende Optionen wie CHICKEN Scheme, CLISP, SBCL und Packrat wirkten ebenfalls völlig ausreichend
- Die REPL-gesteuerte Entwicklung in Common Lisp zeichnet sich vor allem dadurch aus, dass man bei Laufzeitfehlern Stack, Variablen und Live-Daten untersuchen und verändern und danach die unterbrochene Ausführung fortsetzen kann
- Zwar kostet es etwas, eine neue Sprache und Standardbibliothek zu lernen, doch der Autor hält es für lohnend, diesen Workflow in einer Umgebung zu erleben, in der er von Anfang an Teil des Designs war
Warum das Interesse von Janet zu Common Lisp wechselte
- Aus dem Wunsch heraus, Code zu schreiben, den man auch in Zukunft noch verwenden kann, wurde über future-proof programming languages nachgedacht, doch beim Bauen und Veröffentlichen von Dingen überwog meist ein pragmatischer Ansatz: beliebt, portabel und bequem
- Gleichzeitig wurden zum Spaß und zum Experimentieren auch andere Programmiersprachen und Werkzeuge betrachtet
- Vor Kurzem entstand durch das kostenlose Buch Janet for Mortals Interesse an Janet
- Janet ist eine relativ kleine, von Lisp/Clojure inspirierte Skriptsprache für eine ähnliche Nische wie Lua
- Sie bietet eine echte Standardbibliothek, Lisp-artige Metaprogrammierung und Compile-Time-Ausführung per Makros
- Am meisten reizten an Janet neben dem allgemeinen Lisp-Charakter Portabilität, Einbettbarkeit und Parsing Expression Grammars
- Da derzeit keine Sprache eingebettet werden muss, war die Einbettbarkeit kein entscheidender Grund
- Bei der Portabilität wirkten auch CHICKEN Scheme, CLISP und Steel Bank Common Lisp überzeugend
- Für Parsing schien Packrat eine vernünftige Wahl zu sein
- Am Ende blieben also immer weniger Gründe, Janet statt eines bestehenden Lisp zu wählen
Der REPL-gesteuerte Workflow, der mich Common Lisp lernen ließ
- Ein Artikel über den beinahe einzigartigen REPL-gesteuerten Workflow von Common Lisp wurde zur Lernmotivation
- Die Beispielsituation ist, dass die Funktion
foodie noch nicht definierte Funktionbaraufruft und während der Ausführung vonfooein Fehler auftritt- In älteren Lisp- oder Smalltalk-Umgebungen führt die Unterbrechung von
foozu einer Breakloop - Die Breakloop ist eine vollständige REPL innerhalb der dynamischen Umgebung der unterbrochenen Funktion
- Man kann sich durch den unterbrochenen Call Stack bewegen und die in jedem Stack-Frame sichtbaren Variablen prüfen
- Die Live-Daten des laufenden Programms lassen sich untersuchen und verändern
- Wenn ein falscher Variablen- oder Feldwert die Ursache ist, kann man den Wert ändern und danach die unterbrochene Funktion fortsetzen
- Man kann die fehlende Funktion
bardirekt in der REPL definieren undfoowieder aufnehmen, um ein plausibles Ergebnis zu erhalten
- In älteren Lisp- oder Smalltalk-Umgebungen führt die Unterbrechung von
- Zwar wurden schon viele „edit and continue“-Funktionen gesehen, aber dass Common Lisp mit genau dieser Art von Workflow im Blick entwickelt wurde, wirkte neu
- Die bisherige Debugging-Erfahrung verlief von
printf debugginghin zur Nutzung echter Debuggerprintf debuggingbedeutet, vorübergehend Logging-Code einzufügen, neu zu kompilieren und dann die Ausführung zu prüfen- In Situationen, in denen sich Programme nicht verändern oder neu starten ließen, musste man den Umgang mit Debuggern lernen
- Ein echter Debugger ist das richtige Werkzeug, doch die Einrichtung der Debugging-Umgebung kann schmerzhaft oder manchmal unmöglich sein
- Common Lisp wirkte wie eine Umgebung, die Debugging noch einen Schritt weiter treibt
- Es gab bereits Erfahrungen damit, im Debugger Speicher zu verändern, um potenzielle Fixes zu testen
- Besonders attraktiv erschien aber die Möglichkeit, Code direkt im laufenden Prozess umzuschreiben und zu patchen
- Das Ziel beim Lernen von Common Lisp ist, Programme interaktiv aufzubauen und selbst herauszufinden, ob diese Arbeitsweise Spaß macht
- Es ist nicht sicher, ob das Lernen einer neuen Sprache samt Standardbibliothek die beste Wahl ist, um einen neuen Workflow zu erkunden, doch eine klar bessere Alternative ist ebenfalls nicht erkennbar
- Auch in anderen Sprachen könnte es ähnliche REPL-Editor-Integrationen geben
- Solche Funktionen wurden dort aber womöglich nachträglich ergänzt statt von Anfang an eingeplant und könnten daher fehleranfälliger sein
- Selbst wenn es nicht überzeugt, bleibt immerhin, es unter den bestmöglichen Bedingungen mit den Standardwerkzeugen ausprobiert zu haben
- Materialien, die beim Lernen von Common Lisp geholfen haben, sind in einem separaten Beitrag Common Lisp resources zusammengestellt
1 Kommentare
Meinungen auf Hacker News
Was mir an Common Lisp bisher gefallen hat, ist, dass das Condition System sauber ist und man mit Restarts auch weit entfernten Code leicht steuern kann. REPL-getriebene Entwicklung ist ebenfalls praktisch, wenn man nicht genau weiß, was passieren wird – etwa beim Parsen einer unbekannten Datenquelle –, weil man den Code anpassen und weitermachen kann, ohne den Kontext zu verlieren.
Es gibt viele Implementierungen, und die Interoperabilität ist gut; in einem Fall konnte ich für Geschwindigkeit SBCL und zur Reduzierung des Speicherverbrauchs CLISP austauschbar verwenden. Einer der Gründe, warum ich beim Lernen von Lisp eher zu CL als zu Scheme tendierte, war ebenfalls, dass es mehrere kompatible Implementierungen gibt.
Obwohl ich Emacs-Anfänger bin, war die Common-Lisp-Integration hervorragend, und sie läuft sogar auf dem sehr langsamen Netbook gut, das ich für die Entwicklung nutze. In Zeiten schneller Computer, VS Code und Language Server ist das kein ganz so großer Vorteil mehr, aber es hat definitiv ein Retrofuture-Gefühl.
Es gibt auch Dinge, die mir weniger gefallen. Der beliebteste Package Manager, QuickLisp, ist in Ordnung, erreicht aber nicht das Funktionsniveau, das man aus modernen Sprachökosystemen gewohnt ist. Die Sprache selbst ist in der Zeit eingefroren, sodass man für vieles – Threads, Synchronisation, Kommandozeilenargumente usw. – Interop-Bibliotheken braucht. Und ich fände es wirklich großartig, wenn SBCL vollständig statische Builds unterstützen würde, damit man Binaries auch für Linux-Distributionen ohne glibc ausliefern kann.
Wenn eine so wichtige Aufgabe wie das Abrufen von Quellcode keine Verschlüsselung oder Authentifizierung unterstützt, ist QL meiner Meinung nach für sicherheitsbewusste Menschen kaum eine Option.
Außerdem ist es ein Problem, dass SBCL auf SourceForge gehostet wird. SourceForge hat in der Vergangenheit Malware in Download-Archive eingeschleust, daher sehe ich auch das als Sicherheitsproblem. Ich sehe heute keinen triftigen Grund mehr, SourceForge weiter zu nutzen, und verstehe nicht wirklich, warum ein wichtiges Projekt immer noch dort liegt.
Ich mag Lisp und besonders Common Lisp wirklich, aber solche Dinge haben mich davon weggebracht, und es scheint auch nicht viel Interesse daran zu geben, sie zu beheben.
https://www.timmons.dev/posts/static-executables-with-sbcl.h...
https://www.timmons.dev/posts/static-executables-with-sbcl-v...
https://cons.io
Gerbil/Gambit Scheme kann als Alternative zu CL vollständig statische Binaries erzeugen.
Es kann als normale Quicklisp-Bibliothek, als sofort startendes Core Image oder als Binary verwendet werden. Skripte lassen sich ebenfalls fast sofort ausführen, insofern ähnelt es Babashka ein wenig. Ich feile noch an den Details, und v1.0 ist es noch nicht.
Im Blog hätte „Laufzeitfehler behandeln, indem man kaputten Code an Ort und Stelle repariert, ohne neu zu starten“ hier gezeigt werden sollen: https://www.youtube.com/watch?v=jBBS4FeY7XM
Wenn bei einer langen, schweren Berechnung im letzten Schritt ein Fehler auftritt, springt man statt eines kompletten Neustarts in den interaktiven Debugger, geht zur fehlerhaften Zeile, kompiliert die korrigierte Funktion, kehrt zum Debugger zurück und nimmt die Ausführung an einem Punkt im Stack Frame wieder auf. Danach kann man sehen, wie das Programm durchläuft.
M-in die REPL zu kopieren, fühlt sich für mich deutlich besser an als das, was Clojure mittap>bietet. Selbst mit Tools wie Portal wirkttap>für mich eher wie aufgehübschtes Pretty-Printing.Auch bei der Performance kann Common Lisp statische Typangaben nutzen, und SBCL kann daraus sehr effizienten Code erzeugen. Es hilft,
DISASSEMBLEauf selbst geschriebenen Code laufen zu lassen, um zu sehen, was tatsächlich ausgegeben wird, und ihn entsprechend zu optimieren. Besonders Pakete wie SB-SIMD und Loopus sind für numerische Anwendungen wirklich sehr hilfreich.:quacktrue ist, behandle es wie eine Ente“ gut zu Entity-Component-System-artigem Game Design.Aber die Entwicklungsweise von Common Lisp und überhaupt das Condition System habe ich im letzten Jahr wirklich vermisst. Obwohl ich gar kein besonders versierter CL-Hacker bin, ging es mir schnell in Fleisch und Blut über. Ich wünschte, CLOS und die primitiven Datentypen von CL würden besser zusammenpassen, als es auf den ersten Blick wirkt.
In Cider geht das definitiv einfach, und die wichtigsten Tools anderer Editoren dürften das ähnlich gut hinbekommen.
DISASSEMBLEmachen.Es gibt Hilfsprojekte wie https://github.com/Bronsa/tools.decompiler, und auch JitWatch von OpenJDK (https://github.com/AdoptOpenJDK/jitwatch). Für andere JVMs gibt es ähnliche Tools.
Nicht so intuitiv wie Lisp, aber es ist machbar.
Es ist schön, dass ihr etwas Esoterisches geschaffen habt, das attraktiv und gut funktioniert, aber jetzt wird es niemand mehr anfassen wollen außer Leuten, deren Jahresgehalt man mit der Anzahl der Nullen ausdrücken kann.
https://www.digitalocean.com/community/tutorials/java-repl-j...
https://github.com/waf/CSharpRepl
https://pub.dev/packages/interactive
Ganz zu schweigen von Ruby, Python, PHP und Lua; selbst C++ hat eine, wenn auch holprige, REPL: https://github.com/root-project/cling
Jedes Mal, wenn ich sage, dass die REPL bei Lisp kein Alleinstellungsmerkmal mehr ist, werde ich heruntergevotet.
https://github.com/kaveh808/kons-9
Allerdings verschmutzen solche Neudefinitionen und Überschreibungen am Ende den Namespace, und irgendwann geht etwas kaputt, sodass man den Server neu laden muss.
Es hat auch viele Nachteile, aber sie sind nicht schlimmer als die rein technischen Nachteile fast jeder anderen Sprache und Entwicklungsumgebung. Python und JS sind weiter verbreitet und haben mehr Bibliotheken, aber die Entwicklungserfahrung empfinde ich als deutlich schlechter. Ich habe über viele Jahre viel mit C#, TS, Py, Hs und noch obskureren Sprachen gearbeitet, aber jedes Mal, wenn ich in diese Sprachen abtauche oder mich ernsthaft über offenkundig schlechte Dinge ärgere, kehre ich zu CL (SBCL + Emacs + Slime) zurück. Dann bin ich beruhigt und überzeugt, dass es auf der Welt doch noch etwas Gutes gibt.
Derzeit sind wir dabei, Finanzierung für ein Produkt einzuwerben, das wir törichterweise in TypeScript gebootstrappt haben; die Release-Version werden wir in CL neu bauen. Das bedeutet, dass wir in den kommenden 3–5 Jahren bis zum Release mit CL und innerhalb von CL arbeiten werden, und ich freue mich auf die spannenden Dinge wie DSL-Implementierung, Code-Generierung, Makroarbeit und den Bau eines statischen Typ-Analysators.
Was vermisst du aus Lisp, wenn du mit C# arbeitest?
Meine zugespitzte These ist: Funktionale Programmierung hat sich nicht breit durchgesetzt, weil sie zwar durchaus gut zum Schreiben von Programmen ist, die meiste heutige Software aber nicht „ein Programm ist, das auf meinem PC/Server über die Kommandozeile gestartet wird und bis zum Ende durchläuft“, sondern „ein Programm, das startet, auf Benutzereingaben reagiert und endet, wenn der Benutzer es schließt“ oder „ein Programm, das startet, dann auf Netzwerk- oder andere automatische I/O reagiert und stoppt, wenn andere Software ihm sagt, dass es stoppen soll“.
Solche Aufgaben sind im rein funktionalen Stil deutlich schwieriger, oder zumindest war das bei den meinungsstarken funktionalen Implementierungen so, die ich ausprobiert habe. Denn man evaluiert nicht mehr einfach einen Ausdruck, sondern initialisiert Zustand, reagiert auf I/O, aktualisiert Zustand oder führt zusätzliche I/O aus und nutzt womöglich sogar Parallelisierung für Monitoring, Empfang, weitere I/O und Zustandsaktualisierung.
Natürlich ist es nicht unmöglich, so etwas in Lisp zu machen, aber nach einigen Semestern funktionaler Programmierung im Studium und professioneller Nutzung funktionaler Features in C++/Scala zur Lösung solcher Probleme empfand ich es als ziemlich schwierig, funktionale Programmierung in solchen Anwendungen gut zum Laufen zu bringen; diese mangelnde Passung hat mich zögern lassen, tiefer in funktionale Programmierung einzusteigen.
Unabhängig von Common Lisp musste ich schon oft Code schreiben, der Code erzeugt. In diesem Bereich dürfte Lisp glänzen, aber ich hatte noch keine Gelegenheit, es auszuprobieren. Beispiele aus jüngeren Projekten, die ich vor Common Lisp gemacht habe, sind die Generierung von Code in einem statischen Site-Generator zur Validierung eines bestimmten JSON Schema sowie die Erzeugung von JSX aus Markdown für Story-Inhalte in einem Programmierspiel
Ganz zu schweigen von den zahllosen C-Makros, die ich im Laufe der Zeit geschrieben habe
Man muss auch nicht alle guten Features anderer Stile wegwerfen. Teile der funktionalen Programmierung werden auch in gewöhnlichen Sprachen immer verbreiteter. Rust nutzt zum Beispiel in vielen Fällen funktionale Patterns
Und Lisp kann man, wenn man möchte, auch objektorientiert oder imperativ schreiben. Es ist nicht Haskell
Lisp sehe ich als Ausgangspunkt, Haskell als das Eigentliche. Ich empfehle, diesen Stil zu lernen, auch wenn man keinen echten Produktionscode damit schreibt
(let ((pair (cons 1 nil)))(setf (cdr pair) pair)(list (first pair) (second pair) (third pair)));; => (1 1 1)Wenn ich will, dass etwas „für immer kompiliert“, schreibe ich es in ANSI C
Wenn ich will, dass etwas „für immer überlebt“, schreibe ich es in Python 2.7 und mache es zum Rückgrat der Infrastruktur-Templates der ganzen Organisation. Zusatzpunkte, wenn es ein eigenes Ansible-Modul ist
Was ich mir wünsche, ist, dass Lisp Kleinkram und Boilerplate minimiert oder abstrahiert und es dadurch leichter macht, Gedanken als Code auszudrücken. Für mich wirken Lisp-Ausdrücke wie die natürlichste Art, Gedanken auszudrücken
Ich interessiere mich vor allem dafür, GUI-Apps zu schreiben, und möchte deshalb Clog lernen oder einen guten Wrapper über einem GUI-Toolkit finden oder entwickeln
(do-it (do-it first))(do-it first)funktioniert gut, aber was passiert, wenn der Aufruf(do-it (do-it first))fehlschlägt?Es wäre gut, wenn man an der kaputten Stelle die Kontrolle bekommt und die Definition von
do-itreparieren kann. Aber wenn man sie repariert, ändert sich auch das Ergebnis von(do-it first)Dann befindet sich die Maschine an einer Stelle, die mit dem aktuellen Code nicht mehr erreichbar ist
Ich verstehe wirklich nicht, wie das funktionieren soll, wenn eine Änderung, die das Fortsetzen ermöglichen soll, genau den Zustand verändert, in dem man überhaupt erst die Kontrolle zum Reparieren bekommen hat