Syntactic Sugar

Lesezeit: 13 Minuten

Syntactic Sugar, zu Deutsch syntaktischer Zucker, ist diejenige Syntaxerweiterung einer Programmiersprache, die eine alternative Schreibweise ermöglicht, dabei aber keine neue Funktionalität einführt. Das Ziel von Syntactic Sugar ist es, Code expliziter zu machen bzw. die Lesbarkeit zu verbessern. Eine Syntaxerweiterung kann als Syntactic Sugar bezeichnet werden, wenn sich der Code auch gleichbedeutend mit der vorherigen Syntax schreiben lässt.

In diesem Artikel möchte ich drei Beispiele von Syntactic Sugar näher beleuchten und anschließend ein Fazit ziehen. Bei den drei Beispielen handelt es sich um den Ternary Operator, Strings und Operatoren im Allgemeinen.

Ternary Operator

Der Ternary Operator ist vermutlich eines der verbreitetsten Beispiele für Syntactic Sugar:

// if else
if (condition) {
    result = value1;
} else {
    result = value2;
}

// ternary
result = condition ? value1 : value2;

Wie das Beispiel demonstriert, benötigt die If-Else-Schreibweise fünf Zeilen Code für etwas vermeintlich sehr Triviales. Der Ternary Operator erlaubt es, die gleiche Logik in einer Zeile auszudrücken. Es stellt sich vielleicht die Frage, warum If-Else dann nicht vollständig durch den Ternary Operator ersetzt werden kann. Die Antwort darauf ist, dass sich zwar Ternary Operations auch verschachteln lassen, dies aber schnell sehr unübersichtlich wird, denn dem Ternary Operator fehlt ein entscheidendes Feature: das else if

if (condition1) {
    result = 1;
} else if (condition2) {
    result = 2;
} else if (condition3) {
    result = 3;
} else {
    result = -1;
}

Versucht man diese Logik mit dem Ternary Operator nachzubilden, ergibt sich folgender, schwer lesbarer, Code:

result = condition1
    ? 1
    : condition2
        ? 2
        : condition3
            ? 3
            : -1;

Verzichtet man auf den Einsatz des if else, so ergibt sich nur mit if und else ein ähnliches Konstrukt:

if (condition1) {
    result = 1;
} else {
    if (condition2) {
        result = 2;
    } else {
        if (condition3) {
            result = 3;
        } else {
            result = -1
        }
    }
}

Beide Alternativen haben gegenüber dem if else den Nachteil, dass jede Bedingung eine weitere Verschachtelungstiefe erfordert. Der Ternary Operator ist also Syntactic Sugar, der in einem ganz bestimmten Fall seinen Vorteil ausspielt: Wenn die Fallunterscheidung trivial ist.

Ein grundsätzliches Problem beim Ternary Operator sehe ich darin, dass die Notation gänzlich anders ist als die des if else. Dadurch wird die Komplexität spürbar erhöht, denn nun können zwei völlig unterschiedliche Syntaxen das Gleiche ausdrücken. Dass es auch anders geht, zeigt die Sprache Python, dort wird die Ternary Operation nämlich mit den Schlüsselwörtern if und else realisiert:

result = value1 if condition else value2

Nicht jede Sprache unterstützt den Ternary Operator, Go z.B. verzichtet aufgrund der bekannten Nachteile bewusst darauf.

Ein weiteres Beispiel für Syntactic Sugar in Verzweigungen ist das Switch-Statement. Auch dieses hat seine Nachteile und ist nicht unumstritten. Python hat 30 Jahre lang, bis zur Einführung von Pattern Matching, auf ein Switch-ähnliches Konstrukt verzichtet, da die Abbildung von If-Else in Python prägnant genug ist.

Today’s syntactic sugar is tomorrow’s syntax

Dieses Zitat unbekannter Herkunft finde ich sehr treffend. Die wenigsten würden auf den Gedanken kommen, dass if else auch nur Syntactic Sugar für eine Verschachtelung von if und else ist. Dass dies so ist, habe ich weiter oben demonstriert. Diese Syntaxerweiterung ist aber einfach so etabliert und vorteilhaft, dass sie gar nicht als Syntactic Sugar wahrgenommen wird.

Strings

Dass Strings auch nur Syntactic Sugar für Char-Arrays sind, zeigt die Sprache C sehr deutlich. Die folgenden Anweisungen in C führen dazu, dass 3x die Zeichenkette hello ausgegeben wird:

char string1[7] = {'h', 'e', 'l', 'l', 'o', '\n', '\0'};
char string2[7] = {"hello\n\0"};
char string3[7] = "hello\n\0";
printf(string1);
printf(string2);
printf(string3);

Strings in C++ sind Objekte und Objekte werden über den Aufruf eines Konstruktors erzeugt. Jedoch kann man sich über Syntactic Sugar den expliziten Konstruktoraufruf ersparen:

std::string s = std::string("hello"); // klassischer Konstruktor
std::string s = "hello"; // Syntactic Sugar

In Java, einer Sprache die sich syntaktisch sehr an C und C++ orientiert hat, ist es sogar so, dass die Konstruktor-Variante bei Strings in der Regel unbeabsichtigte Nebeneffekte hat und daher fast immer die Kurzform verwendet werden sollte.

var string1 = new String("hello"); // bad practice
var string2 = "hello"; // best practice

Strings sind ein weiteres Beispiel für Syntactic Sugar, der sich zu einem Teil der allgemeinen Sprachsyntax entwickelt hat.

Operatoren

Einige Sprachen wie Java bieten vielfältige Möglichkeiten, eine Zahl um eins zu erhöhen:

var n;

n = 3;
n = n + 1; // assignment

n = 3;
n += 1; // addition assignment

n = 3;
n++; // post increment

n=3;
++n; // pre increment

Bei den letzten drei dieser vier Varianten handelt es sich um Syntactic Sugar. Während die letzten zwei Varianten die Zahl um eins erhöhen, bzw. analog mit -- den Wert um eins reduzieren, kann der Wert im addition assignment um einen beliebigen Ausdruck inkrementiert werden. Analog gibt es meist weitere Operatoren wie -=, *=, /= und %= (Subtraktion, Multiplikation, Division, Modulo/Rest).

Ich empfinde += und -= klar als Bereicherung, da sie die Intention der Anweisung hervorheben: Die Berechnung wird auf Basis des aktuellen Wertes durchgeführt. n = n + 1 bringt das nicht so klar zum Ausdruck, insbesondere dann nicht wenn die Formel auf der rechten Seite komplexer ist, etwa n = 3 * m - 4 / 1 + n. Die Operatoren *=, /= und vor allem %= finde ich weniger wertvoll. Zum einen ist es konsequent, auch diese Operatoren als Zuweisung anzubieten, zum anderen kommen sie aber deutlich seltener zum Einsatz und Syntactic Sugar kann nur dann seine Stärke ausspielen, wenn die Syntax gut verständlich ist. Gerade der Operator %= könnte eher Verwirrung stiften, wenn man an die Bedeutung von % in einigen Sprachen oder Frameworks denkt, wie etwa in Perl oder Java Server Pages (JSP).

Die Operatoren ++ und -- sind umstritten, insbesondere wenn sie wie in Java in zwei Varianten vorliegen. Beim post increment wird der Wert erst inkrementiert, wenn der aktuelle Ausdruck ausgewertet wurde. Beim pre increment wird der Wert vorher erhöht. Diese Differenzierung macht nur dann Sinn, wenn der Operator in Zuweisungen oder Bedingungen integriert wird, was stark zu Lasten der Lesbarkeit geht und außerdem einen Seiteneffekt darstellt. Denn durch das wiederholte Auswerten eines solchen Ausdrucks in einem Debugger beispielsweise ändert sich der Wert permanent. Vielfach wird es daher als Best Practice angesehen, diese Operatoren wenn überhaupt nur alleinstehend zu verwenden.

Einige Sprachen, darunter C++, Python und Ruby, ermöglichen es Operatoren zu überladen. Dies klingt auf den ersten Blick praktisch, denn lange Rechenformeln mit Objekten sind nicht besonders gut lesbar. Ein einfaches Beispiel mit einem Java-BigDecimal verdeutlicht dies:

System.out.println(new BigDecimal(100)
    .add(new BigDecimal(50))
    .subtract(new BigDecimal(120))
    .divide(new BigDecimal(4))
    .multiply(new BigDecimal(6)))

Der ausgegebene Wert ist übrigens 45.0.

Operator Overloading ist Syntactic Sugar. In Kombination mit den verschiedenen Operatoren für die gleiche Rechenart, wie oben die Addition, hat das aber durchaus seine Tücken. Um dies zu demonstrieren schauen wir uns die Klasse QuirkyNumber an:

class QuirkyNumber {
	public:
		QuirkyNumber(int number) {
			n = number;
		}

		int value() {
			return n;
		}

		QuirkyNumber operator+(QuirkyNumber& qn) {
			return value() + qn.value();
		}

		QuirkyNumber operator+=(QuirkyNumber& qn) {
			n = value() - qn.value();
			return value();
		}

	private:
		int n;

};

Diese Klasse ist sehr einfach gestrickt. Sie kapselt eine Ganzzahl, das interne Feld n. An den Konstruktor wird der gewünschte Wert von n übergeben. Die Methode value() gibt den aktuellen Wert von n preis. Die Klasse hat eine Besonderheit: Sie überlädt die Operatoren + und +=, so dass Instanzen dieser Klasse wie primitive Zahlentypen betrachtet werden können.

