Posts tagged ‘Lisp’

Terminplaner in Lisp

Ich habe das Terminplaner-Beispiel zuerst in Java programmiert und dann nach Groovy (Skriptsprache für die Java-Plattform) programmiert. Das Beispiel ist in Groovy als eine der jüngsten Programmiersprachen doch erheblich kürzer und einfacher als in Java. Da hat es mich dann doch gereizt, das Beispiel auch einmal in einer der ältesten Programmiersprachen zu programmieren: Lisp. Dabei habe ich Common Lisp verwendet mit CLOS (Common Lisp Object System) und Lisp-Unit von Chris Riesbeck.

Die quantitativen Ergebnisse aller drei Implementationen zuerst einmal im Vergleich:

Java

  • 5 Fachklassen, 1 Exception-Klasse, 1 Testklasse
  • 37 Methoden inkl. Konstruktoren + 10 Methoden in Testklasse
  • 246 LOC operativ + 144 LOC für Testklasse (inkl. Leerzeilen)
  • 4 For-Schleifen, 4 If-Abfragen

Groovy

  • 4 Fachklassen, 1 Exception-Klasse, 1 Testklasse
  • 30 Methoden inkl. Konstruktoren + 9 Methoden in Testklasse
  • 203 LOC operativ + 123 LOC für Testklasse (inkl. Leerzeilen)
  • Alle 4 For-Schleifen der Java-Lösung wurden durch Closures ersetzt, die 4 If-Abfragen blieben bestehen.

Common Lisp

  • 4 Fachklassen, 1 Testklasse – Exceptions werden mit Hilfe von Conditions modelliert (nur eine Zeile notwendig)
  • 17 Methoden, 1 Condition-Funktion, 10 Testmethoden
  • 76 LoC operativ, 100 LoC Test (inkl. Leerzeilen)
  • Alle 4 Schleifen der Java-Lösung wurden durch Closures ersetzt, die 4 If-Abfragen blieben bestehen, allerdings in den spezialisierten Varianten when und unless.

Wow! Die Common-Lisp-Variante hat erheblich weniger Code als die Java- oder die Groovy-Variante. Woran liegt das?
Naheliegend wäre es, die Ursache bei dem Lisp-Feature zu suchen, dass keine andere Programmiersprache anbietet: Macros (Vorsicht: Lisp-Makros sind ganz anders als die berüchtigten C-Makros). Dem ist aber nicht so. Tatsächlich habe ich in dem Lisp-Code kein eigenes Makro selbst definiert und nur an einer Stelle von der Mächtigkeit der
Lisp-Makros profitiert. Mit assert-error aus Lisp-Unit kann man sehr einfach prüfen, ob eine Exception (im Lisp-Jargon Condition) geworfen wird:


