[Sprachentwurf] fun pl
Verfasst: 21.06.2012, 19:31
Da im Nachbar-Thread Interesse am Entwurf der Sprache angemeldet wurde, die ich vor einem halben Jahr zusammen mit jokester aus dem sppro.de-Forum nebenan angedacht hatte, mache ich mit diesem Post mal einen Anfang, diesen zu skizzieren. Für die Sprache existiert soweit nur ein unfertiger Parser, und es ist fraglich, ob je mehr daraus wird.
Folgende Punkte waren uns besonders wichtig:
Hier wird bereits deutlich, wie Wert- und Typ-Kontext strikt getrennt sind. Die Trennung von def und var hat den Hintergedanken, dass die Definition von Konstanten mit def in keiner Weise umständlicher als die Definition von Variablen gemacht wird, und so nach Möglichkeit zum Standardfall wird.
Funktionen
Im Sinne der einfachen Übersetzbarkeit ist es auch, dass Funktionen ein eigenes Schlüsselwort erhalten, welches sowohl zur Definition als auch als Typkonstruktor dient.
Die vielen Varianten sehen zunächst etwas einschüchternd aus, können aber zum Großteil schon bei der Konstruktion des AST dank der expliziten Syntax sehr einfach behandelt werden. Die stark verbesserte Lesbarkeit ist es in meinen Augen auf jeden Fall wert.
Zur Definition von Funktionstypen nutzen wir ganz intuitiv fun als Typkonstruktor:
Soweit haben wir den funktionalen Teil, Zeit für etwas Objektorientierung.
Klassen und Objekte
Da unsere Klassen im Kern nur Daten enthalten, haben wir uns für das C-Keyword struct entschieden:
Um konsistent zu bleiben, haben wir Konstruktoren durch Placement-Funktionen ersetzt. Das + macht this zum (zunächst nicht konstruierten!) Ergebnisobjekt und erzwingt die Einhaltung zusätzlicher Bedingungen des Konstruktionsablaufs.
Auch Move-Semantics haben wir angedacht. Der folgende Konstruktor ersetzt den vorangegangen und deckt analog zu C++ Konstruktion durch Kopie und Verschiebung ab, indem er die Kopie, sofern erforderlich, implizit in den Aufruferkontext verlagert:
Übrigens erlaubt diese Syntax beliebige Konstruktornamen, womit auch lästige statische Delegationsmethoden zur Benennung von Konstruktoren entfallen. Ein großer Teil der Konstruktoren (Kopie, Movement, Initialisierungsliste?) sollte auf jeden Fall automatisch generiert werden, die Details erscheinen mir hier aber gerade weniger interessant.
Der Vollständigkeit halber noch die Destruktion, die aber logischerweise genau wie in C++ fast immer ohne expliziten Destruktor korrekt laufen sollte:
Die Manipulation von Objekten durch Funktionen (aka Methoden) sähe wie folgt aus:
Der Template-Instantiierungsoperator entstand aus der Not heraus, weil wir keinen vernünftigen Weg sahen, spitze Klammern von den entsprechenden Operatoren zu trennen. Ich bin mir noch nicht sicher, ob er in dieser Form brauchbar wäre. Eventuell wäre es der Sprache auch zuträglich, wenn einige Operatorsymbole durch Text ersetzt würden:
So, mir geht schon die Puste aus, ich mache ein anderes Mal weiter. Wir hatten damals bei weitem noch nicht alles durchdacht, und doch kriege ich nur die grobe Skizze eines Bruchteils dessen in diesem Post unter. Auch das zeigt, wie verflucht viel Arbeit so eine Sprache schlussendlich ist, noch bevor sie überhaupt implementiert wird. ;)
Folgende Punkte waren uns besonders wichtig:
- Die Vereinheitlichung von Methoden und freien Funktionen: Die Verankerung von Methoden in Klassen erschien uns überflüssig; sie führt offensichtlich zu Inkonsistenz beim Aufruf, wenn fremde Module zusätzliche Operationen auf Objekten implementieren. Diese könnten zwar umständlich nachträglich in die Klasse injiziert werden, dies entspricht jedoch in keinster Weise der Kapselung als Grundidee hinter Klassen. Tatsächlich erscheint die traditionelle Umsetzung von Klassen aus meiner heutigen Sicht kein gutes Mittel der Kapselung, weil sie vorschreibt, welche Operationen aus Implementierungsgründen objekteigen sind, und welche sich alleine auf der Objektschnittstelle implementieren lassen.
- Ein nicht textbasiertes Modulsystem: Hierzu muss ich vermutlich nicht viel sagen, dass Text-#includes suboptimal bis nicht zu gebrauchen sind, hat wohl jeder hier zur Genüge selbst erfahren.
- Verbesserte Templates: Templates in C++ sind umständlich zu schreiben und erlauben ohne vollständige Instantiierung nur eine sehr eingeschränkte statische Prüfung. Concepts retten uns hoffentlich mit dem nächsten C++-Standard aus dieser misslichen Lage.
- Einfache Übersetzbarkeit: Die Grammatik haben wir nie formalisiert, sie existiert nur implizit im Parser-Prototyp. Das geht, weil zu jedem Zeitpunkt klar ist, welches Token folgen muss. Damit einher geht auch der nächste Punkt ...
- Trennung von Typ- und Werte-Kontexten: Um im Rahmen eines bequemen Modulsystems ohne jegliches Vorwissen durch Deklarationen vollständig übersetzen zu können, ist an jeder Stelle klar, ob ein Typ oder ein Wert gemeint ist. Damit erübrigen sich auch Mehrdeutigkeiten bei gleichnamigem Objekt und Objekttyp.
- Deterministische logische Objektkonstruktion und -destruktion: RAII funktioniert in C++ so gut, dass wir dem erstmal nichts hinzuzufügen hatten.
- Ausnahmebehandlung ebenso, leicht ergänzt.
Code: Alles auswählen
def a = 2 // Konstante vom Typ int
def b : int = 2 // Konstante vom Typ int mit expliziter Typangabe
var c : string // String-Variable
var d : float = uninitialized // Uninitialisierte float-Variable
d = b -> float // b zu float gecastet
Funktionen
Im Sinne der einfachen Übersetzbarkeit ist es auch, dass Funktionen ein eigenes Schlüsselwort erhalten, welches sowohl zur Definition als auch als Typkonstruktor dient.
Code: Alles auswählen
// Volle Typisierung. Parametervariablen sind standardmäßig konstant.
fun double(n : int) -> int
{
return 2 * n;
}
// Automatische Bestimmung des Rückgabetyps ist in der Regel trivial, also Rückgabetyp optional.
fun double(n : int)
{
return 2 * n;
}
// Wenn uns der Typ nicht interessiert, lassen wir ihn weg, und erhalten ein implizites Template.
fun double(n)
{
return 2 * n;
}
Code: Alles auswählen
// Kurze Funktionen lassen sich schöner schreiben.
fun double(n) = 2 * n
// Natürlich auch gültig:
fun double(n : int) = 2 * n
fun double(n : int) -> int = 2 * n
Zur Definition von Funktionstypen nutzen wir ganz intuitiv fun als Typkonstruktor:
Code: Alles auswählen
fun updateProgressBar(text : string, progress : double) { ... }
def callback = updateProgressBar
// callback hat Typ fun (string, double) bzw. fun (string, double) -> void
Klassen und Objekte
Da unsere Klassen im Kern nur Daten enthalten, haben wir uns für das C-Keyword struct entschieden:
Code: Alles auswählen
struct name
{
first : string // Implizit: def, auch var möglich.
last : string
}
Code: Alles auswählen
fun +name(first : string, last : string) -> name
{
// Konstruktion, keine Zuweisung, deshalb Reihenfolge erzwungen.
// Lokale Variablen an jeder Stelle möglich, Manipulation von Attributen nach erster Konstruktion derselben.
this.first = first;
this.last = last;
}
Code: Alles auswählen
// '+' vor Parameter erzwingt Kopie statt konstanter Referenz (by Value, in C++ also string statt const string &)
fun +name(+first : string, +last : string) -> name
{
// ~ analog zu std::move() in Anlehnung an die Destruktorsyntax, weil first und last danach höchst wahrscheinlich ungültig sein werden.
this.first = ~first;
this.last = ~last;
}
Der Vollständigkeit halber noch die Destruktion, die aber logischerweise genau wie in C++ fast immer ohne expliziten Destruktor korrekt laufen sollte:
Code: Alles auswählen
fun ~name()
{
// Tue nichts, Destruktion der Attribute läuft automatisch.
// Dieser Destruktor würde natürlich in einem echten Programm gar nicht erst explizit definiert.
}
Code: Alles auswählen
// & macht v zu manipulierbarer Referenz.
// Instantiierung von vector Template durch Instantiierungsoperator: Template => Template-Argumente
// Implizites Template, Syntax Java-inspiriert: ? leitet inplace die Definition eines Typparameters ein, der im Anschluss verwendet werden darf.
fun append(&v : vector => ?E, e : E)
{
// Verschiebe e ans Ende von v
}
// Benutzung:
var v : vector => int
append(v, 2)
Code: Alles auswählen
fun append(ref v : vector => typename E, copy e : E)
{
// Verschiebe e ans Ende von v
}