Seiten

Samstag, 5. Juli 2008

Groovy und JavaScript und die Kunst, geschweifte Klammern zu setzen

Vor ein paar Jahren bin ich in ein Projekt gekommen, in dem darüber abgestimmt werden sollte, wie die Klammern in Java zu setzen seien. Es gab zwei Alternativen: den K&R-Stil und den Allman-Stil. Das Ergebnis damals war deutlich knapp. Nach einigen Wochen Gemurre programmierte aber jeder so, wie abgestimmt wurde. Es blieb der Eindruck zurück, dass sehr viel heiße Luft erzeugt und viel produktive Arbeitszeit sprichwörtlich verbrannt worden ist.

Nach hitzigen Diskussion vor der Abstimmung (vielen war "absolut klar", was denn die einzig wahre Art geschweifte Klammern zu setzen sein müsse) wurde mir damals klar, dass es eine Frage von Geschmack und Ästethik sein müsse und nicht objektiv beantwortbar sein könne, welcher Stil besser wäre als der andere. Es gab damals einfach kein rationales Argument für den einen oder den anderen Stil. Die Argumente beruhten auf Spekulation oder subjektivem Empfinden der Beteiligten.

Die Diskussion und die Entscheidung wären vermutlich anders verlaufen, wenn wir damals in Groovy oder JavaScript programmiert hätten. Dort gibt es jeweils ein Beispiel dafür, dass der Allman-Stil nicht immer durchgängig angewendet werden kann. Da der Klammersetzungs-Stil aber gerade eine einheitliche Formatierung des Quelltextes ermöglichen soll, würde eine inkonsistente Anwendung des Stils - mal soll er eingehalten werden, mal nicht - das Aus für diesen Stil bedeuten. Schon rein praktisch stelle ich es mir sehr kompliziert vor: IDEs bieten eine automatische Quelltext-Formatierung, die bei den folgenden Beispielen entsprechende Fehler einbauen würden. Keine schöne Vorstellung.

Zu den Beispielen: Das JavaScript-Beispiel steht in Douglas Crockfords "JavaScript: The Good Parts", das Groovy-Beispiel war dann nicht schwer davon abzuleiten. Hier das von mir adaptierte Beispiel in JavaScript im K&R-Stil:
var assertEquals = function(expected, actual) {
if(expected === actual) return;
alert('AssertionError! Expected: <' + expected + '>, but was <' + actual + '>');
}

var foo = function() {
return {
key: 'value'
};
}

assertEquals('value', foo().key);


Die erste Methode assertEquals ist ein Ersatz für JavaScripts Assertions-Losigkeit. assertEquals hier ist das, was man von JUnits assertEquals als Java-Entwickler kennt. Die Methode vergleicht einen erwarteten mit einem tatsächlichen Wert: Sind sie gleich, passiert nichts (ergo: Alles okay), andernfalls wird eine entsprechende Fehlermeldung als modaler Dialog ausgegeben. Dies ist noch nicht das eigentliche Beispiel, erleichtert aber die Lesbarkeit im Folgenden.

Die Methode foo gibt ein Objekt zurück. In JavaScript können Objekte literal durch geschweifte Klammern dargestellt werden. Das Objekt hat eine Instanzvariable key mit dem Wert value.

Die letzte Zeile ist die Zusicherung: Wenn foo aufgerufen wird, dann kann ich auf die Variable key zugreifen und erhalte den Wert value. So weit, so alles gut.

Nun formatiere ich die Methode foo nach dem Allman-Stil:
var foo = function()
{
return
{
key: 'value'
};
}


Es gibt eine Fehlermeldung in der Zeile mit dem Aufruf der Zusicherung: foo() has no properties, foo() hat keine Variable key. Warum?

JavaScript kennt das Konzept des Semicolon Insertion: Fehlt am Ende einer Zeile ein Semikolon, dann wird es dort von JavaScript automatisch hinzugefügt. Das ist vielfach praktisch, aber ungünstig beim Allman-Stil und dem return-Statement. Was da nun eigentlich steht, ist dies:
var foo = function()
{
return;
{
key: 'value'
};
}


JavaScript kümmert sich nicht um das, was nach dem return-Statement kommt, es wird ignoriert, ohne Fehlermeldung. Wenn wir foo() ausgeben, dann sehen wir den wahren Rückgabewert: undefined. Klar, dass undefined keine Variablen kennt.

In Groovy sieht's ähnlich aus. Dieses Beispiel ist im K&R-Stil formatiert:
def foo() {
return { ->
return 42
}
}

assert 42 == foo().call()


Die Methode foo() gibt eine Closure zurück, die, wenn aufgerufen, 42 zurückgibt. In Groovy ist das return-Statement optional: Die letzte Anweisung einer Methode wird automatisch als return-Statement benutzt. Viele Entwickler schreiben aber gerne noch das return-Keyword hin, um explizit zu machen, dass hier etwas zurückgegeben wird.