(define-test benutzer-nicht-eingeladen-exception
(setup)
(let ((termin (make-termin stefans-kalender ein-datum 180 "TDD-Dojo")))
(assert-error 'benutzer-nicht-eingeladen (lehne-termin-ab termin "Henning"))))

Zum Vergleich der entsprechende Java-Code:


public void testBenutzerNichtEingeladenException() {
Termin termin = _stefansKalender.newTermin(_jetzt, 180, "TDD-Dojo");
try {
termin.lehneTerminAb(HENNING);
fail("BenutzerNichtVorhandenException erwartet");
} catch (BenutzerNichtEingeladenException e) {
assertTrue("Exception erwartet", true);
}
}

Das erklärt aber nicht, warum der operative Code in Lisp so viel kürzer ist als in Java oder Groovy. Eine nähere Analyse fördert folgende Gründe zu Tage:

  • In Java und Groovy werden eigene Zeilen spendiert, für schließende geschweifte Klammern. Für das Lisp-Äquivalent (schließende runde Klammern) werden keine eigenen Zeilen spendiert. Dierk König schlägt in seinem kommenden Groovy-Buch diese Art der Formatierung für Groovy für bestimmte Situationen auch vor (bei den Buildern).

    • In Java und Groovy ist es üblich, Leerzeilen in Methoden einzufügen, teilweise auch zwischen Attributen. Beides macht man in Lisp eher nicht.
    • Das aufschreiben von Attributen einer Klasse ist in Common-Lisp schlanker als in Groovy oder Java. Insbesondere gibt es prägnante Abkürzungen, um Setter und Getter und Defaultwerte zu definieren.
    • Einige Implementierungen lassen sich nicht sinnvoll 1:1 nach Common-Lisp transferieren. Daher vergleicht man ein Stück weit dann doch Apfelsinen mit Orangen (so groß wie zwischen Äpfel und Birnen sind die Unterschiede dann doch nicht 🙂

    Der gewaltige Unterschied in den LoC findet sich bei der Menge der Syntaxelemente in deutlich reduzierter Form. So spart man sich in Common-Lisp zwar Einiges an Zeilen für die Attributdefinitionen in Klassen, es gibt aber die gleiche Anzahl von Attributdeklarationen. In diesem Sinne ist die Lisp-Lösung zwar kürzer als die Lösungen in Java und Groovy. Sie ist bzgl. der Komplexität aber äquivalent zur Groovy-Lösung und diese beiden Lösungen sind weniger komplex als die Java-Lösung.

    Wieviel wirkt der große Unterschied in den LoC? Quelltext wird viel häufiger gelesen als geschrieben. Daher ist die Einsparung der Tastenanschläge bei der Code-Erstellung nicht wirklich von Interesse. Wenn man allerdings beim Lesen von Quelltext weder vertikal noch horizontal scrollen muss, erleichtert dies das Lesen: Im Lisp-Terminplaner passt jede operative Klasse problemlos auf eine Bildschirmseite, so dass man jede Klasse auf einen Blick erfassen kann. Das kann man als (leichten) Vorteil von Lisp werten. Allerdings muss ich zugeben, dass man den Groovy-Code an der einen oder anderen Stelle noch etwas eleganter Formulieren kann und dann auch noch etwas an LoC einsparen kann.

    Quelltext des Terminplaners in Common-Lisp.

    September 22, 2006 at 7:35 pm 2 comments

  • IDEs und der K(r)ampf zwischen Statik und Dynamik

    Für Java existieren heute mit Eclipse, IntelliJ IDEA und Konsorten sehr mächtige Entwicklungsumgebungen. Cross-Referencing, Refactoring und Auto-Completion sind Features, die man nicht mehr missen möchte.
    Dass diese Features für die aktuellen Skriptsprachen a la Python, Ruby oder Groovy nicht oder nur eingeschränkt existieren, wird häufig als Argument gegen diese Skriptsprachen verwendet.

    Aber wird über diese Features wirklich reflektiert?

    Cross-Referencing
    Mit den Cross-Referencing-Möglichkeiten der IDE kann man auf Tastendruck oder Mausclick von einer Codestelle zum referenzierten Element gelangen (z.B. von einer Variablendeklaration zur Klasse, von deren Typ die Variable ist).

    Natürlich sind die Cross-Referencing-Möglichkeiten in den IDEs deutlich mächtiger als eine Textsuche über Dateien. Aber ist der Unterschied wirklich so groß? Und ist die Textsuche häufig nicht sogar schneller?

    Refactoring
    Refactoring ist eine der großen Errungenschaften in der Neuzeit der Softwareentwicklung und ihr Automatisierungsgrad ist wirklich erstaunlich hoch. Aber welche automatisierten Refactorings nutzen wir überhaupt bei der täglichen Arbeit und wie häufig? Viele Entwickler verzichten sogar absichtlich auf bestimmte einfache Automatisierungen (z.B. “Introduce Variable”), weil sie von Hand schneller sind – die Mächtigkeit der IDEs führt bei großen Projekten leider dazu, dass die Arbeit mit ihnen mitunter doch etwas zäh wird.
    Viele Refactorings kann man in der Tat mit vertretbarem Aufwand und Risiko auch von Hand durchführen. Einige andere (z.B. “Rename Class”) lassen sich auch auf anderem Wege automatisieren (Find&Replace über Dateien). Natürlich kann man sich dann oft nicht 100%ig sicher sein, dass alles korrekt geändert wurde. Man muss anschließend die Tests ausführen. Dass muss man in Java aber auch, weil es immer mehr untypisierte Referenzen in XML-Dateien (Struts, Spring, EJB etc.) gibt, die den IDEs auch mal durch die Lappen gehen.

    Auto-Completion
    Bei der automatischen Vervollständigung tippt man nur den Beginn eines Wortes ein und die IDE ergänzt die fehlenden Zeichen. Dadurch spart man Tippzeit, reduziert die Wahrscheinlichkeit von Tippfehlern und muss sich nur an den Anfang von Methodennamen erinnern.
    Interessanterweise gibt es auch in programmiersprachen-unabhängigen Editoren wie Emacs oder JEdit Auto-Completion. Dabei werden viel einfachere Verfahren eingesetzt als in den Java-IDEs, die aber erstaunlich gute Ergebnisse liefern. Ein ganz einfacher Ansatz besteht z.B. darin, bei der Auto-Completion nur die Wörter zu berücksichtigen, die in der aktuellen Datei schon mal verwendet wurden. Etwas umfangreicher ist der Ansatz, die Wörter aller Dateien eines Projektes zu verwenden.
    Ähnlich wie beim Cross-Referencing sind diese Auto-Completion-Features der Editoren qualitativ denen der IDEs unterlegen. Aber wie beim Refactoring sie sind schneller.

    Und das ist ein Faktor, den man nicht unterschätzen sollte. Ich ertappe mich beim Java-Programmieren jedenfalls immer wieder dabei, dass ich Namen komplett manuell eintippe, weil ich damit schneller bin als die zäh reagierende Entwicklungsumgebung. Und wenn ich in einem einfachen Editor eine Skriptsprache programmiere, genieße ich es, dass der Editor immer sofort reagiert. Die IDEs sind nur Bruchteile von Sekunden langsamer, aber das reicht schon aus, um den Flow beim Programmieren zu beeinträchtigen.

    August 24, 2006 at 4:16 pm Leave a comment

    Closures in Common Lisp

    Martin Fowler published an online article about closures. He used the Ruby programming language for the examples. There are translations of the examples to Python and a language called Boo.

    As a programming task for myself I did the examples in Common Lisp, which supported Closured already decades ago. A real lisper might be able to write the code in a more elegant way but I think the principle should become clear.

    I didn’t use the Common Lisp Object System (CLOS) since for this simple example an ad-hoc modeling with hashes is simpler.


    ;; Ruby
    ; def managers(emps)
    ; return emps.select {|e| e.isManager}
    ; end

    ;; Common Lisp
    (defun is-manager (emp) (getf emp :is-manager))

    (defun managers (emps)
    (remove-if-not (lambda (emp)
    (when (is-manager emp) emp))
    emps))


    ;; Ruby
    ; def highPaid(emps)
    ; threshold = 150
    ; return emps.select {|e| e.salary > threshold}
    ; end

    ;; Common Lisp
    (defun high-paid (emps)
    (let ((threshold 150))
    (remove-if-not (lambda (emp)
    (when (> (getf emp :salary) threshold) emp))
    emps)))


    ;; Ruby
    ; def paidMore(amount)
    ; return Proc.new {|e| e.salary > amount}
    ; end

    ;; Common Lisp
    (defun paid-more (amount)
    (lambda (emp) (when (> (getf emp :salary) amount) emp)))

    ;; Ruby
    ; highPaid = paidMore(150)
    ; john = Employee.new
    ; john.salary = 200
    ; print highPaid.call(john)

    ;; Common Lisp
    (let ((high-paid (paid-more 150))
    (john '(:name John :is-manager nil :salary 200)))
    (princ (funcall high-paid john)))


    ;; Tests
    (defparameter manager-list
    '((:name Stefan :is-manager nil :salary 150)
    (:name Henning :is-manager t :salary 151)
    (:name Martin :is-manager nil :salary 120)
    (:name Christian :is-manager t :salary 200)))

    (princ #\newline)
    (princ "Test function managers")
    (princ #\newline)
    (princ (managers manager-list))
    (princ #\newline)

    (princ #\newline)
    (princ "Test function high-paid")
    (princ #\newline)
    (princ (high-paid manager-list))
    (princ #\newline)

    August 19, 2006 at 6:01 pm Leave a comment

    Revenge of the Nerds

    Durch Dierk König bin ich auf einen Artikel von Paul Graham aus dem Jahr 2002 gestoßen: Revenge of the Nerds. Dort argumentiert Paul Graham, dass die Programmiersprachen sich mit der Zeit immer weiter an LISP angenähert haben, allerdings immer noch nicht die Ausdruckskraft von LISP erreicht haben (das ist schon erstaunlich, wenn man bedenkt, dass LISP bereits 1958 erfunden wurde). Außerdem behauptet er, dass die Programmiersprache eine große Rolle bei der Produktivität spielt. Die zweite These erscheint mir sofort plausibel und passt auch mit den gängigen Untersuchungen zur Produktivität in Softwareprojekten zusammen.
    Die erste These war mir neu, scheint mir aber ebenso plausibel. Sie hat mich dazu animiert, nochmal an meine Studienzeit zurückzudenken, in der LISP-Programmierung zur Grundausbildung gehörte. Und tatsächlich: Die Programmiersprachen sind immer LISP-ähnlicher geworden und haben LISP immer noch nicht erreicht. Und ich trauere irgendwie doch immer mal wieder der Art und Weise nach, wie man LISP programmieren konnte. Es ist doch schon erstaunlich, dass man mit LISP bereits die Möglichkeit hatte, sich für sein Problem eine adäquate Sprache zu definieren und dann das Problem in dieser Sprache zu lösen. Modern ausgedrückt ist in LISP von Anfang an die Möglichkeit enthalten gewesen, DSLs (Domain Specific Languages) zu definieren. Smalltalk, Ruby, Python und Groovy sind trotz ihrer Dynamik nicht so ausdrucksstark geworden. Aber viel fehlt nicht mehr. Warten wir noch ein oder zwei Programmiersprachengenerationen ab und wir programmieren alle in LISP. Das heißt dann vielleicht etwas anders und hat vielleicht ein leicht andere Syntax, aber konzeptuell wird es LISP sein – wenn Paul Graham Recht hat. Irgendwie freue ich mich darauf und hoffe, dass ich selbst dann noch zum programmierenden Volk gehöre.

    March 26, 2006 at 5:42 pm Leave a comment

    Revenge of the Nerds

    Durch Dierk König bin ich auf einen Artikel von Paul Graham aus dem Jahr 2002 gestoßen: Revenge of the Nerds. Dort argumentiert Paul Graham, dass die Programmiersprachen sich mit der Zeit immer weiter an LISP angenähert haben, allerdings immer noch nicht die Ausdruckskraft von LISP erreicht haben (das ist schon erstaunlich, wenn man bedenkt, dass LISP bereits 1958 erfunden wurde). Außerdem behauptet er, dass die Programmiersprache eine große Rolle bei der Produktivität spielt. Die zweite These erscheint mir sofort plausibel und passt auch mit den gängigen Untersuchungen zur Produktivität in Softwareprojekten zusammen.
    Die erste These war mir neu, scheint mir aber ebenso plausibel. Sie hat mich dazu animiert, nochmal an meine Studienzeit zurückzudenken, in der LISP-Programmierung zur Grundausbildung gehörte. Und tatsächlich: Die Programmiersprachen sind immer LISP-ähnlicher geworden und haben LISP immer noch nicht erreicht. Und ich trauere irgendwie doch immer mal wieder der Art und Weise nach, wie man LISP programmieren konnte. Es ist doch schon erstaunlich, dass man mit LISP bereits die Möglichkeit hatte, sich für sein Problem eine adäquate Sprache zu definieren und dann das Problem in dieser Sprache zu lösen. Modern ausgedrückt ist in LISP von Anfang an die Möglichkeit enthalten gewesen, DSLs (Domain Specific Languages) zu definieren. Smalltalk, Ruby, Python und Groovy sind trotz ihrer Dynamik nicht so ausdrucksstark geworden. Aber viel fehlt nicht mehr. Warten wir noch ein oder zwei Programmiersprachengenerationen ab und wir programmieren alle in LISP. Das heißt dann vielleicht etwas anders und hat vielleicht ein leicht andere Syntax, aber konzeptuell wird es LISP sein – wenn Paul Graham Recht hat. Irgendwie freue ich mich darauf und hoffe, dass ich selbst dann noch zum programmierenden Volk gehöre.

    March 26, 2006 at 5:42 pm Leave a comment

    Newer Posts