Typinferenz in Luau

Seit 2006 nutzen Roblox-Entwickler die Programmiersprache Lua, um Spiele und interaktive Erlebnisse auf Roblox zu erstellen. Roblox-Entwickler kommen aus allen Bereichen des Lebens und verfügen über unterschiedlichste Erfahrungsstufen – ebenso vielfältig sind ihre Kreationen.
Einige dieser Kreationen sind wirklich ausgefeilte Softwarewerke. Viele umfassen Zehntausende von Codezeilen.
Obwohl Lua eine wunderbare Programmiersprache ist und wir sie sehr mögen, haben wir dieselbe Lektion gelernt, die auch die Webentwickler-Community gelernt hat: Das Schreiben großer Anwendungen mit dynamisch typisierten Programmiersprachen ist schwierig!
Wir haben Luau entwickelt, um diese Lücke zu schließen.
Im Kern ist Luau in erster Linie eine Typinferenz-Engine und erst in zweiter Linie ein Typ-Checker. Diese Philosophie leiten wir aus erfolgreichen früheren Arbeiten wie den OCaml- und TypeScript-Compilern ab.
Allerdings ist Lua nicht ganz dasselbe wie OCaml oder JavaScript. Um Luau so gut wie möglich zu machen, ist es sehr wichtig, dass wir die Besonderheiten von Lua genau modellieren.
Lassen Sie uns zunächst einige Grundlagen erläutern.
Typinferenz 101
Bei der Typinferenz geht es fast immer darum, festzustellen, dass ein Typ A mit einem anderen Typ B identisch sein muss. Das ist eine Vereinfachung, aber weniger als man vielleicht denkt.
Beginnen wir mit einem ganz kleinen Beispiel:
function id(x)
return x
end
local a = 5
local b = id(a)Wenn wir den Typ von `id` prüfen, erstellen wir zwei Platzhaltertypen: den Typ von `x` und den Rückgabetyp der Funktion.
Diese Platzhalter deuten nur eines an: Wir wissen nichts über sie.
Anschließend analysieren wir die Anweisung `return`. Wir stellen fest, dass der Typ, den die Funktion zurückgibt, unabhängig davon, um welchen Typ es sich handelt, mit dem Typ von `x` übereinstimmt. Da für `x` keine weiteren Einschränkungen gelten, wird der endgültige abgeleitete Typ von `id` als die generische Funktion `(T) -> T` abgeleitet.
(Luau bietet noch keine Syntax für generische Funktionen, aber die internen Datenstrukturen können diese darstellen.)
Wenn es an der Zeit ist, einen Typ für b abzuleiten, können wir unser Wissen über id nutzen. Wir nehmen den Typ von id und binden dessen Parametertyp an den konkreten Argumenttyp, den wir haben. Da der Rückgabetyp der Funktion bereits an ihren Parametertyp gebunden ist, wird (T) -> T als (number) -> number instanziiert. Daraus können wir ableiten, dass der Typ von b number ist.
Fast die gesamte von Luau durchgeführte Typüberprüfung ist eine Erweiterung dieses Konzepts.
Mehrere Rückgabewerte
In fast jeder weit verbreiteten Programmiersprache geben Funktionen genau einen Wert zurück. Viele Sprachen (insbesondere funktionale Sprachen) bieten einfache Tupel an, um Mehrfachrückgaben einfach und bequem zu gestalten.
Lua widersetzt sich diesem Trend, indem es jeder Funktion erlaubt, 0 oder mehr Werte zurückzugeben. Hier kommt kein Tupel-Mechanismus zum Einsatz, da es keine Möglichkeit gibt, das gesamte Tupel an einen einzigen Namen zu binden.
function take_five(a, b, c, d, e)
print(a, b, c, d, e)
end
function get_five()
return 1, 2, 3, 4, 5
end
-- a receives 1. The other return values are discarded.
local a = get_five()
-- foo, bar, and baz receive 1, 2, and 3, respectively.
-- Arguments 4 and 5 are discarded.
local foo, bar, baz = get_five()
-- the 5 return values from get_five are passed as
-- parameters to take_five
take_five(get_five())Dies bringt einige neue Herausforderungen mit sich. Welchen Typ hat die folgende Funktion `compose`?
function compose(f, g)
return function(...)
return f(g(...))
end
endIn anderen Sprachen könnten wir daraus schließen, dass der Rückgabetyp von `g` mit dem Argumenttyp von `f` übereinstimmen muss. In Lua muss `f` dieselbe Anzahl an Argumenten und dieselben Typen akzeptieren wie die von `g` zurückgegebenen.
In Luau stellen wir dies mit etwas dar, das wir als Typ-Pack bezeichnen. Es ist der Datenstruktur sehr ähnlich, die wir zur Beschreibung eines Typs verwenden, repräsentiert jedoch 0 oder mehr Typen.
Wie Typen unterstützen auch Typ-Packs das Konzept von Platzhaltern, die später an andere Packs gebunden werden können. Typ-Packs haben noch einige weitere Eigenschaften: Ihre Länge kann bekannt oder unbekannt sein, und wenn sie bekannt ist, können sie entweder eine feste oder eine variable Größe haben.
Luau modelliert Funktionen als ein Paar von Typ-Packs: eines für die Argumentliste und eines für die Rückgabewerte.
Wenn wir die Syntax „A...“ als generisches Typ-Pack definieren, können wir einen Typ für „compose“ schreiben:
((B...) -> C..., (A...) -> B...) -> (A...) -> C...
(Luau unterstützt diese Syntax ebenfalls noch nicht. In Kürze verfügbar!)
Tabellen
Tabellen sind beim Schreiben von Lua sehr wichtig. Sie sind unsere Arrays, Hash-Maps und Objekte in einem. Es liegt auf der Hand, dass die korrekte Ableitung von Tabellentypen aus Lua-Code ziemlich wichtig ist.
In Luau unterteilen wir Tabellen in 4 Kategorien:
- Tabellen, deren genaue Struktur wir kennen
- Tabellen, die stückweise aufgebaut werden
- Tabellenähnliche Objekte, die als Funktionsparameter übergeben werden, und
- Roblox-API-Datentypen
Wir bezeichnen diese als „sealed tables“, „unsealed tables“, „generic tables“ und „native classes“.
Versiegelte Tabellen
Ein sehr häufiger Fehler, den wir abfangen möchten, ist ein falsch geschriebener Eigenschaftsname bei der Zuweisung einer Tabelleneigenschaft.
local some_table = {some_property=0}
-- oops. I got the name of the property wrong
some_table.sone_property = 55Tabellen sind standardmäßig in der Regel „sealed“.
Unversiegelte Tabellen
Um reibungslos mit idiomatischem Lua arbeiten zu können, benötigen wir eine Möglichkeit, Funktionen und Module zu unterstützen, die Tabellen über mehrere Anweisungen hinweg aufbauen.
local Counter = {}
Counter.value = 0
function Counter.increment()
Counter.value = Counter.value + 1
return Counter.value
endIn diesem Beispiel wäre es unsinnig, die erste Zeile zu betrachten, den Typ „{}“ abzuleiten und in der zweiten Zeile einen Typfehler zu erzeugen, da wir erwarten, dass „Counter“ für immer leer bleibt.
Wir gehen daher davon aus, dass Counter eine unversiegelte Tabelle ist. Luau kann die genaue Struktur dieser Tabelle leicht erkennen, aber wir betrachten sie als erweiterbar.
Wir wollen es nicht zu einfach machen, eine Tabelle zu entsiegeln, daher wenden wir einige einfache Heuristiken an:
- Der Typ einer Tabelle ist unversiegelt, wenn sie mit einer leeren Literaltabelle initialisiert wird, und
- Unsealed-Tabellen werden in Sealed-Tabellen umgewandelt, sobald wir in einer Funktionssignatur auf eine solche stoßen.
Dies führt zu einer recht guten Benutzerfreundlichkeit.
function new_counter()
local Counter = {}
Counter.value = 0 -- OK. Counter is unsealed.
function Counter.increment()
Counter.value = Counter.value + 1
return Counter.value
end
return Counter
end
local c = new_counter()
c.value_ = 5 -- Not allowed. c is a sealed table here.Generische Tabellen
Beim Umgang mit nicht annotierten Funktionsparametern ist es selten möglich, die genaue Form eines Arguments zu bestimmen, das tabellenartig verwendet wird:
local function print_point(p)
print(‘X =’, p.X, ‘Y =’, p.Y)
endWir wissen zwar, dass „p“ die Eigenschaften „X“ und „Y“ hat, aber da die Funktion „print“ alles ausgeben kann, ist das auch schon alles. Diese Eigenschaften könnten jeden beliebigen Typ haben. Es könnten auch beliebig viele weitere Eigenschaften vorhanden sein. Es muss nicht einmal tatsächlich eine Tabelle sein; es könnte sich um einen Roblox-API-Typ wie „Vector3“ handeln.
(Mehr zur Roblox-API gleich)
Wie in OCaml und bestimmten anderen Programmiersprachen sind Luau-Tabellenparameter zeilenpolymorph. In Luau nennen wir sie generische Tabellen. Felder, deren Vorhandensein in einer generischen Tabelle abgeleitet wird, sind Anforderungen, die an Aufrufer gestellt werden. Andere Eigenschaften sind zulässig, solange die erforderliche Struktur vorhanden ist:
local a = print_point({X=3, Y=4})
local c = print_point({X=4, Y=3, Name='The Best Point'})
local b = print_point(Vector3.new(3, 4, 0))Die Roblox-API
Die Roblox-API stellt unserer ambitionierten Entwickler-Community eine ganze Reihe leistungsstarker Werkzeuge zur Verfügung. Die API besteht aus einer Vielzahl von C++-Klassen, die in Lua abgebildet wurden.
Es liegt auf der Hand, dass Luau diese API kennen muss. Dazu gehört auch das Wissen, dass Roblox-Klasseninstanzen eigentlich keine Lua-Tabellen sind. So kann beispielsweise die integrierte Funktion `pairs()` nicht verwendet werden, um die Eigenschaften eines Roblox-API-Typs zu durchlaufen.
Das Typsystem, das wir bisher für Luau beschrieben haben, ist ein vollständig strukturelles Typsystem. Lua (und Luau) betrachten Tabellen als nichts anderes als die Menge der Eigenschaften, die sie enthalten. Die Roblox-API passt nicht in dieses Modell. C++ bietet ein nominales Typsystem, in dem jede Klasse ihre eigene „Selbstheit“ besitzt. Es ist völlig typisch, dass zwei Klassen unterschiedlich sind, obwohl sie genau dieselbe Struktur teilen. Es gibt tatsächliche Roblox-Klassen, die diese Eigenschaft erfüllen, und Luau muss sie korrekt modellieren.
Wir lösen dies, indem wir einen tabellenähnlichen Typ für integrierte Roblox-Klasseninstanzen einführen. Klassentypen unterscheiden sich von Tabellentypen dadurch, dass sie Identitäten besitzen, die sie unterscheiden, selbst wenn sie dieselben Methoden mit denselben Typen unterstützen. Sie unterstützen auch das Konzept der Vererbung, genau wie die C++-Klassen, die sie modellieren sollen.
Fazit
Das Ableiten statischer Typen aus einer dynamischen Sprache wie Lua birgt viele Herausforderungen. Viele dieser Herausforderungen sind spezifisch für Lua, aber durchaus lösbar. Wir finden, es funktioniert ziemlich gut! 🙂
Andy Friesen ist technischer Leiter des Luau-Typ-Checkers. Er freut sich darauf, an der Schnittstelle zwischen Videospielen, Entwicklertools und Programmiersprachen zu arbeiten.
Weder die Roblox Corporation noch dieser Blog befürworten oder unterstützen irgendein Unternehmen oder einen Dienst. Außerdem werden keine Garantien oder Zusagen hinsichtlich der Genauigkeit, Zuverlässigkeit oder Vollständigkeit der in diesem Blog enthaltenen Informationen gegeben.