Die Zusicherung (im Gegensatz zu JavaScript hat Groovy die schon eingebaut) erwartet 42, wenn die Closure, die foo() zurück gibt, ausgeführt wird. So weit, so alles gut.

Nun formatiere ich die Methode foo nach dem Allman-Stil:
def foo()
{
return
{ ->
return 42
}
}


Es gibt eine Fehlermeldung in der Zeile mit dem Aufruf der Zusicherung: java.lang.NullPointerException: Cannot invoke method call() on null object, auf foo() kann kein .call() aufgerufen werden. Warum?

Auch in Groovy darf man die Semikolons weglassen. Und auch in Groovy wird alles nach dem ersten return-Statement ignoriert. Folglich liefert foo() den Wert null zurück, was die NullPointerException erklärt; die folgende Closure wird ohne Fehlermeldung ignoriert.

Fazit: Ich möchte hier keinen Religionskrieg um den Klammersetzungs-Stil ausrufen oder Öl ins Feuer derer gießen, die sich da bekriegen. Ich fand es nur interessant, dass der Klammersetzungs-Stil in einigen Sprachen Konsequenzen außerhalb von subjektiv empfundener Ästethik oder Benutzungsfreundlichkeit haben kann. Ich sehe diese Beispiele nicht als Argument gegen Semicolon Insertion oder (optionalen und) mehrfachen return-Statements von Groovy und JavaScript; die Vorteile dieser Eigenschaften der beiden Sprachen überwiegen die Nachteile meiner Ansicht nach deutlich.

Meine Lehre, die ich aus den Beispielen ziehe: In Groovy oder JavaScript würde ich Abstand vom Allman-Stil nehmen, weil er nicht konsistent durchgehalten werden kann.

Übrigens I: Python- und Haskell-Entwickler können wirklich froh sein, dass sie durch derartige Religionskriege nicht durch müssen; Python und Haskell ordnen Blöcke durch tiefere Einrückung unter. Eingebaute Schönheit - meinem subjektiven Empfinden nach, versteht sich ;-)

Übrigens II: Bei den Sourcen von werkannwann.de und bei denen vom Grails-Buch formatieren wir Groovy und JavaScript nach K&R mit zwei Ausnahmen:
  1. Geschweifte Klammern einzeiliger Closures oder Methoden schreiben wir mit einem Leerzeichen Abstand zwischen den geschweiften Klammern und dem Inhalt in die gleiche Zeile wie den Closure-/Methoden-Inhalt. Beispiel:
    [1, 2, 3].each{ println it }
    Im oberen Beispiel würde foo() so aussehen:
    def foo() { return { return 42 } }

    Wobei wir das return-Keyword immer dort weglassen, wo es möglich ist. Dann wird daraus:
    def foo() { { 42 } }
  2. if-, do-, while- und for-Statements schreiben wir ohne geschweifte Klammern, wenn es sich beim folgenden Block um einen Einzeiler handelt. Wenn diese Statements einen einzeiligen Block haben, dann schreiben wir ihn in die gleiche Zeile direkt nach dem Statement. Beispiel:
    if(guardCondition) return
Von beiden Ausnahmen machen wir recht häufig Gebrauch, da sowohl Groovy als auch JavaScript einen wesentlich kompakteren Code erzeugt als beispielsweise Java, und wir so auf relativ viele Einzeiler kommen.