Wer genau hinschaut wird allerdings feststellen, dass sich bei der Umsetzung ein Fehler eingeschlichen hat und dies wird hier zum Problem, denn wer würde auf die Idee kommen, dass n = n + 5 und n += 5 zu unterschiedlichen Ergebnissen führen? Tatsächlich handelt es sich aber um zwei völlig unterschiedliche Implementierungen, so dass sich eine Abweichung leicht einschleichen kann. Die Logik dieser Klasse ist ziemlich offensichtlich, anders sieht es aber aus, wenn die Operatoren für komplexere Objekte überladen werden, um z.B. Datumsberechnungen mit Zeitzonenangabe zu ermöglichen, oder physikalische Sachverhalte abzubilden.

Ich verwende nun QuirkyNumber, um den Fehler in der Klasse zu demonstrieren:

	QuirkyNumber n1 = QuirkyNumber(5);
	QuirkyNumber n2 = QuirkyNumber(3);
	QuirkyNumber n3 = n1 + n2;
	n1 += n2;
	std::cout << n1.value() << std::endl;
	std::cout << n2.value() << std::endl;
	std::cout << n3.value() << std::endl;

Auf den ersten Blick haben n1 und n3 den gleichen Wert, denn n3 ist die Summe von 5 und 3, und zu n1 wird nachgelaget der Wert von n2 hinzuaddiert, also ebenfalls 5 plus 3. In der Überladung von += wird aber versehentlich statt einer Addition eine Subtraktion durchgeführt, womit n1 den Wert 5 - 3 = 2 erhält.

Das Beispiel zeigt eindrucksvoll, dass Syntactic Sugar seinen Preis und auch die Entscheidung, ob man Operatoren überladen möchte oder nicht, kein Selbstgänger ist.

Alles ist Syntactic Sugar

Es gibt keine formale Definition von Syntactic Sugar, und damit bleibt dieser Begriff Auslegungssache. Bytes sind Syntactic Sugar für Bits. Assembler ist Syntactic Sugar für Maschinencode. C ist Syntactic Sugar für Assembler. Java ist Syntactic Sugar für C. Quellcode ist Syntactic Sugar für den Abstract Syntax Tree. Schleifen sind Syntactic Sugar für Goto-Statements. Potenz ist Syntactic Sugar für Multiplikation. Multiplikation ist Syntactic Sugar für Addition. Ampel ist Syntactic Sugar für Lichtzeichenanlage.

Was letztlich als Syntactic Sugar bezeichnet wird hängt meiner Meinung nach davon ab, wie etabliert und verbreitet bestimmte Syntax ist, zu welchem Zeitpunkt sie es in eine Sprache geschafft hat und ob sie ein Problem allgemein gut adressiert oder eher bei Randfällen punktet. Syntactic Sugar ist wie Umgangssprache, ein Akronym oder eine Abkürzung.

Die Anforderungen an Software sind vielfältig. Wenn man aber nicht gerade zu systemnahen Sprachen wie C, C++, Go oder Rust greift, dann sollten Aspekte wie Lesbarkeit und Erweiterbarkeit einen sehr hohen Stellenwert haben und Performanceoptimierungen sind erstmal zweitrangig. Syntactic Sugar kann dazu sehr gut beitragen, oder das Gegenteil bewirken und zu Syntactic Salt werden, wenn es falsch eingesetzt oder ins Extreme getrieben wird. In diesem Artikel habe ich einige Beispiele und ihre Fallstricke aufgezeigt.

Folgende Fragen sollte ich stellen, wenn ich Syntactic Sugar einsetze: Macht der Einsatz von Syntactic Sugar an dieser Stelle den Code lesbarer, ausdrucksstärker, konziser? Ist das Vorgehen anerkannt, passt es zu gängigen Best Practices, können wir uns im Team darauf verständigen und werden auch Neuzugänge den Code gut verstehen können? Es gibt ganze Bibliotheken für Syntactic Sugar, wie etwa Lombok in Java, die sich, wenn man sich darauf einlässt, durch den kompletten Code ziehen und damit einen starken Impact haben. Die Frage ob dies eine gute oder schlechte Entscheidung ist, kann aber nur das Team selbst für sich individuell beantworten. Für Syntactic Sugar gilt wie für vieles: Die Dosis macht das Gift.

Abschließen möchte ich mit einem Zitat des Erfinders der Sprache Perl:

To me, one of the most agonizing aspects of language design is coming up with a useful system of operators. To other language designers, this may seem like a silly thing to agonize over. After all, you can view all operators as mere syntactic sugar – operators are just funny looking function calls.

Larry Wall