Programmierkonzepte, Teil 4: Dynamische Typisierung

the next big thing Golo Roden  –  43 Kommentare

Typsysteme lassen sich auf vielerlei Art einteilen: statisch, dynamisch, streng, schwach, explizit, implizit, nominal, strukturell … Derzeit erfahren vor allem dynamisch typisierte Sprachen viel Zuspruch, doch das war nicht immer so. Ein Überblick.

Programmierkonzepte

Die erste höhere Programmiersprache, Fortran, basierte auf einem statischen Typsystem. Von dort vererbte sich dessen Verwendung über Algol nach C, was schließlich viele der heute verbreiteten Sprachen wie C++, Java und C# beeinflusste.

Das Verwenden eines statischen Typsystems stellt sicher, dass ein Typ bereits zur Übersetzungszeit bekannt ist. Das kann auf zwei Wegen erfolgen: Entweder muss der Entwickler die verwendeten Typen explizit benennen oder der Compiler ermittelt sie eigenständig. Im ersten Fall spricht man von einem expliziten, im zweiten von einem impliziten Typsystem.

In C# beispielsweise sind beide Vorgehen möglich. Eine lokale Variable lässt sich dort explizit deklarieren, indem der Entwickler den gewünschten Typ angibt. Der Compiler stellt sicher, dass der anschließend zugewiesene Wert zum Typ der Variablen passt:

int[] primes = new int[] { 2, 3, 5, 7, 11 };

Alternativ lässt sich die Variable aber auch implizit deklarieren, wozu in C# das var-Schlüsselwort dient. In dem Fall ist die Angabe von new int[] zwingend erforderlich, im vorigen Beispiel hätte sie auch entfallen können. In beiden Fällen ist die Variable primes vom Typ int[], enthält also ein Array von Ganzzahlen:

var primes = new int[] { 2, 3, 5, 7, 11 };

Die Fähigkeit des Compilers, den Typ eigenständig herleiten zu können, wird als Type Inference bezeichnet und findet unter anderem in Sprachen wie Haskell, F# oder OCaml weitreichende Verwendung. Definiert man in F# beispielsweise die Funktion add wie folgt, werden der Typ für den Parameter und den Rückgabewert vom Compiler als int hergeleitet, da zu n eine Ganzzahl addiert wird:

let add n = n + 100

Anders verhält es sich in dynamisch typisierten Sprachen, bei denen die verwendeten Typen erst zur Laufzeit feststehen. In JavaScript beispielsweise lässt sich vor der Ausführung nicht erkennen, von welchem Typ der Parameter n sein wird:

const add = function (n) {
return n + 100;
};

Es liegt nahe, dass es sich bei n um einen Parameter vom Typ number handelt, doch lässt sich die Funktion ebenso gut mit einem string aufrufen. In dem Fall würde JavaScript die Zahl 100 in eine Zeichenkette konvertieren und die beiden Zeichenketten anschließend zu einer verbinden:

console.log(add('the native web'));
// => 'the native web100'

Selbst bei Typen, für die die Addition nicht sinnvoll ist, versucht JavaScript eine mehr oder weniger intelligente Konvertierung durchzuführen. So ergibt beispielsweise die Addition eines leeren Arrays mit der Zahl 100 die Zeichenkette 100. Über Sinn und Unsinn der Umwandlung lässt sich sicherlich streiten. Das Beispiel zeigt aber, dass JavaScript Typen bei Bedarf zur Laufzeit anpasst:

console.log(add([]));
// => '100'

Das wäre in C# beispielsweise nicht möglich, zumindest nicht ohne explizite Operatoren für die Konvertierung. Daher ist JavaScript schwächer typisiert als C#.

Von den eingangs genannten Arten bleiben die nominale und die strukturelle Typisierung, die sich allerdings sehr leicht abgrenzen lassen. Eine Sprache mit nominaler Typisierung unterscheidet Typen an Hand ihrer Namen, eine Sprache mit struktureller Typisierung unterscheidet sie an Hand ihrer Struktur: Sind zwei Typen gleich aufgebaut, gelten sie als kompatibel. Ein modernes Beispiel für eine Sprache mit struktureller Typisierung ist TypeScript.

Die erste höhere Sprache, die dynamisch typisiert war, war wie auch bei logischen Bedingungen, Funktionen höherer Ordnung und Rekursion Lisp. Dort ist es, ähnlich wie in JavaScript, möglich, eine Funktion zu definieren, bei der die Typen von Parametern und Rückgabewert erst zur Laufzeit feststehen:

(defun add (n)
(+ n 100))

Anders als JavaScript ist Lisp aber weitaus strenger, was die Kompatibilität der möglichen Typen angeht. Ruft man die Funktion mit einer Zahl auf, wird die entsprechende Summe berechnet, ruft man sie hingegen mit einer Zeichenkette auf, erhält man eine passende Fehlermeldung, die auf nicht zueinander passende Typen hinweist:

(add 23)
; => 123

(add "the native web")
*** - +: "the native web" is not a number

tl;dr: Die erste Sprache mit einem dynamischen Typsystem war Lisp. Im Gegensatz beispielsweise zu JavaScript ist Lisp aber weitaus strenger, was die Kompatibilität von semantisch nicht zueinander passenden Typen angeht, so dass man dort aussagekräftige Fehlermeldung erhält, die auf einen Typfehler hinweisen.