4 Kommentare:

  1. Na, da machst Du aber gleich den nächsten Glaubenskrieg auf: Ist es richtig (oder ein Designfehler?), wenn in Sprachen wie Python/Javascript/Groovy die Menge/Art/Platzierung von Whitespace eine Bedeutung hat?

    Meiner Meinung nach ist das tatsächlich hilfreich, aber bestimmt gibt es auch Ignoranten, die das anders sehen. Bestimmt die gleichen Bornierten, die den Allman-Stil propagieren. ;-)

    AntwortenLöschen
  2. Huch, ein Kommentar. Die Benachrichtigungs-E-Mail scheint der Spamfilter geschluckt zu haben. Sorry, Marko, für die späte Antwort.

    Bin mir nicht sicher, ob ich Dich da richtig verstanden habe. In Python und Haskell ist die Einrückungstiefe und damit auch die Whitespaces entscheidend. Bei Groovy und JavaScript ist Whitespace bzgl. der Einrückung nicht entscheidend.

    In all diesen Sprachen (Haskell, Groovy, JavaScript, Python) ist Whitespace entscheidend: Jede dieser Sprachen unterscheided per Whitespace zwischen einem Keyword und einem Bezeichner. In Groovy ist z.B. die Zeile "deffoo = 1" in einem Skript etwas anderes als "def foo = 1".

    Bin mir nun nicht sicher, was genau Du mit dem Satz "Ist es richtig, wenn ..." gemeint hast. Etwas von dem, was ich da gerade geschrieben habe. Oder noch etwas ganz anderes?

    Ich möchte wirklich keinen (!) Glaubenskrieg vom Zaun brechen. Auch nicht klammheimlich und versteckt, wie man es vielleicht herauslesen könnte :-) Über diese Phase der "Sich-die-Programmierhörner-abstoßen" bin ich wirklich drüber hinweg. Ich fand es halt nur interessant, dass es unter ganz bestimmten Umständen tatsächlich nachweisliche Argumente auf Sachebene gibt, die gegen ein konsistentes Verhalten eines Klammersetzungsstils spricht. Aber selbst dieses Argument kann man spielend entkräften, indem einer angibt, so komische return-Konstrukte niemals anzuwenden...

    Müßig, solche Glaubenskriege...

    AntwortenLöschen
  3. Das wichtigste an meinem Kommentar war der zwinkernde Smiley am Ende: Es war nicht sehr ernst gemeint, mir entfleuchte beim Lesen Deines Beitrages nur der Gedanke, dass es ein Kuhhandel sei, wenn man keine Diskussion mehr über die richtige Klammersetzung mehr hat, dafür aber sich darüber streiten kann, ob es richtig ist, dass in einer Programmiersprache Whitespaces wichtig sind.

    Beim Stellenwert von Whitespace in Programmiersprachen sehe ich ein Spektrum: Java auf der einen Seite, Groovy/Ruby/Javascript in der Mitte, Haskell/Python am anderen Extrem.

    In Java trennt Whitespace Tokens. (Fast) niemand murrt darüber oder will das diskutieren.

    In Groovy/Ruby/Javascript ist die Einrückung nicht relevant. Was aber relevant sein kann, sind Zeilenumbrüche, da diese ein Semikolon als Befehlstrenner ersetzen können.

    In Python werden durch Einrückung Blöcke gekennzeichnet und Zeilenumbrüche können als Befehlstrenner benutzt werden. Haskel kenne ich nicht so genau. Soweit ich es verstanden habe, ist es dort ähnlich wie in Python, nur dass es gar kein Zeichen wie ein Semikolon gibt, welches man anstatt eines Zeilenumbruches verwenden kann.

    (Genaugenommen sind auch in Java Zeilenumbrüche nicht ganz so egal, wenn man mal an Zeilenkommentare und String denkt, aber das steht auf einem anderen Blatt. Wer will schon dogmatisch sein? ;-) )

    Interessanterweise pflege ich in Java eher einen "kompakten" Programmierstil z.B. ohne Zeilenumbrüchen vor öffnenden Klammern, aber dort wo ich eine Sinngrenze aufzeigen will. Gleichzeitig mag ich es, wenn bei Programmiersprachen Whitespace relevant ist, da sie mich dabei unterstützen die Struktur eines Programmes mit wenigen Blicken zu erfassen.

    AntwortenLöschen
  4. Das wichtigste an meinem Kommentar war der zwinkernde Smiley am Ende: Es war nicht sehr ernst gemeint, mir entfleuchte beim Lesen Deines Beitrages nur der Gedanke, dass es ein Kuhhandel sei, wenn man keine Diskussion mehr über die richtige Klammersetzung mehr hat, dafür aber sich darüber streiten kann, ob es richtig ist, dass in einer Programmiersprache Whitespaces wichtig sind.

    Beim Stellenwert von Whitespace in Programmiersprachen sehe ich ein Spektrum: Java auf der einen Seite, Groovy/Ruby/Javascript in der Mitte, Haskell/Python am anderen Extrem.

    In Java trennt Whitespace Tokens. (Fast) niemand murrt darüber oder will das diskutieren.

    In Groovy/Ruby/Javascript ist die Einrückung nicht relevant. Was aber relevant sein kann, sind Zeilenumbrüche, da diese ein Semikolon als Befehlstrenner ersetzen können.

    In Python werden durch Einrückung Blöcke gekennzeichnet und Zeilenumbrüche können als Befehlstrenner benutzt werden. Haskel kenne ich nicht so genau. Soweit ich es verstanden habe, ist es dort ähnlich wie in Python, nur dass es gar kein Zeichen wie ein Semikolon gibt, welches man anstatt eines Zeilenumbruches verwenden kann.

    (Genaugenommen sind auch in Java Zeilenumbrüche nicht ganz so egal, wenn man mal an Zeilenkommentare und String denkt, aber das steht auf einem anderen Blatt. Wer will schon dogmatisch sein? ;-) )

    Interessanterweise pflege ich in Java eher einen "kompakten" Programmierstil z.B. ohne Zeilenumbrüchen vor öffnenden Klammern, aber dort wo ich eine Sinngrenze aufzeigen will. Gleichzeitig mag ich es, wenn bei Programmiersprachen Whitespace relevant ist, da sie mich dabei unterstützen die Struktur eines Programmes mit wenigen Blicken zu erfassen.

    AntwortenLöschen