EScript ist eine interpretierte, objektorientierte, C-Syntax-ähnliche Skriptsprache, die mit dem Fokus entwickelt wurde C++-Programme (relativ) einfach durch Skripting steuern und erweitern zu können. Dabei sollen insbesondere Objekte der Programmlogik direkt durch EScript Objekte dargestellt werden. Dafür ist das Klassen- und Objekt System von EScript deutlicher ausgeprägt als z.B. von LUA.
out("Hallo Welt!\n");
Hallo Welt!
SESpEES cEStSppc EEii iE cp itp pE iiiii irE p iiiiii p E iiiiii E p tt ri iiiii E Et iiiii pEiiSE t iS iiii E Ei iii pE p Eiip Etiii Et Ec i ipE c p p riEcSSEEES E iSEpE icpcE tti pEp cip SErpi tcp ppi iEit pp ipc rEpt icp iEEcittEE iiE pt E iE pE ii ci E pi E pE S ip S Etri p E E EEcctE SE iSEpcritrci cr ppi iEESSEEcicpptci SEiiSpEpES ipEpciSi ii ii iiirEcrri ii iEptSiiiiii iEpEEEp rEpiEEE iiiir tpE pS iiii SEEEESp irpErpEE cpc EpEtc E rStiEEt ic pE ci prpEtrpi Spt it icrEEErEE cEtE i pptppi iEcSE ipi Ei p pS E p iEr pc pci irEi rp tr p Ep p p ip i pp Et iiEi rE Eci rii pE cp cE i EEEppEr iii ipt iSEr iii tpE E iiii Sc pi r pc i iEpt itS iii rEr i irii cE tSiiiiii itEE pr i tppci itEr ii E rEEpEpttE rS i ictti pc i Epi Sp ii iEiEpi ttctSEi ppiiE i ES tp iii EE i rE pp ES ii ip p iip iEp iii E pE iiEr icp iii p Ei iiS rcc iiiiE Er i E Ep Eiiii rc ip i tE ipEp Sp iii p Ep EE iEp iii pE ppr tEpi tES ii pE tpi ipEpctcpEpp ipi EpEiiiiccrtr itppEp iptc iirttcSE pEcciii rSSS rE E ic i iES ii iiEt tE pp EEcSSSScitEirEiS pt iE ici pE prrc
ExtObjects
Dieses Dokument soll einen kurzen Überblick über die Sprache EScript liefern.
Es kann dabei leider keinen Anspruch auf Vollständigkeit erheben.
Bei Fragen oder Anregungen meldet euch bei mir.
Claudius
// Codebeispiel
Ausgabe von Codebeispielen
Syntax Grammatik
Interessant zu wissen; das braucht man aber vermutlich normalerweise nicht.
double
. Bsp. 1
, 27.4
, 0x1a
"s"
, 's'
.
Strings sind Unicode codiert.
Ein Zeichen ist ein Unicode-codepoint. true
oder false
void
true
. ACHTUNG! Der Wert 0
ist auch true
! 0
sonst.true
. true
→"true"
oder false
→"false"
. false
. Object
(auch die primitiven Typen).Collection
.Wie in C.
// Kommentar bis zum Ende der Zeile /* Ein Blockkommentar */
{...}
Grammatik: Block ::= '{' Statement* '}' Statement ::= Block
Wie in den meisten C-Syntax-ähnlichen Sprachen. Zwischen {
und }
können
Anweisungen stehen. Diese werden durch ;
getrennt.
var
Grammatik: VarDecl ::= 'var' Identifier [...]
var
deklariert eine lokale Variable innerhalb der aktuellen Funktion im umschließenden Block ( {...}
).
Die Ausnahme dieser Regel ist die Deklaration von Variablen innerhalb von Schleifenbedingungen (seit Version 0.5.1).
Dann gilt die Variable nicht im direkt umschließenden Block, sondern innerhalb der Schleife (also in etwa so wie in C).
// var-Beispiel { // in diesem Block sind a und b deklariert var a; // führt zur deklaration von a im umschließenden Block a=1; // einen Wert zu a zuweisen var b=1; // b deklarieren und einen Wert zuweisen { // in diesem Block ist ein eigenes a deklariert a=2; // dem im umschließenden Block definierten a eine 2 zuweisen b=2; var a; // führt zur deklaration von a im umschließenden Block } // am Ende des Blocks verschwindet das hier deklarierte a wieder for(var a=0;a<5;++a) // das in der Schleife deklarierte a gilt nur in der Schleife out(a," "); out("\n a:",a," b:",b,"\n"); // a und b des äußeren Blocks ausgeben }
0 1 2 3 4 a:1 b:2
Einige Dinge zur Beachtung:
var
gilt immer nur für den einen Identifier, der direkt dahinter steht.
Wenn man mehrere Variablen deklarieren möchte, braucht jede ihr eigenes var
.
static
deklariert werden.if-else
Nichts besonderes.
Wenn die Anweisung in den Klammern des if
auf true
ausgewertet wird, wird der Block ausgeführt.
Sonst wird danach oder bei einem eventuell existierenden else
weitergemacht.
Grammatik: If-Statement ::= 'if' '(' Expression ')' Statement ['else' Statement] Statement ::= If-Statement
// if-Beispiel if( true ){ out("a"); if( 1-1 == 0) out("b"); else if (2-2 == 0) out("c"); }else out("d");
ab
true
interpretiert.
Nur false
und void
werden als false
interpretiert.
Um z. B. ein Bit einer Zahl zu prüfen, braucht man daher so etwas: if( (zahl & 0x10)>0 ) ...
.
? :
Wie in C...
Grammatik: ShortIf-Expression ::= Expression '?' Expression ':' Expression Expression ::= ShortIf-Expression
// short-if-Beispiel out( 5<0 ? "klein\n" : "gross\n" ); out( true ? ( "a" + ((1-1==0) ? "b" : ((2-2==0) ? "c" : "") )) : "d" );
gross ab
||, &&, !
Wie in C...
Wenig besonderes...
Ein Funktionsaufruf wird durch eine (möglicherweise leere) Parameterliste nach einem Ausdruck, der die Funktion zurück gibt, beschrieben.
Die Parameter werden durch Kommata getrennt. Funktionen können einen Wert zurückgeben.
// Funktionsaufruf-Beispiele: out("foo"); // gib "foo" aus var time=clock();
fn
Funktionen sind in EScript ebenfalls Objekte. Sie haben von sich aus keinen Namen, sondern man muss sie Variablen zuweisen, um ihnen einen Namen zu geben. Daher sieht eine typische Funktionsdeklaration, die ein Funktionsobjekt an eine lokale Variable zuweist, so aus:
var myNewFunction = fn(x){ // Funktion mit einem Parameter return x*x; }; outln( myNewFunction ); // gib den Wert der Variable myNewFunction aus (und einen Zeilenumbruch). outln( myNewFunction(7) ); // gib den Rückgabewert des Aufrufs der Funktion myNewFunction aus.
#Function 49
ACHTUNG! Beliebter Fehler: Da es sich bei den meisten Funktionsdeklarationen eigentlich um Wertzuweisungen handelt, muss diese dann auch durch ein Semikolon ;
abgeschlossen werden!
ACHTUNG! Da Funktionen eigentlich gar keinen Namen haben, können sie auch nicht (wie z. B. in C++) durch eine andere Funktion gleichen Namens aber anderer Parameterliste überladen werden.
Wie in anderen Sprachen auch kann über das return
-Statement ein Wert zurückgegeben werden.
Der Typ des zurückgegebenen Wertes kann beliebig sein (sollte aber durch den Funktionsnamen oder den Kommentar klar werden!).
Wird kein return
verwendet, oder ein return
ohne Parameter, dann wird void
zurückgegeben.
Ähnlich wie in C++ können Standardwerte für Parameter angegeben werden. Werden dann beim Funktionsaufruf weniger Werte angegeben, als die Funktion akzeptiert, werden die fehlenden durch die Standardwerte belegt.
var f = fn( a, b=1, c=2 ){ // Parameter b und c müssen nicht angegeben werden return a+b+c; }; out( f(3,4) ); // Parameter c wird weggelassen und mit dem Standardwert 2 gefüllt. (3+4+2=9)
9
Um eine Funktion mit einer variablen Parameterliste zu definieren, muss der Parameter mit einem ...
versehen werden.
Dann werden alle Werte, die nicht den normalen Parametern zugewiesen werden, in ein Array
gespeichert und dieses Array
dann dem (Multi-)Parameter zugewiesen.
Werden keine Parameter angegeben, ist dieses Array leer.
var f = fn( x, summands... ){ // in summands wird ein Array mit allen restlichen Parameterwerten geschrieben var sum=0; foreach(summands as var v) sum += v; return x * sum; }; out( f(2,1,2,3) ); // 2 * (1+2+3) = 12
12
Bei einem Funktionsaufruf kann zur Laufzeit der Typ der übergebenen Werte überprüft werden. Im einfachen Fall wird das entsprechende gewünschte Typobjekt vor den Parameternamen geschrieben. Wird die Funktion mit einem Wert anderen Typs aufgerufen, wird ein Laufzeitfehler ausgelöst.
var f = fn(Number x){ // x muss vom Typ Number sein. return x*2; }; out( f(4) ); // Dies ist in Ordnung. out( f("blabla") ); // Die Übergabe eines String wird einen Fehler hervorrufen!
Man kann aber auch komplexere Wertebereiche angeben.
Dazu wird in Arrayschreibweise eine Liste möglicher Typen und möglicher Werte vor den Parameter geschrieben.
Beispielsweise akzeptiert die Funktion fn( [String,3,void] x){}
für den Parameter x
entweder einen beliebigen String, den Wert 3
oder den Wert void
.
Intern wird auf jedem Bedingungsobjekt die Memberfunktion _checkConstraint
aufgerufen.
Liefernt eine Bedingung true
zurück, gilt der Parameterwert als valide.
Der Parametertest in der Funktion fn( [Number,void] p1){}
entspricht also im Wesentlichen if( !Number._checkConstraint(p1)&& !void._checkConstraint(p1) ) throw new Exception("...");
Um eine Funktion rekursiv auzurufen kann man innerhalb des Funktionsrumpfs über die implizit deklarierte lokale Variable thisFn
auf das Funktionsobjekt selber zugreifen.
var fak = fn(Number x){ if(x>1) return x*thisFn(x-1); else return x; }; out( fak(4) ); // (4*(3*(2*(1))))=24
24
Um eine Memberfunktion rekursiv aufzurufen, verwendet man am besten ein entsprechendes FnBinder: (this->thisFn)(...)
Eine andere Möglichkeit für rekursive Aufrufe ist, die Funktion in einer statische Variable zu speichern, die auch in der Funktion selber zur Verfügung steht (siehe statische Variablen).
try{ ... }catch(e){...}
Die meisten Ausnahmen (inklusive einiger C++ Ausnahmen) können über try{ ... }catch(e){...}
abgefangen werden (ungefähr so wie in C++).
Dabei wird das Ausnahmeobjekt bei Auslösung an die Variable des catch
-Blocks zugewiesen.
Anders als in C++ gibt es aber immer nur genau einen catch
Block der alle Ausnahmen (unabhängig deren Typs) entgegen nimmt.
Möchte man nur bestimmte Typen von Ausnahmen fangen; muss man manuell eine Typprüfung im catch
-Block vornehmen und alle anderen Ausnahmen neu durch throw
auslösen.
try{ 1/0; }catch(e){ out("Caught: ",e); }
Caught: [#EXCEPTION "Division by zero ...
throw(...)
Mit throw
wird eine Ausnahme ausgelöst.
Der übergebene Parameter kann von einem beliebigen Typ sein und wird dann ggf. an den catch
-Block übergeben.
try{ throw( "Mein Fehler!"); }catch(e){ out("Caught: ",e); }
Caught: Mein Fehler!
[...]
Arrays lassen sich direkt durch eine Komma-getrennte Aneinanderreihung von Werten innerhalb eckiger Klammern [ ]
erzeugen.
Werden keine Werte angegeben, wird ein leeres Array erzeugt.
(Mehr zu Operationen auf Arrays im Referenzkapitel zu Collections und Arrays.)
print_r( [1,2,"foo"] ); // formatierte Augsgabe eines Arrays.
[ 1, 2, "foo" ]
{ ... : ... }
Maps lassen sich direkt durch eine Komma-getrennte Aneinanderreihung von Schlüssel-Wert-Paaren innerhalb geschwungener Klammern { }
erzeugen.
Schlüssel werden von den Werten durch einen Doppelpunkt :
getrennt.
(Mehr zu Operationen auf Maps im Referenzkapitel zu Collections und Maps.)
Werden keine Werte angegeben, wird ein leerer Anweisungsblock und keine leere Map erzeugt.
Um eine leere Map zu erzeugen, sollte new Map()
verwendet werden.
print_r( {1:"foo",2:"bar","blub":3} ); // Formatierte Ausgabe einer Map.
{ 1:"foo", 2:"bar", "blub":3 }
Collection[key]
Um auf ein Element einer Map oder eines Arrays lesend oder schreibend zuzugreifen, wird der entsprechende Schlüssel in eckigen Klammern [ key ]
angegeben.
void
zurückgegeben.void
Einträgen gefüllt.var a=["a","b","c"]; // Array anlegen var m={"foo":"bar" , 2:"blub" }; // Map anlegen m["hubhub"]=5; // in der Map einen Wert ablegen out( a[1]," ",m["hubhub"]," ",m["dibbel"] ); // einige Werte ausgeben
b 5 #void
In EScript gibt es for
, while
, do...while
und foreach
Schleifen.
Zunächst einige allgemeine Punkte zu Schleifen:
continue
innerhalb eines Schleifenrumpfs wird bis zum Ende des Schleifenrumpfs gesprungen (wie in C).break
innerhalb eines Schleifenrumpfs lässt sich die weitere Bearbeitung der Schleife abbrechen und es wird nach dem Schleifenrumpf fortgeführt (wie in C).for( init ; condition ; stepExpression) ...
Es gibt einen initialen Ausdruck, der einmalig ausgeführt wird, eine Bedingung, deren boolescher Wert über die weitere Ausführung der Schleife entscheidet und einen Ausdruck, der nach jedem Schleifendurchlauf ausgeführt wird.
for(var i=0;i<3;++i) out("bla");
blablabla
while( condition ) ...
Solange wie die Bedingung gilt, wird der Schleifenrumpf ausgeführt.
var i=0; while(i<3){ out("bla"); ++i; }
blablabla
do ... while(condition);
Es wird der Schleifenrumpf ausgeführt, dann die Bedingung geprüft, die entscheidet, ob der Schleifenrumpf erneut ausgeführt wird.
var i=0; do{ out("bla"); ++i; }while(i<3);
blablabla
foreach(.. as ..)
Foreach-Schleifen führen den Schleifenrumpf für alle Elemente einer Collection
(Array
oder Map
) aus.
Steht hinter dem as
nur ein Identifier (ggf. mit einem var
) wird in diese Variable der aktuelle Wert des Eintrags gespeichert.
var a=["a","b","c"]; foreach(a as var value) out(value);
abc
Werden zwei durch Komma getrennte Identifier angegeben, wird an den ersten der aktuelle Schlüssel (bei Arrays ist das der Index) zugewiesen und an den zweiten der Wert.
var a=["a","b","c"]; foreach(a as var key, var value) out(key,":",value," ");
0:a 1:b 2:c
Grammatik: VarDecl ::= 'static' Identifier [...]
static
deklariert eine statische Variable im umschließenden Block ( {...}
), mit den gleichen Regeln wie lokale Variablen -- jedoch mit zwei wesentlichen Unterschieden:
// static-Beispiel { var a; // führt zur Deklaration von a im umschließenden Block -- ohne Funktionen static b = 0; // führt zur Deklaration von b im umschließenden Block -- mit Funktionen static f = fn(){ if( b<10 ){ ++b; // Zugriff auf b; a ist nicht sichtbar f(); // Rekursiver Aufruf von f } }; f(); outln( f ); }
10
Einige Dinge zur Beachtung:
object.identifier
Objekte in EScript können Attribute haben.
Dies sind Variablen, die zu dem Objekt gehören und auf die über eine Namenskonstante (Identifier) zugegriffen werden kann.
Neben Attributen, die direkt in dem Objekt gespeichert sind, haben Objekte auch noch Referenzen auf Typobjekte, deren Attribute allen Instanzen des Typs zur Verfügung stehen.
So hat jedes Zahlobjekt beispielsweise eine Typreferenz auf Number
und hat daher Zugriff auf alle Attribute von Number
.
Der normale Zugriff auf Attribute erfolgt durch den Punkt-Operator (.
).
Links steht das Objekt und rechts der Name des gewünschten Attributs.
out( "foo".length,"\n); // Zugriff auf das Attribut "length" vom String "foo". // Dies ist nicht direkt im String gespeichert, sondern // im Typobjekt "String". Das Ergebnis ist eine Funktion. out( "foo".length(),"\n" ); // Ausführung dieser Funktion
#Function 3
Man kann auch über eine Stringkonstante auf ein Attribut zugreifen (z. B. obj."attrName"
entspricht obj.attrName
).
Dies ist insbesondere praktisch, wenn man auf Attribute zugreifen will, deren Name sich nicht als Identifier schreiben lässt (z. B. obj."+"
).
Die meisten Operatoren werden intern auf den Zugriff auf Attribute realisiert.
So ist 1+2
äquivalent zu 1."+"(2)
.
Oder auch: a[1] = "foo"
entspricht a._set(1,"foo")
.
ExtObjects
Verschiedene Objekte (insbesondere vom Typ ExtObject
oder auch vom Typ Type
, aber dazu später mehr...) können zur Laufzeit mit weiteren Attributen erweitert werden.
Dazu wird über den :=
-Operator ein neues Attribut definiert, welches dann direkt im Objekt gespeichert wird.
Der Aufruf erfolgt immer in Kombination mit dem Punkt-Operator: objekt.nameDesNeuenAttributs := initialerWert
.
Danach können der Wert des Attributs normal gelesen und ihm mit dem normalen Zuweisungsoperator =
neue Werte zugewiesen werden.
var e=new ExtObject(); // erzeuge eine neue Instanz von ExtObject e.newAttr:="foo"; // neues Objektattribut anlegen out(e.newAttr); // lesend auf Objektattribut zugreifen e.newAttr="bar"; // schreibend auf Objektattribut zugreifen out(e.newAttr); // lesend auf Objektattribut zugreifen e.anotherAttr=5; // Fehler: Es wird versucht, auf "anotherAttr" zuzugreifen, obwohl es nie angelegt wurde!
foobar
Auch Funktionen können als Attribute einem Objekt zugewiesen werden.
Wenn so eine Funktion dann ausgeführt wird, kann über this
auf das jeweilige Objekt zugegriffen werden.
Des Weiteren sind alle Objektattribute (und die Typattribute, dazu später mehr) direkt zugreifbar.
var e=new ExtObject(); // erzeuge eine neue Instanz von ExtObject e.number:=1; // Objektattribut hinzufügen e.doSomething:=fn(){ // eine Funktion als Attribut hinzufügen this.foo := "bar"; // ein neues Objektattribut hinzufügen number++; // direkter Zugriff auf ein Objektattribut (äquivalent zu this.number++) out( foo," ",number ); // Ausgabe }; e.doSomething();
bar 2
Typobjekte bestimmen zum einen die Eigenschaften eines jeden Objekts (z. B. welche Attribute es initial hat) und welches C++-Objekt intern verwendet wird.
Wenn man ein neues Type
-Objekt erzeugt, sollte man sich zunächst überlegen, von welchem Basistyp man erben möchte (man muss von irgendetwas erben!).
Meistens ist ExtObject
die richtige Wahl, da dann alle Instanzen auch Attribute aufnehmen können.
Ein neuer Typ wird mit new Type( Basistyp )
erzeugt.
Wenn kein Basistyp (von dem der neue Typ erbt) angegeben wird, wird ExtObject
angenommen.
var Animal=new Type; // erzeuge einen neuen Typ (erbt von ExtObject) var Rabbit=new Type(Animal); // erzeuge einen neuen Typ (erbt von Animal) var myRabbit=new Rabbit; // erzeuge eine Instanz von Rabbit
Type
-Objekte lassen sich wie ExtObject
-Objekte mit :=
um Objektattribute erweitern.
Sobald eine Instanz eines Typs mit new
erzeugt wird, wird für jedes Objektattribut des Typs ein entsprechendes Attribut im neuen Objekt zugewiesen.
var Animal=new Type; // erzeuge einen neuen Typ (erbt von ExtObject) Animal.name:="genericAnimal"; // Objektattribut "name" als String zu Animal hinzufügen var a=new Animal; // erzeuge eine Instanz von Animal out(a.name); // Objektattribut "name" von a ausgeben
genericAnimal
Bei der Zuweisung der Werte an die Instanz werden nur die Werte simpler Typen (Zahl,Bool,String) kopiert, wie bei einer normalen Zuweisung zu einer Variablen. Ist der Wert des Attributs beispielsweise ein Array, referenziert das Attribut der Instanz das gleiche Array (wie bei einer normalen Zuweisung zu einer Variablen)!
Leitet man von einen Typ ab (und erstellt damit einen neuen), werden zu dem Zeitpunkt auch alle Objektattribute des abgeleiteten Typs an den neuen weitergegeben.
Wird zu einem Typ ein neues Objektattribut hinzugefügt (oder geändert), nachdem dieser als Basistyp für einen neuen Typ eingesetzt wurde, wird diese Änderung nicht an den neuen, abgeleiteten Typ weitergegeben.
Erzeugt man einen Typ, der von einem Objekt erbt, das keine Objektattribute aufnehmen kann (z. B. man erbt direkt von Object
), dann darf der Typ keine Objektattribute enthalten.
Andernfalls wird bei der Instanziierung versucht, dem Objekt die Attribute zuzuweisen, was in diesem Fall nicht möglich ist!
var Animal=new Type; // erzeuge einen neuen Typ (erbt von ExtObject) Animal.canFly:=false; // Objektattribut "canFly" als Bool hinzufügen Animal.friends:=[]; // Objektattribut "friends" als Array hinzufügen (vermutlich eine dumme Idee) var Rabbit=new Type(Animal); // erzeuge einen neuen Typ (erbt von Animal) Rabbit.name:=void; // leeres Objektattribut "name" hinzufügen Animal.weight:=0; // Objektattribut "weight" hinzufügen // Achtung: Der Typ Rabbit ist aber schon angelegt, bekommt davon also nichts mit! var hubert=new Rabbit; // erzeuge eine Instanz von Rabbit var emma=new Rabbit; // erzeuge eine Instanz von Rabbit hubert.name = "Hubert"; // weise dem Objektattribut "name" von hubert einen Wert zu emma.name = "Emma"; // weise dem Objektattribut "name" von emma einen Wert zu emma.canFly = true; // Emma kann fliegen! hubert.friends += "HubDiWub"; // Füge einen Eintrag in das Array "friends" hinzu. // Achtung: Da "friends aber vom Typ Array ist und Arrays call-by-reference übergeben werden, // teilt sich hubert das "friends"-Array mit Animal, Rabbit und auch emma! // hubert.weight = 10; // Fehler: "weight" ist kein Objektattribut von hubert! out( hubert.name, " ", hubert.canFly, " friends:",hubert.friends.implode(",") ,"\n" ); // Ausgabe out( emma.name, " ", emma.canFly, " friends:",emma.friends.implode(",") ,"\n" ); // Ausgabe
Hubert false friends: HubDiWub Emma true friends: HubDiWub
Neben den Objektattributen können Typen auch Typattribute enthalten.
Diese Attribute sind von jeder Instanz zugreifbar, sie werden jedoch nicht als Attribut in jeder Instanz angelegt sondern verbleiben im Typ-Objekt.
Man kann sie sich daher in etwa wie Klassenvariablen (Schlüsselwort static
) in C++ vorstellen.
Neben dem Einsatz für typenübergreifende (statische) Variablen, lassen sie sich vor allem zum Speichern von Funktionen verwenden.
Meistens braucht man nicht für jede Instanz eines Typs ein eigenes Objektattribut für jede Funktion, sondern es reicht aus, wenn die Funktion über den jeweiligen Typ zugreifbar ist.
So wird z. B. nicht für jeden einzelnen String bei seiner Erzeugung ein Attribut für die length
-Funktion angelegt, sondern diese ist nur einmal im String
-Typobjekt als Typattribut gespeichert.
Da jeder String auf den Typ String
verweist, ist der Zugriff dennoch möglich: "foo".length()
.
Ein Typattribut ist auch von allen abgeleiteten Typen zugreifbar.
Fügt man beispielsweise ein Typattribut zum Typobjekt Object
hinzu, ist dieses beispielsweise von jedem Number
-Objekt aus zugreifbar.
Um ein Typattribut zu einem Typobjekt hinzuzufügen, wird der ::=
-Operator verwendet: typeObj.typeAttr::=initialValue;
.
var Animal=new Type; // erzeuge einen neuen Typ (erbt von ExtObject) Animal.name:="genericAnimal"; // Objektattribut hinzufügen Animal.printCounter::=0; // Typattribut hinzufügen (von allen Instanzen zugreifbar) Animal.print::=fn(){ // Funktion als Typattribut hinzufügen printCounter++; // Zugriff auf Typattribut out(printCounter,".) My name is ",name,"\n"); // Zugriff auf Typattribut "printCounter" und Objektattribut "name" }; var a=new Animal; // erzeuge eine Instanz von Animal a.name="Pubert"; var b=new Animal; // erzeuge eine Instanz von Animal b.name="Moehk"; a.print(); b.print();
1.) My name is Pubert 2.) My name is Moehk
Bei der Instanziierung eines Typs wird über den new
-Operator der Konstruktor aufgerufen.
Um einen eigenen Konstruktor zu definieren, kann das _constructor
-Attribut des Typobjekts definiert werden.
(Sinnvollerweise als Typattribut und nicht als Objektattribut; eine eigene Referenz auf den Konstruktor in jeder Instanz macht in den meisten Fällen keinen Sinn.)
var Animal=new Type; // erzeuge einen neuen Typ (erbt von ExtObject) Animal.name:=void; // Leeres Objektattribut hinzufügen Animal._constructor::=fn(n){ // Konstruktor als Typattribut hinzufügen name=n; }; Animal.print::=fn(){ // Funktion als Typattribut hinzufügen out("My name is ",name,"\n"); }; var a=new Animal("Pubert"); // erzeuge eine Instanz von Animal a.print();
My name is Pubert
Bevor der Rumpf eines Konstruktors aufgerufen wird, wird immer jeweils der Konstruktor der Basisklasse (=Superkonstruktor) rekursiv aufgerufen.
Um an den jeweiligen Superkonstruktor Parameter übergeben zu können, können entsprechende Ausdrücke bei der Definition der Konstruktorfunktion nach den Parametern hinter einem Punktoperator angegeben werden.
fn( konstruktorParameter )@(super( superkonstruktorParameter )) { ... }
var Animal = new Type; // erzeuge einen neuen Typ (erbt von ExtObject) Animal.name := void; // Leeres Objektattribut hinzufügen Animal.canFly := void; // Leeres Objektattribut hinzufügen Animal._constructor ::= fn(n,f){ // Konstruktor als Typattribut hinzufügen name = n; canFly = f; out("Animal created\n"); }; Animal.print ::= fn(){ // Funktion als Typattribut hinzufügen out("My name is ",name,"and I can ", (canFly?"fly":"not fly") ,".\n"); }; var Dragon = new Type(Animal); // Dragon erbt von Animal Dragon._constructor ::= fn(n)@(super(n+" the Great",true)){ // Konstruktor ruft Superkonstruktor mit zwei Parametern auf out("Dragon created\n"); }; var a = new Dragon("Pubert"); // erzeuge eine Instanz von Dragon a.print();
Animal created Dragon created My name is Pubert the Great and I can fly.
Es wird als erstes der Konstruktor desjenigen Basistyps aufgerufen, dessen Konstruktor ein C++-Funktionsobjekt vom Typ Function
und nicht mehr vom Typ UserFunction
ist.
Dieser muss dann das eigentliche C++-(EScript)-Objekt zurück liefern.
Neben der Unterscheidung von Typattributen und Objektattributen, gibt es noch mehrere anpassbare Attributeigenschaften (seit Version 0.6.0).
Um diese zu definieren, kann zwischen dem Namen des Attributes und dem Attributerzeugungsoperator (:=
oder ::=
) eine Liste von Eigenschaftskonstanten angegeben werden: Object.AttributName @(Eigenschaft1,Eigenschaft2, ...) := Wert
.
Es können dabei mehrere Eigenschaften kombiniert werden, wobei sich unsinnige Kombinationen ergeben können, die dann eine Warnung verursachen (z.B. @(private,public) ).
Es folgt eine Übersicht über die wichtigsten Eigenschaften.
@(const)
Die Eigenschaft @(const)
verhindert, dass einem Attribut ein neues Objekt zugeordnet wird.
Der Wert des gespeicherten Objektes kann sich jedoch durchaus ändern.
Damit entspricht es in C++ einem const-Pointer (und nicht einem const-Wert).
Beim Versuch einen neues Objekt zuzuweisen, wird eine Ausnahme geworfen.
var a = new ExtObject; a.v @(const) := 1; // Konstantes Objektattribut anlegen. a.v += 1; // Erlaubt, das Objekt bleibt gleich und nur sein Wert ändert sich; a.v = a.v + 1; // Ausnahme wird geworfen beim Versuch der Zuweisung zu einem konstanen Attribut.
@(init)
Die Eigenschaft @(init)
erlaubt die Initialisierung von Objektattributen mit Instanzen eines Typs oder dem Rückgabewert einer beliebigen Funktion (oder FnBinder oder anderem ausführbaren Objekt).
Dazu ein Beispiel, bei dem jeder Instanz eines Objektes ein Array als Objektattribut hinzugefügt werden soll:
// Möglichkeit 1: Im Konstruktor var A = new Type; A.arr := void; // leeres Objektattribut anlegen A._constructor ::= fn(){ arr = []; // Array erzeugen und zuweisen }; // Möglichkeit 2: FALSCH!!!!!!!!!! var B = new Type; B.arr := []; // Einzelnes Array-Objekt als Objektattribut anlegen // Dies führt dazu, dass zwar alle Instanzen von B ein eigenes Objektattribut // 'arr' besitzen. Alle teilen sich jedoch das selbe Array!!!!!! // Möglichkeit 2: @(init) mit Typ var C = new Type; C.arr @(init) := Array; // Attribut wird mit einem Type-Objekt initialisiert. // Jede Instanz von C bekommt nun eine eigene Instanz von Array als Objektattribut 'arr' // Möglichkeit 2: @(init) mit Funktion var D = new Type; D.arr @(init) := fn(){ return []; }; // Attribut wird mit einer Funktion initialisiert. // Für jede Instanz von D wird die Funktion aufgerufen und ihr Rückgabewert im Objektattribut // 'arr' gespeichert.
@(override)
Die Eigenschaft @(override)
führt dazu, dass das Attribut bereits existieren muss (im Objekt selber oder in einem der geerbten Typen), wenn es angelegt wird.
Existiert noch kein Attribut gleichen Namens, wird eine Warnung ausgegeben; die Attributerstellung wird jedoch ausgeführt.
Diese Eigenschaft ist nützlich um auf Tippfehler oder Änderungen in den Methodennamen einer Basisklasse hingewiesen zu werden und sollte daher an entsprechenden Stellen auch eingesetzt werden ;-)
var A = new Type; A.m ::= fn(){ out("foo!"); }; var B = new Type(A); B.m @(override) ::= fn(){ out("bar!"); }; // Ok. B.m2 @(override) ::= fn(){ out("grrr!"); }; // Warnung, da 'm2' nicht existiert. B.m2 @(override) ::= fn(){ out("blubb!"); }; // Jetzt ok, da 'm2' trotz Warnung in der letzten Zeile angelegt wurde.
@(private)
Die Eigenschaft @(private)
schränkt den Zugriff auf ein Memberattribut so ein, dass nur noch von Membermethoden des jeweiligen Objektes darauf zugegriffen werden kann.
Zugriffe von ausserhalb führen zu einer Warnung und werden nicht ausgeführt.
@(private)
verhält sich anders als z. B. das private
in C++.
In C++ schränkt es den Zugriff auf Memberfunktionen des gleichen Typs ein; in EScript wird der Zugriff auf das jeweilige Objekt eingeschränkt.
Ein Zugriff auf geerbte, private Attribute ist möglich (in C++ nicht).
Ein Zugriff auf private Attribute eines anderen Objektes des exakt gleichen Typs ist nicht möglich (in C++ jedoch schon).
var A = new Type; A.v1 @(private) := 1; // privates Objektattribut anlegen A.m1 @(private) ::= fn(){ out("m1:",v1,"\n"); }; // privates Typattribut anlegen A.m2 ::= fn(obj){ out("m2:",obj.v1,"\n"); }; var B = new Type(A); var b = new B(); b.m3 := fn(){ m1(); }; // aus öffentlicher Funktion, geerbte, private funktion aufrufen...ok! out( b.v1 ); // Warnung wegen Zugriffsverletzung; Ausgabe 'void' b.m1(); // Warnung wegen Zugriffsverletzung. Keine Ausgabe. b.m3(); // Ok. Ausgabe: 'm1:1'. (b->fn(){m1();})(); // Ok (Da das FnBinder als Memberfunktion wirkt. Siehe FnBinders) Ausgabe: 'm1:1'. b.m2(b); // OK. Ausgabe: 'm2:1'. b.m2(new B()); // Warnung wegen Zugriffsverletzung. Keine Ausgabe.
@(type)
Die Eigenschaft @(type)
sorgt für die Anlegung eines Typattributes (anstatt eines Objektattributes).
Damit ist es nichts anderes als eine alternative Schreibweise für den ::=
-Operator.
Der Einfachheit halber sollte auch der ::=
-Operator vorgezogen werden.
Ich habe die @(type)
-Eigenschaft an dieser Stelle nur erwähnt, um deutlich zu machen, dass der ::=
-Operator nur eine syntaktische Kurzschreibweise für diese Eigenschaft darstellt.
var A = new Type; A.t1 ::= "dies ist ein Typattribut"; A.t2 @(type) := "dies ist auch ein Typattribut";
Ein FnBinder ist die Kombination eines Funktionsobjektes und gebundener Parameterwerte und/oder gebundenem, aufrufenden Objekt.
Ein FnBinder, mit einem gebundenen, aufrufenden Objekt, wird durch den ->
-Operator erzeugt: object -> function
.
Die Funktion des FnBinders wird wie eine Attributsfunktion (oder auch Memberfunktion) des Objekts ausgeführt; d. h. innerhalb der Funktion kann man z. B. mit this
auf das Objekt des FnBinders zugreifen.
Einsetzen lassen sich FnBinders beispielsweise bei der Implementierung von Listenern:
var a = new ExtObject; // ExtObject erzeugen a.name := "foo"; // Objektattribut hinzufügen a.wakeUp := fn(){ // Funktion hinzufügen out(name," wakes up"); }; // Einfaches Beispiel var delegate = a -> fn(){ // ein FnBinder erzeugen out(name,"\n"); }; delegate(); // FnBinder ausführen // Listener Beispiel var listener = []; // Array anlegen listener += a -> a.wakeUp; // FnBinder erzeugen und zu Listener-Array hinzufügen // ... foreach(listener as var l) // jeden Listener ausführen l(); // Hier brauchen wir den Attributnamen der aufgerufenen Funktion (wenn sie denn einen hat) // nicht zu kennen. Sowohl Objekt als auch Funktion sind ja im FnBinder gespeichert.
foo foo wakes up
Ein FnBinder, mit gebundenen Parameterwerte, wird durch den =>
-Operator erzeugt: Array mit Werten => function
.
Die Werte werden an die ersten Parameter gebunden; die restlichen Parameter bleiben frei.
var f = fn( a,b,c){ outln( "a:", a, " b:", b, " c:", c ); }; var b1 = [4] => f; var b2 = [4,17] => f; var b3 = [42] => b2; f(1,2,3); b1(1,2); b2(1); b3();
a:1 b:2 c:3 a:4 b:1 c:2 a:4 b:17 c:1 a:4 b:17 c:42
Wenn man mit einem FnBinder sowohl ein Objekt, als auch Parameter binden möchte, sollte das Objekt innen (rechts) stehen. Z. B. [1,2,3] => obj->fn(p1,p2,p3){}
.
Alle globalen Variablen werden durch Attribute eines speziellen GLOBALS
-Objekts abgebildet.
Globale Variablen sind von überall direkt zugreifbar, wie z. B. die Funktion out
.
Um neue globale Variablen zu deklarieren, können sie als Objektattribute zu GLOBALS
hinzugefügt werden.
GLOBALS
selbst ist eine globale Variable und kann so von überall erreicht werden
GLOBALS.doSomething:=fn(){ // neue globale Funktion deklarieren out("Dumdidum...\n"); }; var f=fn(){ doSomething(); // doSomething ist global, daher auch in allen Funktionen aufrufbar. }; f();
Dumdidum...
Neue globale Variablen sollten nur in Ausnahmefällen deklariert werden! Statische Variablen sind eine mögliche Alternative.
Während des Parsens von Skriptdateien werden vom Tokenizer einige Ausdrücke durch andere Werte ersetzt (wie beim C-Präprozessor):
Das ist vor allem praktisch fürs Debugging und um Skriptdateien aus dem gleichen Verzeichnis zu laden.
In der StdLib sind einige Funktionen im globalen Namensraum definiert. Im Folgenden eine kurze Übersicht über die wichtigsten.
Object eval(String expression)
Mit eval
wird EScript-Code compiliert, ausgeführt und das Ergebnis zurückgegeben.
Fehler, die beim Parsen auftreten können (z. B. Syntaxfehler), lösen eine Ausnahme aus.
var s="1+2;"; var result; try{ result = eval(s); // Anweisung compilieren, ausführen und Ergebnis ausgeben }catch(e){ // mögliche Fehler abfangen out(e); } out(result);
3
Object load(String filename)
/ Object loadOnce(String filename)
load
und loadOnce
laden eine EScript-Datei und führen sie aus.
loadOnce
tut dies jedoch nur einmal für jede Datei und gibt bei wiederholtem Aufruf lediglich void
zurück.
Wird die geladene Datei mit einem return
-Statement beendet, liefern die Funktionen den entsprechenden Rückgabewert zurück.
Kann die Datei nicht geladen werden oder werden Fehler entdeckt, wird eine Ausnahme ausgelöst.
void out(...)
/ void outln(...)
/ void print_r(...)
out
gibt alle übergebenen Parameter als String
aus.
outln
gibt alle übergebenen Parameter als String
und einem abschließenden Zeilenumbruch aus.
print_r
gibt ebenfalls alle Parameter als String
aus, jedoch werden Array
- und Map
-Objekte formatiert ausgegeben.
Number clock()
clock
gibt die Anzahl der Sekunden seit Programmstart zurück.
Number time()
/ Map getDate( [Number time] )
time
gibt die aktuelle Zeit als fortlaufende Zahl zurück.
getDate
wandelt diese Zahl in eine Map mit entsprechenden Angaben um.
print_r( getDate() );
{ [hours] : 19, [isdst] : 1, [mday] : 19, [minutes] : 56, [mon] : 7, [seconds] : 32, [wday] : 1, [yday] : 199, [year] : 2010 }
String chr(Number ascii)
Gibt einen String mit einem Zeichen entsprechend des übergebenen ASCII-Codes zurück.
for(var i=65;i<=90;++i) out(chr(i));
ABCDEFGHIJKLMNOPQRSTUVWXYZ
String getOS()
Gibt einen String entsprechend des verwendeten Betriebssystems zurück. Mögliche Werte sind "WINDOWS", "MAC OS", "LINUX", "UNIX" oder "UNKNOWN".
String toJSON( String|Number|Bool|Array|Map|Void value[, Bool formatted=true] )
toJSON
wandelt Standardtypen (String
, Number
, Bool
, Array
, Map
und Void
) in eine JSON-formatierte Zeichenkette um.
void
wird dabei zu null
.
Wenn der Parameter formatted
auf true
gesetzt ist, wird die Zeichenkette formatiert (ggf. mehrzeilig mit Einrückung).
Object parseJSON(String)
toJSON
wandelt eine JSON-formatierte Zeichenkette wieder in Standardobjekte um.
TODO...
Object
Wie bereits erwähnt, erben alle Objekte aus denen man in EScript zugriff hat von der Klasse Object
.
Die Memberfunktionen dieser Klasse sind daher auch von jedem Objekt aufrufbar.
new Object()
Erzeugt ein neue Instanz von Object
.
ExtObject
verwenden.toBool()
, toNumber()
, toString()
) Jedes Objekt unterstützt die explizite Typumwandlung in die Standardtypen Bool
,Number
und String
über die Funktionen toBool()
, toNumber()
und toString()
.
Die Konvertierung verläuft nach den oben genannten Regeln.
toBool()
den Wert false
zurückliefern sind der Wert false
und void
.
Auch die Zahl 0
liefert true
!==
, !=
, ===
, !==
) Die Vergleichsoperator ==
überprüft zwei Werte auf Gleichheit und gibt das Ergebnis als Bool
zurück.
Ist die linke Seite des Vergleichs ein Objekt eines einfachen (call by reference) Typs wird bei der rechte Wert mit impliziter Typumwandlung in den Wert des linken umgewandelt.
Dadurch liefert z.B. 17 == "17"
und true == 0
den Wert true
.
Andere Objekte können den Operator anders implementieren.
Arrays überprüfen bespielsweise ob das andere Objekt auch ein Array mit den gleichen (im Sinne von "==") Objekten ist.
Der Operator !=
liefert standardmäßig den negierten Wert des ==
-Operators zurück.
Der Operator ===
überprüft ob die beiden Objekte identisch sind.
Bei einfachen (call by reference) Typen (toBool()
, toNumber()
, toString()
) bedeutet dies, dass sowohl der Wert als auch der Typ übereinstimmen muss.
Dadurch liefert z.B. 17 === "17"
und true === 0
hier den Wert false
.
Bei anderen Typen (z.B. Array
, ExtObject
...) wird überprüft ob es sich tatsächlich um die selben Objekte handelt.
Der Operator !==
liefert den negierten Wert des ===
-Operators zurück.
void
ist, sollte grundsätzlich der ===
-Operator genutzt werden.
Z.B.:if( myVar === void) doSomething();
EScript::isEqual(runtime,leftObject,rightObject)
(oder das entsprechende isIdentical(...)
) verwendet werden.
Um aus C++ für einen eigenen Typ eine eigene Vergleichsoperation zu definieren, sollte die Memberfunktion rt_equals( other )
überschrieben werden.
In EScript kann ein eigener Vergleichsoperation durch überschreiben des "=="
-Attributs erreicht werden.
Das "!="
-Attribut muss nicht zusätzlich implementiert werden.
EScript::isEqual(rt,left,right) -> left.'==' '==' -> rt_equals( other ) '!=' -> ! EScript::isEqual(rt,caller,right) EScript::isIdentical(rt,left,right) -> left.'===' '===' -> rt_identical( other ) '!==' -> ! EScript::isIdentical(rt,caller,right)
Object ---|> Type
Der ---|>
-Operator (isA-Operator) liefert true
wenn der Wert links vom rechts angegeben Typ ist.
Die Vererbung der Typen wird dabei beachtet, d.h. auch wenn der Typ des Objekts vom angegebenen Typ erbt, liefert der Operator true
.
getType()
Funktion den Typ abfragen und dann mit dem gewünschten Typ vergleichen.out( 3 ---> Number ); // 3 is of Type Number or inherits from Number ... true out( 3.getType() == Number ); // check if the type of 3 is Number (and not inherited from Number) ... true var a=new Array(); out( a ---> Collection ); // a is of Type Array, Array inherits from Collection ... true out( "foo" ---|> String ); // The type of "foo" is String ... true out( "foo".getType() ---|> String ); // The type of "foo" is String, String is of type Type (and not String) ... false out( "foo".getType() ---|> Type ); // The type of "foo" is String, String is of type Type ... true out( 3 ---|> "foo" ); // "foo" is no Type ... ERROR
Object.clone()
Alle in EScript integrierten Typen unterstützen die clone()
-Funktio, die eine Kopie des Objektes zurück gibt.
Wird eine Collection
(also ein Array
oder eine Map
) mittels clone
kopiert, wird nur eine flache Kopie angelegt.
D.h. enthält die Collection
auch Werte mit nicht simple Typen (wie z.B. weitere Collection
s) werden diese nur referenziert und nicht rekursiv kopiert.
// create a map with a simple value and a non simple value var m={ "simple" : "foo", "complex" : [ 1,2,3 ] }; var m2=m.clone(); // m2 is a flat copy of m1 m2["simple"] = "bar"; m2["complex"] += 4; // this does also modify m1, as "complex" references // the same Array out("m1:"); print_r(m1); out("m2:"); print_r(m2);
m1: { "simple : "foo" , [ 1,2,3,4] } m2: { "simple : "bar" , [ 1,2,3,4] }
Bool
TODO!
Number
TODO!
String
TODO!
Collection
TODO!
ExtObject
ExtObject.clone()
Das Zuweisen der Attribute an das neue Objekt geschieht entsprechend der Call-By-Value oder Call-By-Reference Eigenschaft des entsprechenden Wertes.
Ist ein Attribut beispielsweise ein Array, so wird nachher vom Klon auf das gleiche Element referenziert.
Da dies nicht immer erwünscht ist, empfiehlt sich hier das Überschreiben der code
-methode:
\\ TODO Beispiel erstmal nur aus Testcase übernommen; muss noch überarbeitet werden! var A=new Type(ExtObject); // A erbt von ExtObject A.m1:=1; // m1 enthält einen call-by-value Wert, kann also so initialisiert werden A.m2:=void; // m2 soll einen call-by-reference Wert enthalten, wird also zunächst leer initialisiert... A._constructor ::=fn(){ m2=[]; // ... und dann im Konstruktor zugewiesen }; // ---|> ExtObject A.clone ::=fn(){ var n=(this -> (ExtObject.clone))(); // zunächst die clone-Methode von ExtObject auf dem aktuellen Objekt aufrufen n.m2=m2.clone(); // und von hand den call-by-reference Wert clonen. return n; }; var a=new A(); var b=a.clone(); a.m1++; a.m2+="x"; print_r(a._getAttributes()); print_r(b._getAttributes());
Array
self Array.sort( [comparatorFunction] )
Man kann Array.sort
eine Funktion mit 2 Parametern geben, die für jeden Vergleich aufgerufen wird.
// Einfaches Beispiel: (normales Sortieren) var array=[3,23,7,3,100,1,35]; array.sort( fn(a,b) { return a<b; } ); // == [1,3,3,7,23,35,100] // Komplexeres Beispiel: (Nach Abstand zum Wert 59 sortieren.) var s=new ExtObject(); s.base := 59; array.sort( s->fn(a,b) {return (a-base).abs()<(b-base).abs(); } ); // == [35,23,100,7,3,3,1]
Intern ist hier ein QuickSort-Algorithmus implementiert.
Map
TODO!
TODO!