Dynamische Typisierung: Vorteile von PHP

7. Oktober 2015

Ich bin beruflich momentan überwiegend mit PHP in Kontakt, wie man wohl auch schon an meinem letzten Beitrag gemerkt hat ;).
PHP war 1998/99 meine erste Programmiersprache und trotz vielerlei Kritik mag ich sie bis heute. Genau wie Perl, Python und Ruby ist auch PHP dynamisch typisiert, welches einer der Gründe der vielseitigen Kritik ist. Obgleich PHP und generell dynamische Typisierung umstritten sind, empfinde ich letzteres als überwiegend angenehm, da sie viele Dinge ermöglicht, die so nicht oder zumindest nicht so einfach in anderen Sprachen umsetzbar wären. Natürlich gehen damit auch Probleme einher. So ist nicht auf den ersten Blick ersichtlich, welcher Typ vorliegt und man muss zur Laufzeit überprüfen, ob die übergebene Variable auch vom gewollten Typ ist. In PHP schaffen Type-Hints jedoch eine gewisse Abhilfe.
Ein weiterer Nachteil dynamischer Typisierung ist, zumindest im Falle von PHP, dass jegliche Typ-Bindung verloren geht. So kann eine Variable in dem einen Moment ein int und im nächsten schon wieder ein string sein. Für Einsteiger ist dieses Verhalten wohl eher kontraproduktiv, aber für bereits geübte Programmierer bringt diese Besonderheit, wie ich finde, durchaus Vorteile mit sich. Um nur einen zu nennen: ein einziger Invalid-Typ. In anderen Sprachen nimmt man für gewöhnlich den Default-Wert des jeweiligen Typs, z.B. 0 für int oder „“ für string. Aber die Default-Werte können auch ein valides Resultat sein. Also wie unterscheiden wir zwischen invaliden und validen Werten?

Dazu ein Beispiel in C++:

int readNumber(const std::string& str) {
    if (str.empty() || !std::isdigit(str[0])) {
        return ...; // Was soll ich returnen?!
    }
    
    // ...
}

Ein möglicher Lösungsweg wäre, dass wir die Variable, in der wir ohnehin das Resultat speichern, als Referenz übergeben und als Rückgabewert bool wählen. Sofern ein valider Wert herauskommt, speichern wir ihn in der Referenz und geben true zurück, andernfalls lassen wir die Referenz wie sie ist und geben false zurück.

Das gleiche Beispiel in C++ mit geänderten Verhalten:

bool readNumber(int& number, const std::string& str) {
    if (str.empty() || !std::isdigit(str[0])) {
        return false;
    }
    
    // ...
}

Etwas geschichtlicher Hintergrund: In den Anfängen von C wurde definiert, das 0 für false und für den ungültigen Wert des Makros NULL steht, während alles was nicht 0 ist mit true gleichzusetzen ist. Allerdings wurde das Makro NULL nicht dahingehend spezifiziert. So war, je nach Compiler, entweder 0 oder (void*) 0 gleichbedeutend mit NULL. Diese Compiler-Abhängigkeit und die implizite Konvertierung von 0 zu NULL und vice versa ermöglichte diese Art von Problemen:

void foo(int) { }
void foo(int*) { }

foo(0); // Compiler-Fehler: Der Aufruf ist doppeldeutig
foo(NULL);  // Compiler-Fehler: Der Aufruf ist doppeldeutig (sofern NULL 0 ist)

Dieses Problem brachte im Lauf der Zeit diverse Lösungsversuche auf. Mit dem Aufkommen anderer Sprachen gab es dafür fest definierte Sprachelemente und Syntax, wie null, nil, Maybe-Konstrukte oder Nullable-Typen. Und mit dem Standard C++11 gibt es auch für C++ ein solches Konstrukt, genannt nullptr.
Nullable- und Maybe-Typen lösen das von mir angesprochene Problem eines einheitlichen Invalid-Wertes, bilden aber dafür in Form eines Wrappers eine weitere Abstraktionsschicht.
In PHP ist es möglich, dank der dynamischen Typisierung, einen übergreifenden Invalid-Wert zu nutzen, ohne einen Wrapper zu benutzen. Eine gute Wahl wäre null.
Somit lässt sich nach jedem Aufruf prüfen, ob der Rückgabewert null ist oder nicht. Je nachdem ist er eben (un)gültig.
So würde unser obiges Beispiel folgendermaßen in PHP aussehen:

function readNumber($str) {
    if (!is_string($str) || strlen($str) === 0 || !ctype_digit($str[0])) {
        return null;
    }
    
    $ival = 0;
    // ...
    
    return $ival;
}

Entweder ist der Rückgabewert ein int oder null.

Auf Grund des Neu-Kontakts las ich mich wieder in den PHP-Quellcode ein und sah mir nochmal an, wie PHP seine dynamische Typisierung intern regelt. Die Vorgehensweise ist leicht zu verstehen: Mittels union können effizient verschiedene Datentypen unter einem Dach gehalten und ausgetauscht werden.
Ich begann ähnliches Verhalten in C++ zu programmieren, um sogenannte Variants zu konstruieren.
Ein ganz einfaches Beispiel eines solchen Variants:

Wir brauchen die union mit den verschiedenen Datentypen sowie einen Helfer in Form des enum’s. So wissen wir jederzeit, was für einen Typ wir haben. Und je nachdem welcher Typ vorliegt, können wir diesen auch entsprechend ausgeben. Und das ist auch schon die ganze „Magie“ dahinter.

Für ein Variant-Beispiel, welches deutlich mehr PHP parallelen aufweist, wäre das folgende passend. Alle Variablen sind geteilte Referenzen mit einem Referenz-Zähler, genau wie es in PHP der Fall ist. Darüber hinaus hat dieses Beispiel auch eine Möglichkeit, um Variablen zu „klonen“ (ebenfalls wie PHP) und zu casten. In C wäre ein cast innerhalb eines unions automatisches und definiertes Verhalten. Leider ist das in C++ nicht der Fall, weswegen der cast etwas umfangreicher ist: