Dieser Artikel beschreibt ein altes, nicht mehr existierendes Feature und ist in keinster Weise für das Gameplay relevant.

Die aktuelle Version der Spezifikation befindet sich direkt im Git Repository. [1]

Diese Seite dient als Übersicht über alle Features und Einzelheiten von Q3 und ist nicht als Schritt-für-Schritt Anleitung gedacht.

Datentypen und Konstanten[Bearbeiten | Quelltext bearbeiten]

Jeder Wert ist ein Objekt und hat somit Membervariablen und Membermethoden. Es wird jedoch unterschieden zwischen primitiven Datentypen und Klassen. Erstere sind bereits vorgegeben und gehen aus der Syntax hervor. Klassen hingegen werden durch Libraries der World oder durch den Programmierer definiert.

Integer
Eine vorzeichenbehaftete ganze Zahl mit 64 Bit Länge. Es existiert derzeit nur die Notation im Dezimalsystem. Beispiele sind 1, 512 oder -17.
Float
Eine Gleitkommazahl mit 64 Bit Präzision. Um als solche interpretiert zu werden, muss sie einen Punkt enthalten. Beispiele sind 1.0, 3.1415926, -5. oder -0.123.
Boolean
Ein Wahrheitswert, der entweder wahr oder falsch sein kann. true ist wahr, false, null und _ sind äquivalent und sind falsch.
String
Eine Zeichenkette mit beliebig vielen Zeichen. Wird von zwei doppelten Anführungszeichen begrenzt und kann sich über mehrere Zeilen erstrecken. Beispiel: "Hello World!"
Tuple
Eine unveränderbare, geordnete Liste von Werten. Beispiele sind [], [true, false,] oder [1, 2, "abc", [3, 4,]]
Variable
Zeigt auf eine Speicherstelle.

Zu den restlichen Datentypen zählen Objekte, Funktionen, Klassen, Enums und Enumwerte. Außerdem können von der World neue Datentypen definiert werden.

Aufbau einer Datei[Bearbeiten | Quelltext bearbeiten]

In einer Q3 Datei finden sich 0 bis n Deklarationen von Funktionen, Klassen oder Enums. Falls es eine Hauptfunktion gibt, heißt diese konventionellerweise new und kann beliebig viele Parameter akzeptieren. Eine leere Datei ist eine gültige Q3 Datei.

Beispiel:

$new(x) { # Hauptfunktion
  println(square(x));
}

$square(number) -> number * number; # Kurze Schreibweise für Funktionen

enum Color { # Enumdeklaration
  RED, GREEN, BLUE,
}

class SomeClass { # Klassendeklaration

}

Kommentare[Bearbeiten | Quelltext bearbeiten]

Es werden einzeilige Kommentare unterstützt, die mit # gekennzeichnet werden. Alle Zeichen nach # zählen bis zum nächsten Zeilenumbruch als Kommentar.

Beispiel:

$new() {
# Kommentar
  return 123; # Anderer # Kommentar
}

Einsamer Beistrich[Bearbeiten | Quelltext bearbeiten]

An das Ende von Aufzählungen mit Beistrichen kann ein zusätzlicher Beistrich geschrieben werden, was semantisch keinerlei Auswirkungen hat.

Beispiel:

@list = List(
  "red",
  "green",
  "blue", # In anderen Programmiersprachen wäre so ein Beistrich nicht erlaubt. Jedoch sieht dadurch der Code "symmetrischer" aus.
);

Funktionen[Bearbeiten | Quelltext bearbeiten]

Haupterkennungsmerkmal für eine Funktionsdeklaration in Q3 ist $. Es folgen der Name der Funktion und eine optionale Liste mit 0 bis n Parametern. Danach folgen 0 bis n Stringliterale, die die Dokumentation der Funktion darstellen. Nun kommt entweder -> gefolgt von einem Statement und einem Semikolon oder ein Block. Die erste Variante gibt den Wert des Statements zurück, die zweite true, falls nicht explizit im Block mit return ein Wert zurückgegeben wird. Folglich hat in Q3 jede Funktion einen Rückgabewert, Void-Funktionen wie in C oder Java gibt es nicht.

Beispiel:

$one -> 1; # Funktion ohne Parameter, die 1 zurückgibt.

$foo(a, b, c) { # Funktion mit 3 Parametern.
  a += b;
  return a * c;
}

$hw()
"Diese Funktion printet \"Hello World!\"."
"Zweite Zeile der Dokumentation." # Dokumentation einer Funktion.
{
  println("Hello World!");
}

Dynamische Anzahl an Argumenten[Bearbeiten | Quelltext bearbeiten]

Steht am Ende einer Parameterliste mit n Parametern einer Funktionsdeklaration ... oder :, so können an die Funktion mindestens n - 1 Argumente übergeben werden. Bei einer Funktion mit ... werden die zusätzlichen Argumente in ein Tuple gespeichert und an den letzten Parameter übergeben. Eine Funktion mit : verhält sich ähnlich zur vorherigen Variante, nur werden nur Zweiertupel als Argumente akzeptiert. Ist das erste Element des Tupels ein Stringliteral oder eine andere Konstante, kann auch eine Kurznotation mit der Zeichenkette ohne Anführungszeichen, einem Doppelpunkt gefolgt vom zweiten Wert des Tupels verwendet werden. Diese zusätzlichen Argumente werden als Map gespeichert, wobei die ersten Elemente der Tupels die Keys und die zweiten die Values bilden, und an den letzten Parameter übergeben.

Beispiel:

$new() {
  foo("leer");
  foo("eins", 1);
  foo("mehr", "abc", "def");
  bar();
  bar(0: "abc", 1: "def");
  bar(abc: true, ["def", false]);
}

$foo(name, rest ...) {
  println(name + " " + rest);
}

$bar(rest :) {
  println(rest);
}

gibt folgendes aus:

leer []
eins [1]
mehr [abc, def]
[]
[0: abc, 1: def]
[def: 0, abc: 1]

Variablen[Bearbeiten | Quelltext bearbeiten]

Lokale Variablen[Bearbeiten | Quelltext bearbeiten]

Lokale Variablen werden mit @ deklariert und mit ihrem Namen referenziert. Lokale Variablen werden standardmäßig mit null initialisiert und können nach ihrer Deklaration über die restliche Funktion hinweg verwendet werden. Funktionsparameter zählen ebenfalls lokale Variablen und werden beim Aufruf der Funktion mit den an sie übergebenen Argumenten initialisiert.

Beispiel:

$new(a, b) {
  @c = a + b;
  c *= 2;
  @d = c - a;
  println("d = " + d);
  return c;
}

Referenzen[Bearbeiten | Quelltext bearbeiten]

Da Variablen auch Datentypen sind, kann in einer Variable eine andere Variable gespeichert werden. Analog zu Pointern in C wird mit & eine Variable referenziert und mit * dereferenziert.

Beispiel:

$new() {
  @a = 1;
  @b = &a; # In b wird eine Referenz auf a gespeichert.
  *b = 2; # Ändert den Wert von a.
  b = 3; # Ändert den Wert von b.
  println(a + " " + b); # Gibt "2 3" aus.
}

Referenziert werden können alle Variablen, also lokale Variablen, lokale Variablen von anderen Funktionsaufrufen oder Tasks, Membervariablen- und Funktionen und Werte aus Loadern. Wird auf eine ungültige lokale Variable schreibend zugegriffen, tritt direkt oder indirekt der Fehler STACK_CORRUPTION auf, was in folgendem Codebeispiel der Fall ist:

$new() {
  @ref = getRef(1);
  1 + setRef(ref);
  proc.logger.println(*ref);
}

$getRef(a) {
  return &a;
}

$setRef(ref) {
  *ref = 2;
}

Hier wird zuerst ein Unterfunktionsaufruf getätigt, der eine Referenz auf eine Variable, nämlich einen Parameter, zurückgibt. Anschließend wird in einem weiteren Unterfunktionsaufruf diese nicht mehr existierende Variable manipuliert. Da durch + 1 ein zusätzliches Element auf den Stack pusht, steht an der ursprünglichen Stelle des Parameters nun die Returnadresse, die überschrieben wird. Bei Beendigung des Unterfunktionsaufrufs wird die ungültige Returnadresse erkannt und ein Fehler ausgegeben. Außerdem sind lokale Variablen von beendeten Tasks ungültig.

Werden undeklarierte Variablen referenziert, wird eine undeklarierte Variable zurückgegeben, die einen Fehler bei Zugriffen erzeugt.

$new() {
  @nonexisting = *xyz;
  println(isset *nonexisting); # false
  println(nonexisting == null); # false
  println(nonexisting); # "VAR_UNDEFINED"
  println(*nonexisting); # Fehler: VARIABLE_UNDECLARED
  *nonexisting = 1; # Fehler: SET_UNDECLARED
}

Isset[Bearbeiten | Quelltext bearbeiten]

Das Keyword isset prüft, ob eine Variable existiert und evaluiert zu einem Boolean.

Beispiel:

$new() {
  @capitals = {Austria: "Viena", Germany: "Berlin", Italy: "Rome"};
  println(isset capitals); # true
  println(isset capitals["Austria"]); # true
  println(isset capitals["France"]); # false
  println(isset xyz); # false
}

Backslash-Operator[Bearbeiten | Quelltext bearbeiten]

Mit einem Backslash kann auf eine Variable über einen dynamischen Namen zugegriffen werden. Die Expression nach dem Backslash muss dabei zum Namen der Zielvariable evaluieren.

$new() {
  @var = 123;
  @name = "var";
  println(\name); # 123
  @ref = &\name;
  *ref = 321;
  println(var); # 321
}

Kontrollstrukturen[Bearbeiten | Quelltext bearbeiten]

Diese Strukturen beeinflussen den Kontrollfluss innerhalb einer Funktion, indem durch Bedingungen der weitere Verlauf des Programms beeinflusst wird. Eine Bedingung ist wahr, wenn sie nicht zu null evaluiert.

If-Abfrage[Bearbeiten | Quelltext bearbeiten]

Eine If-Abfrage führt abhängig vom Wahrheitswert ihrer Bedingung unterschiedlichen Code aus und kann innerhalb von Blöcken verwendet werden. Nach dem Keyword if folgt eine Expression in runden Klammern. Anschließend kommt entweder ein Statement gefolgt von einem Semikolon oder ein Block. Optional können das Keyword else und ein Statement gefolgt von einem Semikolon oder ein Block folgen. Ist die If-Bedingung wahr, wird der Then-Branch, also der Code direkt nach der Bedingung ausgeführt, andernfalls der Else-Branch, falls vorhanden.

Beispiel:

$checkPW(pw) {
  if(pw == "secret123")
    return true;
  else {
    println("Wrong password.");
  }
  return false;
}

$initializeIfNull(var) {
  if(!(*var)) {
    *var = List();
  }
}

While-Schleife[Bearbeiten | Quelltext bearbeiten]

Eine While-Schleife besteht aus einer Bedingung und optionalem Body, der ausgeführt werden soll. Bei der Ausführung einer While-Schleife wird wie folgt vorgegangen:

  1. Bedingung prüfen.
    Bedingung wahr: Gehe zu 2.
    Bedingung falsch: Verlasse Schleife.
  2. Body ausführen.
  3. Gehe zu 1.

Beispiel:

$new() {
  @line;
  while(line = readln()) { # Solange readln() nicht null zurückgibt.
    # Body
    println("Eingabe: " + line);
  }
}

Do-While-Schleife[Bearbeiten | Quelltext bearbeiten]

Eine Do-While-Schleife ist etwas anders aufgebaut als eine While-Schleife. Zuerst wird der Body ausgeführt und erst dann wird die Bedingung geprüft und entschieden, ob der Body erneut ausgeführt wird.

  1. Body ausführen.
  2. Bedingung prüfen.
    Bedingung wahr: Gehe zu 1.
    Bedingung falsch: Verlasse Schleife.

Beispiel:

$new() {
  @pw;
  do {
    println("Passwort eingeben:");
  while((pw = readln()) != "secret123")
  println("Passwort korrekt.");
}

For-Schleife[Bearbeiten | Quelltext bearbeiten]

Eine For-Schleife besteht aus einem Initializer, einer Condition, einem Incrementer und einem optionalen Body. Nach Initializer und Condition folgt jeweils ein Semikolon. Im Initializer wird in der Regel eine Variable initialisiert und im Incrementer verändert, wobei eigentlich jedes Statement an diesen Stellen stehen kann. Eine For-Schleife läuft folgendermaßen ab:

  1. Initializer ausführen.
  2. Condition prüfen.
    Condition wahr: Weiter gehen.
    Condition falsch: Schleife verlassen.
  3. Body ausführen.
  4. Incrementer ausführen.
  5. Gehe zu 2.

Beispiel:

$new() { # Gibt die Zahlen von 0 bis 9 aus.
  for(@i = 0; i < 10; i++) {
    println(i);
  }
}

$fillDescending(list) {
  for(@i = 10; i >= 1; list[] = i--);  # Befüllt eine Liste mit 10, 9, 8, ..., 2, 1.
}

Foreach-Schleife[Bearbeiten | Quelltext bearbeiten]

Eine Foreach-Schleife iteriert über alle Elemente einer Liste oder eines Tupels. Die Syntax ist analog zu der in Java.

Beispiel:

$new() {
  @colors = ["red", "green", "blue"];
  for(color: colors) { # Gibt alle Elemente im Tupel colors der Reihe nach aus.
    println(color);
  }
}

Vom Compiler wird eine Foreach-Schleife vorher in eine For-Schleife umgewandelt. Der obige Code würde in diese For-Schleife umgewandelt werden:

$new() {
  @colors = ["red", "green", "blue"];
  @\"$1iterator" = colors.iterator();
  while(\"$1iterator".hasNext()) {
    @color = \"$1iterator".next();
    println(color);
  }
}

Wie aus diesem Code ersichtlich wird, muss die übergebene Collection die Methode iterator() besitzen, die ein Objekt mit den Methoden hasNext() und next() zurückgibt.

Außerdem funktionieren Foreach-Schleifen auch mit Tuple-Assignment. Dabei steht an der Stelle des Variablennamens ein Tuple und die Methode next() des entsprechenden Iterators muss ein Tuple zurückgeben, wobei beide Tuples die gleiche Arität besitzen müssen. So kann man beispielsweise bequem über Maps iterieren:

$new() {
  @map = Map(red: 1, green: 2, blue: 3);
  for([@color, @number]: map) {
    println(color + " - " + number);
  }
}

Dieser Code liefert dann folgende Ausgabe:

red - 1
green - 2
blue - 3

Break und Continue[Bearbeiten | Quelltext bearbeiten]

Das Keyword break bricht die weitere Ausführung der Schleife, in der es sich befindet, ab. Der Kontrollfluss springt also direkt aus der Schleife hinaus, ohne dass der weitere Body ausgeführt wird. continue hingegen bricht die Ausführung des Bodies ab und springt bei While- und Do-While-Schleifen zur Condition und bei For-Schleifen zum Incrementer. Bei mehreren ineinander verschachtelten Schleifen wird die innerste Schleife, in der sich das jeweilige Keyword befindet, gesteuert.

Beispiel:

$readNumbers(amount) {
  @list = List();
  while(amount) {
    @number;
    if(!toInt(readln(), &number)) {
      println("Ungültige Eingabe!");
      continue;
    }
    if(!number) {
      break;
    }
    list.append(number);
    amount--;
  }
}

Dieses Programm liest amount Zahlen in eine Liste ein oder bricht ab, sobald 0 eingelesen wird.

Operatoren[Bearbeiten | Quelltext bearbeiten]

Die Operatoren und Operator-Precedence von Q3 ähneln denen anderer C- und Java-ähnlicher Programmiersprachen. In der folgenden Tabelle werden die am stärksten bindenden Operatoren zuerst angeführt.

Operatoren Assoziativität Erklärung
@ \ links Variablenzugriffe.
() . [] class links Funktionsaufrufe, Memberzugriffe, Arrayzugriffe und Inlineklassendeklarationen.
++ -- links Suffix-Operatoren.
++ -- ! ~ & * - + / rechts Prefix-Operatoren.
wait int float isset key poke sleep unwrap nf typeof fork links Prefix-Keywords.
instanceof !instanceof links Prüft, ob ein Objekt eine Instanz einer Klasse ist.
* / % links Multiplikation, Division und Modulo.
+ - links Addition und Subtraktion.
<< >> links Bitshift.
^ links Bitwise XOR.
& links Bitwise Und.
|
links Bitwise Oder.
<= < => > Arithmetischer Vergleich.
== != Prüfung auf Gleichheit bzw. Ungleichheit. (Logisches XNOR und XOR.)
&& links Logisches Und.
||
links Logisches Oder.
? : rechts Ternärer Operator.
$ links Lambda-Expressions.
= += -= *= /= %= <<= >>= |= &= ^=
rechts Zuweisungen. Die linke Seite muss zu einer Variable evaluieren, die rechte zum zugewiesenen Wert. Die Zuweisung evaluiert zum neuen Wert der Variable.
, - Trennung von Argumenten.
() [] {} - Klammern.

Arithmetische Operatoren[Bearbeiten | Quelltext bearbeiten]

Binäres +
Ist mindestens einer der Operanden ein String, werden zwei Zeichenketten aneinandergehängt. Ist mindestens einer der Operanden eine Gleitkommazahl, wird eine Gleichtkommaaddition durchgeführt. Ansonsten werden zwei ganze Zahlen addiert.
Binäres -
Ist mindestens einer der Operanden eine Gleitkommazahl, wird eine Gleitkommasubtraktion durchgeführt. Ansonsten werden zwei Ganzzahlen subtrahiert.
Binäres *
Ist mindestens einer der Operanden eine Gleitkommazahl, wird eine Gleitkommamultiplikatkion durchgeführt. Ansonsten werden zwei Ganzzahlen multipliziert.
Binäres /
Ist mindestens einer der Operanden eine Gleitkommazahl, wird eine Gleitkommadivision durchgeführt. Ansonsten werden zwei Ganzzahlen dividiert. Das Ergebnis hat im Gegensatz zu Java oder C dasselbe Vorzeichen wie der Divisor.
Binäres %
Berechnet den Rest bei der Division der linken Seite durch die rechte Seite. Ist mindestens einer der Operanden eine Gleitkommazahl, ist das Ergebnis ebenfalls eine Gleitkommazahl. Andernfalls ist das Ergebnis eine Ganzzahl. Das Ergebnis hat im Gegensatz zu Java oder C dasselbe Vorzeichen wie der Divisor.
Unäres Prefix +
Berechnet den Betrag einer Zahl, sodass das Ergebnis immer ein positives Vorzeichen hat. Eine Ganzzahl bleibt dabei eine Ganzzahl und eine Gleitkommazahl bleibt eine Gleitkommazahl.
Unäres Prefix -
Berechnet das addivite Inverse einer Zahl. In anderen Worten wird das Vorzeichen gewechselt. Eine Ganzzahl bleibt dabei eine Ganzzahl und eine Gleitkommazahl bleibt eine Gleitkommazahl.
Unäres Prefix /
Berechnet das multiplikative Inverse einer Zahl p, wobei das Ergebnis q immer eine Gleitkommazahl ist. Es gilt p * q = 1.

Vergleichsoperatoren[Bearbeiten | Quelltext bearbeiten]

Binäres ==
Prüft zwei Werte auf Gleichheit. Bei Objekten werden, wenn nicht anders spezifiziert, deren Referenzen verglichen.
Binäres !=
Prüft zwei Werte auf Unleichheit. Bei Objekten werden, wenn nicht anders spezifiziert, deren Referenzen verglichen.
Binäres <
Ist wahr, wenn die linke Zahl kleiner als die rechte ist.
Binäres <=
Ist wahr, wenn die linke Zahl kleiner als die rechte ist oder beide gleich sind.
Binäres >
Ist wahr, wenn die linke Zahl größer als die rechte ist.
Binäres >=
Ist wahr, wenn die linke Zahl größer als die rechte ist oder beide gleich sind.

Logische Operatoren[Bearbeiten | Quelltext bearbeiten]

Unäres Prefix !
Negiert einen Wert. Ist ein Wert nicht falsch, so ist das Ergebnis false, andernfalls true.
Binäres &&
Logisches Und. Ist genau dann wahr, wenn alle Operanden nicht falsch sind. Evaluiert zum Ergebnis des letzten Operanden, falls alle Operanden nicht falsch sind. Ansonsten evaluiert es zu false. Sobald ein Operand falsch ist, werden alle nachfolgenden Operanden nicht mehr evaluiert. (Short ciruit evaluation)
Binäres ||
Logisches Oder. Ist genau dann wahr, wenn zumindest ein Operand nicht falsch ist. Evaluiert zum Ergebnis des ersten Operanden, der nicht falsch ist, ansonsten zu false. Sobald ein Operand nicht falsch ist, werden alle nachfolgenden Operanden nicht mehr evaluiert. (Short ciruit evaluation)

Inkrementer und Dekrementer[Bearbeiten | Quelltext bearbeiten]

Unäres Prefix ++ und --
Erhöht bzw. dekrementiert den Wert einer Variable um 1 und evaluiert zum Wert der Variable nach der Operation.
Unäres Postfix ++ und --
Erhöht bzw. dekrementiert den Wert einer Variable um 1 und evaluiert zum Wert der Variable vor der Operation.

Bitwise-Operatoren[Bearbeiten | Quelltext bearbeiten]

Unäres Prefix ~
Invertiert die Bits einer ganzen Zahl. Evaluiert zu einer ganzen Zahl.
Binäres &
Führt eine Bitwise-Und-Operation für zwei ganze Zahlen durch. Evaluiert zu einer ganzen Zahl.
Binäres |
Führt eine Bitwise-Oder-Operation für zwei ganze Zahlen durch. Evaluiert zu einer ganzen Zahl.
Binäres ^
Führt eine Bitwise-Xor-Operation für zwei ganze Zahlen durch. Evaluiert zu einer ganzen Zahl.
Binäres <<
a << b schiebt die Bits der Zahl a um b Stellen nach links und füllt die freien Stellen mit Nullen auf. Evaluiert zu einer ganzen Zahl. Ist äquivalent mit der Multiplikation von a und der Zweierpotenz von b.
Binäres >>
a >> b schiebt die Bits der Zahl a um b Stellen nach rechts und füllt die freien Stellen mit dem Wert des höchsten Bits von a auf. Evaluiert zu einer ganzen Zahl. Ist äquivalent mit der Ganzzahldivision von a durch die Zweierpotenz von b.
Binäres >>>
a >>> b schiebt die Bits der Zahl a um b Stellen nach rechts und füllt die freien Stellen mit Nullen auf. Evaluiert zu einer ganzen Zahl.

Ternärer Operator[Bearbeiten | Quelltext bearbeiten]

Mit dem ternären Operator können If-Else-Abfragen innerhalb von Expressions realisiert werden. Ist condition in condition ? positive : negative wahr, wird positive ausgewertet, ansonsten negative. Dadurch kann das Programm

$checkPw(pw) {
  if(pw == "secret") {
    println("Password correct.");
  } else {
    println("Wrong password.");
  }
}

zu

$checkPw(pw) {
  println(pw == "secret" ? "Password correct." : "Wrong password.");
}

vereinfacht werden.

Klassen[Bearbeiten | Quelltext bearbeiten]

Q3 realisiert auch grundlegende Konzepte der objektorientierten Programmierung. Eine Klasse kann innerhalb einer q3-Datei wie folgt definiert werden:

class MyClass {

}

Instanziiert wird eine Klasse, indem sie wie eine Funktion aufgerufen wird:

@instance = MyClass();

In Q3 gilt das "everything is an object"-Prinzip. Daher kann jeder Wert wie ein Objekt behandelt werden.

$new() {
  @i = 4;
  println(i.sqrt); # 2.0
  println(i instanceof Integer); # true
}

Es gibt folgende Arten von Klassen:

Superklasse der Klasse Superklasse einer Instanz dieser Klasse Beschreibung
UserClass UserObject Eine in einer q3-Datei deklarierte Klasse.
CaseClass CaseObject Eine Case Class, die zur Laufzeit deklariert wurde.
NatClass NatObject Eine von einer externen Library zur Verfügung gestellte Klasse.

Membervariablen[Bearbeiten | Quelltext bearbeiten]

Membervariablen können im Class-Body mit @ deklariert werden und können optional mit = bei der Instanziierung einen Wert zugewiesen bekommen. Werte werden standardmäßig mit null initialisiert. Eine Membervariablendeklaration wird mit einem Semikolon abgeschlossen. Die Initializer der Membervariablen werden bei Instanziierung eines Objekts dieser Klasse vor Ausführung des Konstruktor-Bodies evaluiert. Mit dem Operator . gefolgt von einem Namen kann auf die entsprechende Membervariable eines Objekts zugegriffen werden.

class Test {
  @foo = 1 + 2;
  @bar;
}

$new() {
  @t1 = Test();
  @t2 = Test();
  @t3 = null;
  println(t1 == t2); # false
  println(t1.foo); # 3
  println(t2.foo); # 3
  println(t1.bar); # 0
  println(t2.bar); # 0
  t1.foo += 2;
  t2.bar = "abc";
  println(t1.foo); # 5
  println(t2.foo); # 3
  println(t1.bar); # 0
  println(t2.bar); # abc
  println(t3.foo); # Program crashes.
}

Ebenso wie bei lokalen Variablen kann auch bei Membervariablen der Backslash-Operator angewandt werden:

class A {
  @var = 123;
}

$new() {
  @name = "var";
  println(A().\name); # 123
}

Methoden[Bearbeiten | Quelltext bearbeiten]

Analog zu Funktionen in einer q3-Datei können innerhalb des Class-Bodies Methoden definiert werden, die auf die Membervariablen und Methoden der Instanz, für die sie aufgerufen werden, Zugriff haben. Das this Keyword evaluiert zum aktuellen Methodenaufruf zugehörigen Objekt.

class Test {
  @foo;
  @bar;

  $getFoo() -> foo;
  $setFoo(foo) -> this.foo = foo;
  $getBar() {
    return bar;
  }
  $print() {
    println("foo: " + foo);
    println("bar: " + getBar());
  }
}

$new() {
  @t = Test();
  t.setFoo(5);
  println(t.getFoo()); # 5
  t.print();
  # foo: 5
  # bar: 0
}

Konstruktor[Bearbeiten | Quelltext bearbeiten]

Existiert in einer Klasse eine Methode mit dem Namen new, so wird diese unmittelbar bei der Instanziierung aufgerufen, wobei die Parameter dieser Methode an diese bei der Instanziierung übergeben werden.

class Test {
  @foo;

  $new(foo) {
    this.foo = foo;
    println("Side effect!");
  }

  $getFoo() -> foo;
}

$new() {
  @t = Test(5);
  # Side effect!
  println(getFoo()); # 5
}

Vererbung[Bearbeiten | Quelltext bearbeiten]

Mit dem Keyword extends kann eine Klasse von einer anderen Klasse abgeleitet werden. Das bedeutet, dass die abgeleitete Klasse von der Superklasse alle Eigenschaften, also Membervariablen und Methoden übernimmt, aber auch neue Membervariablen und Methoden hinzugefügt werden können. Außerdem können in der Superklasse deklarierte Methoden überschrieben werden. Die Superklasse muss entweder beim Laden der Datei mit der Klasse bereits existieren oder muss in derselben Datei bereits über der Klasse deklariert sein. Es können auch Klassen abgeleitet werden, die bereits von einer anderen Klasse abgeleitet sind. Eine UserClass kann jedoch nur von einer anderen UserClass abgeleitet werden.

Der Konstruktor der Superklasse wird aufgerufen, indem nach der Parameterliste des Konstruktors ein Doppelpunkt und eine Liste mit Argumenten, die an den Superkonstruktor übergeben werden, folgen. Der Superkonstruktor muss aufgerufen werden, wenn er in der Superklasse deklariert ist und mindestens einen Parameter besitzt.

class Superclass {
  @var;

  $new(var) {
    this.var = var;
    println("Hello World!");
  }

  $getVar() -> var;
  $doStuff() -> println("Superclass - " + getVar());
}

class Derived extends Superclass {
  $new(var) : (var * 2) { # Superconstructor call
    println("Side effect!");
  }

  $doStuff() -> println("Derived - " + getVar());
}

$new() {
  @s = Superclass(2);
  # Hello World!
  @d = Derived(2);
  # Hello World!
  # Side effect!
  s.doStuff();
  # Superclass - 2
  d.doStuff();
  # Derived - 4
}

Instanceof[Bearbeiten | Quelltext bearbeiten]

Das binäre Keyword instanceof prüft, ob die Klasse des Wertes auf der linken Seite von der auf der rechten Seite angeführten Klasse abgeleitet ist oder beide Klassen gleich sind.

class A { }
class B { } extends A
class C { } extends A

$new() {
  @a = A();
  @b = B();
  @c = C();
  println(a instanceof A); # true
  println(b instanceof A); # true
  println(c instanceof A); # true
  println(a instanceof B); # false
  println(b instanceof B); # true
  println(c instanceof B); # false
  println(a instanceof C); # false
  println(b instanceof C); # false
  println(c instanceof C); # true
}

Typeof[Bearbeiten | Quelltext bearbeiten]

Das unäre Keyword typeof gibt die Klasse des gegebenen Wertes zurück. Die zurückgegebene Klasse kann wiederum neu instanziiert werden, wie in der zweitletzten Zeile des folgenden Codebeispiels gezeigt wird.

class Test {

}

$new() {
  println(typeof Test); # UserClass
  println(typeof Test()); # Test
  println(typeof 3.0); # Natlib.Float
  println(typeof null); # Natlib.Boolean
  println((typeof Test())()); Test#41f7662a
}

Klasse als Funktion[Bearbeiten | Quelltext bearbeiten]

Eine Klassendeklaration, ihr Konstruktor und die Funktion, über die sie instanziiert wird, sind eine Einheit. Das folgende Codebeispiel erläutert dies:

class Test {
  @a;
  @b;

  $new(x, y) {
    a = x;
    b = y;
  }

  $add() -> a + b;
}

$new() {
  @t = Test;
  @instance = t(1, 2);
  @add = instance.add;
  println(instance instanceof t); # true
  println((typeof instance) == t); # true
  println(add()); # 3
}

Nested Classes[Bearbeiten | Quelltext bearbeiten]

Innerhalb des Bodies einer Klasse können weitere Klassen deklariert werden. Diese Klassen können nur mit einer Instanz der äußeren Klasse instanziiert werden und haben Zugriff auf die Membervariablen und Methoden der äußeren Klasse.

Sei Outer eine Klasse, in der die Klasse Inner deklariert wird. Nun gebe es die Instanten a und b von Outer. Dann gelten a.Inner und b.Inner als zwei unterschiedliche Klassen.

Das unäre Keyword unwrap gibt von der Instanz einer inneren Klasse die Instanz der äußeren Klasse zurück. Besitzt das Objekt kein äußeres Objekt, so wird null zurückgegeben.

class Outer {
  @o;

  $new(o) {
    this.o = o;
  }

  class Inner {
    @i;

    $new(i) {
      this.i = i;
    }

    $print() -> println("o = " + o + ", i = " + i);
  }
}

$new() {
  @o1 = Outer(1);
  @o2 = Outer(2);
  @i1 = o1.Inner(3);
  @i2 = o1.Inner(4);
  println(i1 instanceof o1.Inner); # true
  println(i1 instanceof Outer); # false
  println(i1 instanceof o2.Inner); # true
  println(unwrap i1 == o1); # true
  i2.print(); # o = 1, i = 4
}

Case Classes[Bearbeiten | Quelltext bearbeiten]

Case Classes sind Klassen, die innerhalb von Expressions deklariert werden und nur Membervariablen besitzen. Case Classes haben folgende Eigenschaften:

  • Instanzen von Case Classes werden by-value verglichen.
  • Der Konstruktor einer Case Class gleicht in seinen Parametern der Aufzählung der Membervariablen bei der Deklaration der Case Class.
  • Case Classes können von anderen Case Classes abgeleitet werden. Dabei werden die Parameter der Superkonstruktors vorne an die des Konstruktors gereiht.
  • Instanzen von Case Classes sind immutable.
  • Sei S ein Statement, in dem eine Case Class deklariert und zurückgegeben wird. Wird S zweimal ausgewertet, so werden zwei unterschiedliche Case Classes zurückgegeben.
$create() {
  @clazz = class {foo, bar};
  return clazz;
}

$new() {
  @clazz1 = create();
  @clazz2 = create();
  @instance = clazz1(1, 2);
  @derived = class {rofl} extends clazz1;
  @derivedInstance = derived(3, 4, 5);
  println(instance.foo); # 1
  println(derivedInstance.rofl); # 5
  println(instance instanceof clazz1); # true
  println(derivedInstance instanceof clazz1); # true
  println(instance instanceof clazz2); # false
  println(clazz1 instanceof CaseClass); # true
}

Case Classes können über den Konstruktor CaseClass dynamisch deklariert werden.

$new() {
  @superclazz = class; # No member variables
  @myclazz = CaseClass(superclazz, "MyClazz", ["foo", "bar"]); # (superclass, name, members)
  @instance = myclazz(1, 2);
  println(instance.bar); # 2
  println(myclazz.simplename); # MyClazz
}

Enums[Bearbeiten | Quelltext bearbeiten]

Ein Enum ist eine Sammlung von eindeutig identifizierbaren Konstanten, die logisch zusammengehören. Mit dem Keyword enum wird ein Enum gefolgt von dessen Namen deklariert. In Blockklammern werden die einzelnen Konstanten mit Beistrich getrennt aufgelistet. Das Enum kann analog zu Funktionen mit Stringliteralen vor den Blockklammern dokumentiert werden. Auf die Konstanten kann mit EnumName.CONSTANT_NAME zugegriffen werden. Analog zu Membervariablen kann auch hier der Backslash-Operator verwendet werden.

Die Membervariable values eines Enums speichert ein Tupel, das alle Elemente dieses Enums beinhaltet.

enum Color {
  RED,
  GREEN,
  BLUE,
}

$new() {
  @red = Color.RED;
  println(red); # RED
  println(typeof red); # Color
  println(red instanceof Color); # true
  println(Color.values); # [RED, GREEN, BLUE]
}

Ein Enum kann innerhalb einer Klasse deklariert werden. Dabei gilt ähnlich wie bei Nested Classes, dass die Enums und deren Konstanten zweier Instanzen einer Klasse nicht gleich sind.

class MyClass {
  enum State
  "Represents the state of this object."
  {
    SLEEPING, AWAKE, DEAD,
  }
}

$new() {
  @instance1 = MyClass();
  @instance2 = MyClass();
  println(instance1.State == instance2.State); # false
  println(instance1.State.AWAKE instanceof instance1.State); # true
  println(instance1.State.AWAKE instanceof instance2.State); # false
  println(unwrap instance1.State == instance1); # true
}

Lambda-Expressions[Bearbeiten | Quelltext bearbeiten]

Q3 erlaubt es, funktional zu programmieren. Das heißt, dass Funktionen auch Datentypen sind, die in Variablen gespeichert und übergeben werden können. Eine Lambda-Expression ist eine in einer Funktion definierte Funktion. Der Operator $ kennzeichnet immer eine Funktion.

$new() {
  @function = $ {
    println("Hello World!");
    return 1;
  }
  @retval = function(); # Hello World!
  println(retval); # 1
}

Es existieren mehrere Schreibweisen:

# No parameters
@function = $ { return "Hello World!"; };
@function = $ -> "Hello World!";
@function = $ "Hello World!";
@function = $() -> "Hello World!";
@function = $() { return "Hello World!"; };

# One parameter
@function = $(name) { return "Hello, " + name + "!"; };
@function = $name { return "Hello, " + name + "!"; };
@function = $name -> "Hello, " + name + "!";

# 0 - n parameters
@function = $(x, y) { return x + y; };
@function = $(x, y) -> x + y;

Variable Capturing[Bearbeiten | Quelltext bearbeiten]

In einer Lambda-Expression kann auf Variablen aus äußeren Funktionen zugegriffen werden und falls die äußerste Funktion eine Methode ist, auf das Objekt für das die Methode aufgerufen worden ist.

$addConstant(c) -> $x -> x + c;

$new() {
  @function = addConstant(3);
  println(function(0)); # 3
  println(function(2)); # 5
}

Die Werte der äußeren lokalen Variablen werden fixiert, sobald die Lambda-Expression zu einer Funktion evaluiert wird. Äußere Variablen können nicht von inneren Funktionen verändert werden. Ändert sich eine äußere Variable, bleibt ihr ursprünglicher Wert in inneren Funktionen erhalten.

$new() {
  @var = 3;
  @inner = $ {
    println("var is " + var);
  };
  var = 4;
  inner(); # 3
}

Gleichheit[Bearbeiten | Quelltext bearbeiten]

Zwei Funktionen sind gleich, wenn sie von derselben Lamda-Expression und demselben Objekt stammen und die äußeren Variablen gleich sind.

$addConstant2(c) -> $x -> x + c;

$new() {
  @add1 = addConstant(1);
  @add2 = addConstant(1);
  @add3 = addConstant(2);
  @add4 = addConstant2(1);
  println(add1 == add2); # true
  println(add1 == add3); # false
  println(add1 == add4); # false
}

Multitasking[Bearbeiten | Quelltext bearbeiten]

Das wichtigste Feature von Q3 ist das Ausführen von hintereinander laufenden Tasks und die Kommunikation zwischen den einzelnen Tasks. Ein Task ist dabei eine Ressource bestehend aus einer Hauptfunktion, einem Zustand und weiteren Attributen. Wird die Q3M gestartet, startet diese den Haupttask, der die Hauptfunktion der gegebenen q3x-Datei ausführt. Ein Task kann weitere Tasks starten. Der Scheduler arbeitet dabei die ausstehenden Tasks der Reihe nach ab. Blockiert ein Task, indem er schläft oder auf ein Signal wartet, wird ein anderer Task abgearbeitet. Beendet sich ein Task T, indem die Hauptfunktion abgearbeitet worden ist, T von einem anderen Task beendet worden ist oder abgestürzt ist, werden alle Tasks, die T gestartet hat, ebenso beendet. Folglich läuft die Q3M solange der Haupttask nicht beendet ist.

Sleep[Bearbeiten | Quelltext bearbeiten]

Das unäre Keyword sleep nimmt eine Ganzzahl i entgegen und hält die Ausführung des aktuellen Tasks für i Millisekunden an. Ist die Zeit abgelaufen, wird der Task an den Anfang der Scheduler-Queue gepusht. Ist i < 0, so wird der Fehler SLEEP_NEGATIVE geworfen. Schlafen zwei Tasks bis zum gleichen Zeitpunkt, wird der Task, der zuerst sleep aufgerufen hat, zuerst ausgeführt. Der darunterliegende Scheduler ist also FIFO.

$new() {
  println("Let's wait for 3 seconds...");
  sleep 3000;
  println("We're back!");
}

Fork[Bearbeiten | Quelltext bearbeiten]

Das unäre Keyword fork nimmt eine Funktion entgegen und startet einen neuen Task mit dieser Funktion als Hauptfunktion. Die Expression evaluiert dabei zum eben erstellten Task. Der Task wird an den Anfang der Scheduler-Queue gepusht. Er wird also ausgeführt, sobald der aktuelle Task blockiert.

Das folgende Codebeispiel schreibt im Intervall von 500ms Fizz und im Intervall von 300ms Buzz in die Konsole. Nach 10sek beendet sich das Programm, da die Hauptfunktion fertig ist und sich somit der Haupttask beendet.

$new() {
  fork $ {
    while(true) {
      println("Fizz");
      sleep 500;
    }
  };
  fork $ {
    while(true) {
      println("Buzz");
      sleep 300;
    }
  };
  sleep 10000;
}

Sollen Argumente an die Hauptfunktion übergeben werden, muss dies mithilfe einer Hilfsfunktion geschehen:

@task = fork $ someFunction(foo, bar);

Der Code

@task = fork someFunction(foo, bar);

würde zuerst someFunction(foo, bar) evaluieren und dann an fork übergeben, was zu einem Laufzeitfehler führen würde, sofern die Funktion nicht wieder eine Funktion zurückgibt.

Das Task-Objekt[Bearbeiten | Quelltext bearbeiten]

Das Keyword ctask evaluiert zum Task, der gerade ausgeführt wird.

Error Handler[Bearbeiten | Quelltext bearbeiten]

Wirft ein Task einen Fehler error, geschieht Folgendes:

  • Prüfen, ob die Membervariable errorHandler gesetzt ist.
    • Ja:
      • Alle Sub-Tasks beenden und ihren State auf KILLED setzen.
      • Statt der Hauptfunktion die Funktion errorHandler(error) ausführen.
      • errorHandler auf null setzen.
    • Nein:
      • Den State dieses Tasks auf CRASHED setzen.
      • Den Returnwert dieses Tasks auf error setzen.
      • error beim Task, der diesen Task gestartet hat, werfen.
$new() {
  @t = fork $ 0 / 0;
  t.errorHandler = $ex -> println("Oops, we crashed: " + ex);
  wait t;
}

Ein Fehler kann explizit mit dem Keyword throw geworfen werden, wobei jeder beliebige Wert als Fehler angegeben werden kann.

Timeout[Bearbeiten | Quelltext bearbeiten]

Die Membervariable timeout gibt an, wie lange ein Task maximal laufen kann, ohne zu blockieren. Der Wert ist eine Ganzzahl in Millisekunden. Ist timeout == 0, so ist kein Timeout gesetzt. Läuft ein Task in ein Timeout, so wirft er den Fehler TASK_TIMEOUT. Beim Erstellen eines neuen Tasks wird das Timeout des Parent-Tasks übernommen. Ändert ein Task sein eigenes Timeout, muss er zuerst blockieren, damit die Änderung wirkt.

$new() {
  @t = fork {
    while(true);
  };
  t.timeout = 1000;
  t.errorHandler = $ex -> println(ex);
  wait t;
}

State und Retval[Bearbeiten | Quelltext bearbeiten]

Die Membervariable state ist vom Typ TaskState und gibt an, in welchem Zustand sich der Task befindet. Das Enum TaskState hat dabei folgende Werte:

Wert Beschreibung
ALIVE Der Task ist gerade aktiv oder blockiert. retval ist null.
RETURNED Die Hauptfunktion des Tasks ist fertig abgearbeitet worden und ist nicht mehr aktiv. retval speichert den Returnwert der Hauptfunktion.
CRASHED Der Task hat einen Fehler direkt oder indirekt durch einen Subtask geworfen und ist nicht mehr aktiv. retval speichert den geworfenen Fehler.
KILLED Der Task T ist beendet worden, indem für T kill() aufgerufen worden war oder der Parent-Task P beendet worden war, abgestürzt war oder exec aufgerufen hatte. retval ist null.

Kill[Bearbeiten | Quelltext bearbeiten]

Ein Task kann beendet werden, indem die Methode kill() aufgerufen wird. Ist der Task bereits inaktiv, geschieht nichts. Das Verhalten eines Tasks, der für sich selber kill() aufruft, ist undefiniert.

Logger[Bearbeiten | Quelltext bearbeiten]

Die Membervariable logger kann dazu verwendet werden, den Standardlogger eines Tasks, der für print und readln benutzt wird, zu speichern. Beim Erzeugen eines Tasks übernimmt dieser den Wert von logger des Parent-Tasks.

Working Directory[Bearbeiten | Quelltext bearbeiten]

Nur innerhalb des Ordners directory und dessen Unterordner kann ein Task lesen oder schreiben. Ist directory gleich null, so ist der Task überall berechtigt. Der Task T1 kann das Working Directory von Task T2 nur zu directory verändern, wenn T1.workingDirectory == null || T1.workingDirectory == directory oder directory ein Unterordner von T1.directory ist.

Sonstige Member[Bearbeiten | Quelltext bearbeiten]

Die Membervariable timeCreated speichert als Unix-Timestamp den Zeitpunkt, zu dem der Task erstellt worden ist.

Die Membervariable cpuTime speichert in Millisekunden, wie lange der Task bereits aktiv gelaufen ist, wobei nicht die Zeit miteinberechnet wird, in der der Task blockiert hat und wie lange er gelaufen ist, seitdem er das letzte Mal blockiert hat.

Wait, Key und Poke[Bearbeiten | Quelltext bearbeiten]

Tasks können untereinander asynchron Informationen austauschen, indem sie sogenannte Keys senden oder auf einen bestimmten Key warten. Ein Key ist ein Objekt eines beliebigen Typs. Das unäre Keyword wait nimmt ein Objekt oder eine Klasse, das sogenannte Lock, entgegen und blockiert den aktuellen Task so lange, bis ein Key an ihn gesendet wird, der das gegebene Lock matcht. Die Expression evaluiert zum empfangenen Key. Wann ein Key ein Lock matcht, wird im nächsten Abschnitt beschrieben.

Mit den unären Keywords key und poke kann ein Key gesendet werden. key pusht alle Tasks, die auf einen Lock, der mit dem gegebenen Key matcht, warten, an das Ende der Scheduler-Queue und anschließend pusht sich der aktuelle Task nach den aufgeweckten Tasks ebenfalls an das Ende der Scheduler-Queue. Die Expression evaluiert zu null, wenn kein Task aufgeweckt wurde, oder sonst zu einem Lock der aufgeweckten Tasks. poke verhält sich ähnlich wie key, nur mit dem Unterschied, dass maximal ein Task aufgeweckt wird.

Diese Funktionalität ist vergleichbar mit Condition Variables oder wait, notify und notifyAll in Java.

Es kann vorkommen, dass ein Task einen Key sendet, bevor ein anderer Task auf diesen zu warten beginnt. Der letztere Task wartet also für immer auf den Key, falls er dieser nicht erneut gesendet wird. Dieses Szenario ist bei der Programmierung mit Q3 zu berücksichtigen.

Tasks als Key[Bearbeiten | Quelltext bearbeiten]

Stirbt ein Task, bzw. ändert er seinen State, wird der Task als Key gesendet. Es ist zu beachten, dass ein Task nicht als Key gesendet wird, wenn er exec aufruft oder ein Fehler geworfen wird und ein errorHandler gesetzt ist.

Keymatching[Bearbeiten | Quelltext bearbeiten]

Der Key k matcht Lock l genau dann, wenn einer der folgenden Fälle zutrifft:

  • k == l - Key und Lock sind dasselbe Objekt oder sind gleich.
  • l instanceof Class && k instanceof l - Das Lock ist eine Klasse und der Key ist eine Instanz dieser Klasse.
  • Wenn k instanceof typeof l und wenn für jede Membervariable lv des Locks und für jede dazugehörige Membervariable kv des Keys gilt:
    • lv == null - Wenn die Variable des Locks null ist.
    • lv == kv - Wenn beide Werte gleich sind.
    • lv == true && kv != null - Wenn die Variable des Locks true ist, darf die des Keys nicht null sein.
    • lv instanceof Set && kv instanceof Set && !intersect(lv, kv).empty - Wenn beide Variablen Mengen sind und die Schnittmenge dieser Mengen nicht leer ist.
    • lv instanceof Set && lv.contains(kv) - Wenn die Variable des Locks eine Menge ist und diese die Variable des Keys enthält.
    • kv instanceof Set && kv.contains(lv) - Wenn die Variable des Keys eine Menge ist und diese die Variable des Locks enthält.

Futures[Bearbeiten | Quelltext bearbeiten]

Einige in Libraries enthaltene Funktionen, Methoden und Membervariablen sind als asynchron gekennzeichnet. Wie bereits bekannt sein sollte, wird Q3 Bytecode in einem separaten Thread ausgeführt, während die World in ein oder mehreren anderen Threads läuft. Da letztere Threads Daten verwalten, die nicht gelockt sind, können diese nicht ohne Weiteres vom Q3M-Thread verwaltet werden. Die Q3M muss also, um diese Daten zu verwalten, ein Request an einen World-Thread senden, der die gewünschte Operation durchführt.

Diese Problemstellung löst Q3, indem beim Aufruf einer asynchronen Funktion, Methode oder Membervariable ein Request an den dafür zuständigen World-Thread gesendet wird und die Ausführung des aktuellen Tasks dadurch nicht blockiert. Als Rückgabewert wird eine Datenstruktur, ein sogenanntes Future, zurückgegeben, deren Wert noch nicht bekannt ist. Während der Task weiter ausgeführt wird, bearbeitet der World-Thread asynchron die Anfrage. Erst wenn der konkrete Wert des zurückgegebenen Futures benötigt wird, blockiert der aktuelle Task, bis ein konkreter Wert vorliegt.

Angenommen, es gäbe die Funktion asyncFunction(), die asynchron ist und eine Zahl zurückgibt.

$new() {
  @value = asyncFunction();
  println("Value is:");
  @sum = value + 3;
  println(sum);
}

Dieses Programm blockiert erst, wenn der konkrete Wert von @value benötigt wird, also bei der Addition von 3. Wird der Rückgabewert eines asynchronen Aufrufs nicht abgefragt, blockiert das Programm gar nicht.

Um den Task so lange zu blockieren, bis ein konkreter Wert vorliegt, kann das unäre Keyword nf verwendet werden. Würde im obigen Code @value = nf asyncFunction(); stehen, würde schon an dieser Stelle das Programm blockieren und in @value der konkrete Wert gespeichert werden.

Wenn ein Future während seiner Verarbeitung einen Fehler liefert, geschieht Folgendes:

  • Der konkrete Wert wurde bereits berechnet und ein Task greift auf das Future zu:
    • Diesen Task crashen.
  • Der World-Thread hat gerade den Wert berechnet:
    • Der konkrete Wert wurde noch von keinem Task requested: Der Task, der das Future erstellt hat, wird gecrasht.
    • Der konkrete Wert wurde von einem oder mehreren Tasks requested: All diese Tasks crashen.

Ein World-Thread ist in der Lage, einen Key synchron an die Q3M zu senden, wodurch der World-Thread so lange blockiert, bis alle Tasks, die auf diesen Key gewartet haben, abgearbeitet worden sind. Währenddessen werden alle asynchronen Funktionen dieses World-Threads synchron ausgeführt. Es werden also keine Futures zurückgegeben, sondern gleich konkrete Werte.

Ein Anwendungsgebiet von Futures liegt darin, Non-Blocking IO zu realisieren. So könnte zum Beispiel asynchron ein HTTP-Request gesendet werden, während der Task ohne zu blockieren weiterarbeiten kann und erst blockiert, wenn der Response verlangt wird und noch nicht angekommen ist.

Loader[Bearbeiten | Quelltext bearbeiten]

Ein Loader ist ein Objekt, das dazu dient, Loadervariablen, also Variablen, die weder lokale Variablen, noch Membervariablen sind, aufzulösen. Jede Funktion besitzt einen Loader. Wird auf eine Loadervariable zugegriffen, wird deren Wert im Loader der aktuellen Funktion gesucht. Wird diese dort nicht gefunden, wird rekursiv der übergeordnete Loader dieses Loaders gefragt.

Eine geladene q3x-Datei wird durch einen Loader repräsentiert, da logischerweise alle darin enthaltenen Funktionen aufeinander zugreifen können müssen. Lädt eine dieser Funktionen eine weitere q3x-Datei, wird diese als untergeordneter Loader zurückgegeben. Folglich haben die Funktionen des eben erstellten Loaders auf alle Funktionen des übergeordneten Loaders. Damit die Funktionen des übergeordneten Loaders auf die des untergeordneten zugreifen können, kann können entweder alle untergeordneten Funktionen in den übergeordneten Loader geschrieben werden oder der untergeordnete Loader kann als Namespace geladen werden.

Zum Beispiel wird in der Datei main.q3x die Datei otherFile.q3x geladen:

@parentLoader = self.loader;
[@childLoader, @error] = parentLoader.include(File("module.q3x"));
if(!childLoader) {
  println("Error loading file: " + error);
  return;
}

Es gilt, dass childLoader.loader == parentLoader, parentLoader.fullname == "main", childLoader.fullname == "main.module" und dass childLoader.simplename == "module".

Sei foo eine Funktion im neuen Loader. Auf diese Funktion kann nun folgendermaßen zugegriffen werden:

@f = childLoader["foo"];
f();
@module = childLoader.data;
module.foo();

Der []-Operator gibt also eine im Loader enthaltene Funktion zurück. childLoader.data gibt ein Objekt zurück, das die Funktionen des Loaders als Membervariablen enthält und direkt auf den Loader zugreift. Ändert sich der Loader, ändert sich auch dieses Objekt entsprechend. Dieses Objekt verhält sich wie die Instanz einer inneren Klasse des Loaders, weshalb childLoader == unwrap childLoader.data gilt.

Außerdem können in Loader Funktionen aus anderen Loadern gespeichert werden:

parentLoader.load(childLoader, "foo", "bar");
bar();

Der obige Code speichert die Funktion foo als bar im übergeordneten Loader ab. Ein Loader kann auch als Namespace geladen werden:

parentLoader.loadNamespace(childLoader, "Module");
Module.foo();

Dabei wird childLoader.data unter dem Namen Module in den übergeordneten Loader geschrieben. Mit parentLoader.remove("bar"); und parentLoader.remove("Module"); können die Funktionen wieder aus dem übergeordneten Loader gelöscht werden. parentLoader.removeAll(childLoader); entfernt alle geladenen Funktionen des untergeordneten Loaders.

Loader bieten also die Möglichkeit, Funktionen zur Laufzeit zu laden oder neu zu laden.

Darüber hinaus kann Code auch zur Laufzeit kompiliert werden.

[@loader, @error] = self.loader.compileCode("$new() { return 3; }", "FromString");
if(!loader) {
  println("Compile error: " + error);
  return;
}
println(loader["new"]); # 3

Die Funktion compileCode kompiliert einen als String gegebenen Q3-Code und gibt analog zu include einen Loader zurück. FromString ist hierbei der Name des neuen Loaders.

Günstigerweise sind auch eingebaute Libraries Loader. Es gilt also Machine.natlib instanceof Loader, wodurch eingebaute Libraries wie Loader von q3x-Dateien behandelt werden können. Zum Beispiel kann mit self.loader.load(unwrap Time, "time"); die Funktion Time.time() auch ohne den Namespace aufrufbar gemacht werden. Wie oben schon angedeutet, sind Loader hierarchisch organisiert, wodurch jeder Loader einen übergeordneten Loader besitzt. Oberster Loader in dieser Hierarchie ist der Origin Loader (Machine.origin), der keine Funktionen enthält und Natlib (Machine.natlib) als untergeordneten Loader besitzt. Der Loader der beim Starten der Q3M geladenen Datei hat den Origin Loader als übergeordneten Loader. Alle Loader von eingebauten Libraries sind Natlib untergeordnet.

Standard Library[Bearbeiten | Quelltext bearbeiten]

Listen[Bearbeiten | Quelltext bearbeiten]

Eine Liste ist eine Datenstruktur, in der Elemente beginnend mit 0 indiziert in geordneter Reihenfolge gespeichert werden. Eine Liste ist im Gegensatz zu einem Tuple mutable, wodurch Elemente ausgetauscht, entfernt oder hinzugefügt werden können. List() erzeugt eine leere Liste, während List(1, 2, 3,) eine Liste mitsamt Elementen initialisiert.

Die wichtigsten Funktionen und Membervariablen von Listen werden in der folgenden Tabelle aufgezählt:

Funktion Beispiel Beschreibung
[] list[] = 5; Fügt ein Element an das Ende einer Liste hinzu.
[.] list[0] = list[1]; Greift auf ein Element zu bzw. verändert ein Element.
size List(0, 0, 0).size == 3 Die Anzahl an Elementen in einer Liste.
empty List().empty == true Ist true, wenn die Liste leer ist.
clear() list.clear(); Setzt die Liste zu einer leeren Liste zurück.
removeAt(index) @first = list.removeAt(0); Entfernt das Element an einem gegebenen Index und gibt es zurück.
remove(element) @list = List(1, 2, 1, 3); list.remove(1); list == List(2, 1, 3) Entfernt den ersten Eintrag des gegebenen Elements aus der Liste.
contains(element) List(1, 2, 1, 3).contains(1) == true Gibt true zurück, wenn das gegebene Element mindestens einmal in der Liste vorkommt.
toList list.toList Erstellt eine Kopie dieser Liste.
toSet list.toSet Erstellt eine Kopie als Set dieser Liste.
toTuple list.toTuple Erstellt eine Kopie als Tupel dieser Liste.
iterator() list.iterator() Gibt einen frischen Iterator über diese Liste zurück.

Maps[Bearbeiten | Quelltext bearbeiten]

Eine Map ist eine Zuordnung von Values anhand von Keys. Die Wertepaare werden in ohne Reihenfolge in einer Map gespeichert. Im Prinzip kann eine Map anhand eines Wörterbuches von Sprache A zu Sprache B veranschaulicht werden. Für jedes Wort der Sprache A gibt es genau einen Eintrag und eine Übersetzung in Sprache B. Zwei unterschiedliche Wörter aus Sprache A können aber durchaus dasselbe Wort der Sprache B als Übersetzung haben. Zwei Keys a und b sind gleich, wenn a == b gilt. Eine eigene Hashcodefunktion und Äquivalenzfunktion zu implementieren, unterstützt Q3 nicht. Eine leere Map wird mit Map() initialisiert, während eine Map mit Elementen durch ein oder mehrere Zweiertupel im Konstruktur initialisiert wird. Beispiel:

@tuple = ["Chris", 512];
@score = Map("Alice" => 9000, "Bob" => 42, tuple, ["Dave", 512]);

Die wichtigsten Funktionen und Membervariablen von Maps werden in der folgenden Tabelle aufgezählt:

Funktion Beispiel Beschreibung
[.] score["Alice"]++; Setzt den Value an einem Key oder greift auf ein Element in der Map anhand des Keys zu. Existiert der Key in der Map nicht, wird null zurückgegeben.
isset [.] isset score["Eve"] Ist true, wenn der Key in der Map existiert.
size score.size Anzahl an Keys in der Map.
empty score.empty Ist true, wenn die Map leer ist.
remove(key) score.remove("Bob") Entfernt einen Key mitsamt dessen Value aus der Map und gibt letzteren zurück, falls vorhanden, andernfalls null.
clear() score.clear() Entfernt alle Key-Value-Pairs aus der Map.
entriesToList score.entriesToList == List("Alice" => 9000, "Bob" => 42, "Chris" => 512, "Dave" => 512) Erstellt eine Kopie der Map als Liste von Zweiertupeln.
entriesToSet score.entriesToSet Erstellt eine Kopie der Map als Set von Zweiertupeln.
entriesToTuple score.entriesToTuple Erstellt eine Kopie der Map als Tupel von Zweiertupeln.
keysToList score.keysToList == List("Alice", "Bob", "Chris", "Dave") Gibt die Keys der Map in einer Liste als Kopie zurück.
keysToSet score.keysToSet Gibt die Keys der Map in einem Set als Kopie zurück.
keysToTuple score.keysToTuple Gibt die Keys der Map in einem Tupel als Kopie zurück.
valuesToList score.valuesToList == List(9000, 42, 512, 512) Gibt die Values der Map in einer Liste als Kopie zurück.
valuesToSet score.valuesToSet Gibt die Values der Map in einem Set als Kopie zurück.
valuesToTuple score.valuesToTuple Gibt die Values der Map in einem Tupel als Kopie zurück.
toMap score.toMap Gibt eine Kopie dieser Map zurück.
iterator() score.iterator() Gibt einen frischen Iterator zurück, der über die Key-Value-Pairs der Map iteriert und Zweiertupel liefert.

Sets[Bearbeiten | Quelltext bearbeiten]

Ein Set ist eine Menge im klassischen Sinne, also eine ungeordnete Sammlung aus Elementen, in der jedes Element maximal einmal vorkommen kann. Ein leeres Set wird mit Set() initialisiert, während mit Set("abc", 2, 4, "abc") ein Set mit 3 Elementen initialisiert wird.

Die wichtigsten Funktionen und Membervariablen von Maps werden in der folgenden Tabelle aufgezählt:

Funktion Beispiel Beschreibung
add(element) set.add(3); Fügt ein Element zum Set hinzu. Gibt true zurück, wenn das Element vorher nicht enthalten war.
contains(element) set.contains(3); Gibt true zurück, wenn das Set das Element enthält.
remove(element) set.remove(element) Entfernt ein Element aus dem Set. Gibt true zurück, wenn das Element enthalten war.
size set.size Die Anzahl an Elementen im Set.
clear() set.clear() Entfernt alle Elemente aus dem Set.
toList set.toList Erstellt eine Kopie dieses Sets als Liste.
toSet set.toSet Erstellt eine Kopie dieses Sets.
toTuple set.toTuple Erstellt eine Kopie dieses Sets als Tupel.
iterator() set.iterator() Gibt einen frischen Iterator über die Werte dieses Sets zurück.

Iteratoren[Bearbeiten | Quelltext bearbeiten]

Ein Iterator ist eine Klasse, die die Methoden next() und hasNext() besitzt. Die Methode hasNext() gibt true zurück, wenn es noch weitere Elemente gibt. next() liefert das aktuelle Element und inkrementiert den Iterator. Als Beispiel dient die Klasse Range:

class Range {
  @start;
  @end;

  $new(start, end) {
    this.start = start;
    this.end = end;
  }

  $hasNext() -> start < end;
  $next() -> start++;
  $iterator() -> this;
}

Nun kann in einer For-Schleife über diese Range iteriert werden:

for(i: Range(0, 10)) {
  println(i);
}

Der obige Code gibt die Zahlen von 0 bis 9 aus.

Files[Bearbeiten | Quelltext bearbeiten]

Eine Instanz der Klasse File verweist auf eine Datei oder einen Ordner, der existiert oder nicht existiert. File("my-file.txt") erstellt einen Dateizeiger zur Datei "my-file.txt" relativ zum Working Directory (WD) des aktuellen Tasks. File(directory, "my-file.txt") erstellt einen Dateizeiger relativ zum angegebenen Ordner. Ist bei einem Task ein WD gesetzt, so kann dieser Task nur auf Files zugreifen, die innerhalb seines WDs liegen. Außerdem kann er anderen Tasks nur WDs zuweisen, die gleich seines eigenen WDs sind oder innerhalb davon liegen. Bei einem ungültigen Zugriff wird der Fehler ACCESS_DENIED geworfen.

Die Klasse File hat folgende Methoden und Membervariablen:

Membervariable Typ Beschreibung
hasPriviledge boolean Ist true, wenn der aktuelle Task berechtigt ist, auf dieses File zuzugreifen.
path string Der absolute Pfad dieses Files.
name string Der Name dieses Files.
exists boolean Ist true, wenn diese Datei oder dieser Ordner existiert.
time long Änderungsdatum des Files als Timestamp.
parent File Der Ordner, in der sich das File befindet.
isFile boolean Ist true, wenn das File eine existierende Datei ist.
isDirectory boolean Ist true, wenn das File ein existierender Ordner ist.
content string Inhalt der Datei. Beim Lesen oder Schreiben dieser Membervariable blockiert die komplette Q3M. Legt die Datei neu an, wenn sie nicht existiert.
clear() boolean Setzt eine existierende Datei zu einer leeren Datei zurück oder legt eine leere Datei an, falls diese noch nicht existiert. Gibt true bei Erfolg zurück.
remove() boolean Löscht die Datei oder diesen Ordner. Falls ein Ordner gelöscht wird, muss dieser leer sein. Gibt true bei Erfolg zurück.
mkdir() boolean Legt das File als Ordner an. Gibt true bei Erfolg zurück.
ls List[File] Gibt die Dateien und direkten Unterordner in diesem Ordner zurück. Gibt null zurück, wenn das File kein Ordner ist.

Machine[Bearbeiten | Quelltext bearbeiten]

Das Objekt Machine ist immutable und beinhaltet vor allem Daten, die für das Starten eines Q3 Programms relevant sind. In der Regel kümmert sich das Bootscript um die Handhabung und Verarbeitung dieser Daten.

Membervariable Typ Beschreibung
Machine.logger Object Der Logger, über den Zeichen direkt in die Konsole ausgegeben werden können und eingelesen werden können. Es ist jedoch zu empfehlen, stets den Logger des aktuellen Tasks zu verwenden.
Machine.natlib Loader Der Loader, der alle nativ definierten Funktionen enthält.
Machine.origin Loader Ein leerer Loader, von dem alle anderen Loader erben.
Machine.entry File Die Codedatei, dessen Hauptfunktion als erstes ausgeführt wurde.
Machine.directory File Der Ordner, in dem die Q3M aufgerufen wurde.
Machine.args Tuple Die Argumente, die an die Q3M übergeben wurden.
Machine.version int Die Versionsnummer der gestarteten Q3M.

Namenskonventionen[Bearbeiten | Quelltext bearbeiten]

Für Q3 gelten die auch in Java üblichen Namenskonventionen für Identifier:

Typ Konvention
Klassen, Enums, Loader ThisIsAnExample
Lokale Variablen, Membervariablen, Funktionen thisIsAnExample
Enumwerte, Konstanten THIS_IS_AN_EXAMPLE