Sprachbeschreibung Version: 1.20
λscript wurde geschrieben um große komplexe Systeme mit vielen Sensoren und Aktoren zu steuern. Übliche "SMART HOME"-Systeme sind häufig vergleichsweise klein: Ein paar Rolläden, einige Thermostate und Ventile, ein paar Lampen – viel mehr ist es oft nicht. Trotzdem können auch solche Systeme sehr komplex werden. Die Anzahl der möglichen Verküpfungen wächst sehr viel schneller als die Anzahl der Komponenten. Mit den klassischen Möglichkeiten die FHEM bietet, wird die Programmierung der Steuerung schnell unübersichtlich und fehleranfällig. λscript schafft hier eine Alternative. Moderne Programmiertechniken (insbesondere objektorientierte und funktionale Programmierung) erlauben es sehr komplexe Aufgaben mit wenig Code zu lösen. Komfortables Multithreading rundet die Sprache ab.
λscript ist sehr umfangreich und bietet mehr Möglichkeiten als der durchschnittliche FHEM-Benutzer wahrscheinlich benötigt. Nach der Fertigstellung dieser Seite soll sie den gesamten Sprachumfang dokumentieren. Es ist kaum möglich, etwas vollständig zu erklären ohne auf später Beschriebenes vorzugreifen. Das macht das Verstehen einiger Textpassagen für den Einsteiger, der noch nie programmiert hat, etwas schwieriger. Für erfahrene Programmierer trifft das sicher weniger zu.
Deshalb ist diese Dokumentation eher zum Nachschlagen als zum Erlernen der Sprache gedacht. Für den Anfänger ist es einfacher, mit den kleinen Scripts, wie sie hier z.B. unter dem Menupunkt "Schnellstart" zu finden sind, zu beginnen. Diese können fast spielerisch abgewandelt und ergänzt werden. Das sorgt für ein steile Lernkurve und macht das Verstehen der Sprachbeschreibung leichter.
1 Klassifizierung von λscript
Wenn man λscript klassifizieren möchte, lässt sich das anhand der in ihr umgesetzten Programmierparadigmen tun. λscript ist:
- imperativ
- Bestimmende Merkmale für diese Art des Programmierens sind:
- Im Quellcode wird festgelegt, was in welcher Reihenfolge und wie zu tun ist. Das Grundprinzip ist das schrittweise Fortschreiten innerhalb der Kommandofolge.
- Zur Steuerung der Befehlsausführung werden Kontrollstrukturen (z.B. Schleifen, Verzweigungen) benutzt.
- strukturiert
- Strukturierte Programmierung beinhaltet zum einen die baumartige Zerlegung eines Programms in Teilprogramme (Prozeduren) und enthält somit das Paradigma der prozeduralen Programmierung. Zudem verlangt die strukturierte Programmierung auf der untersten Ebene die Beschränkung auf lediglich drei Kontrollstrukturen:
- Sequenz (hintereinander auszuführende Programmanweisungen)
- Auswahl/Selektion (Verzweigung)
- Wiederholung/Iteration (Schleifen)
- objektorientiert
- Unter Objektorientierung (kurz OO) versteht man in der Entwicklung von Software eine Sichtweise auf komplexe Systeme, bei der ein System durch das Zusammenspiel kooperierender Objekte beschrieben wird. Einem Objekt werden bestimmte Attribute (Eigenschaften) und Methoden zugeordnet. Dabei muss ein Objekt nicht gegenständlich sein. Entscheidend ist, dass bei dem jeweiligen Objektbegriff eine sinnvolle und allgemein übliche Zuordnung möglich ist. Ergänzt wird dies durch das Konzept der Klasse, in der Objekte aufgrund ähnlicher Eigenschaften zusammengefasst werden. Ein Objekt wird im Programmcode als Instanz Klasse definiert.
- statisch typsicher
- Während der Kompilierung wird festgestellt, ob im Script eine Typverletzung auftritt. D.h. Wertzuweisungen werden auf Verträglichkeit geprüft und bei Operationen wird sicher getestet ob diese Operation mit den Operanden verträglich ist. Ist das nicht der Fall, kann das Script nicht gestartet werden. Dieses Konzept sorgt für schneller ablaufenden Programmcode, da die Überprüfung nicht zur Laufzeit erfolgen muss, und gleichzeitig für eine höhere Betriebssicherheit.
- funktional
- Funktionale Programmierung ist ein Programmierparadigma, innerhalb dessen Funktionen nicht nur definiert und angewendet werden können, sondern auch wie Daten miteinander verknüpft, als Parameter verwendet und als Funktionsergebnisse auftreten können. Dadurch kann ein funktionales Programm sehr weitreichend neue Berechnungsvorschriften zur Laufzeit zusammensetzen und anwenden.
Eine Konsequenz daraus ist, dass es in funktionaler Programmierung besonders einfach ist, Algorithmen ohne Berücksichtigung der Beschaffenheit der bearbeiteten Datenobjekte zu beschreiben und dadurch generischen Programmcode zu erstellen. - nebenläufig
- Die Nebenläufigkeit ist die Eigenschaft eines Systems, mehrere Berechnungen, Anweisungen oder Befehle gleichzeitig ausführen zu können. Es kann sich dabei um völlig unabhängige Anweisungen handeln, bis hin zur gemeinsamen Bearbeitung einer Aufgabe. Sie können dabei auch miteinander interagieren (z. B. um Zwischenergebnisse auszutauschen).
2 Grundlegende Begriffe
2.1 Symbol
Ein Symbol ist eine Zeichenfolge, die folgende Zeichen nicht enthält: Leerzeichen, Tabulatoren, Zeilenumbrüche und keines der Zeichen: [ ] ( ) { } ' " ;
Es gibt Symbole, die in λscript für die Definition von Funktionen verwendet werden. Einige dieser reservierten Symbole sind:
:= ==
+ - * / ^
= !=
< <= <> <=> => >
~ !~
<< >>
& and | or ! not
my new
if wait repeat while forEach loop quit
thread always terminate stop
set setReading userReading
today tomorrow yesterday
year easter month week day hour minute now
years months weeks days hours minutes seconds
random shuffle
class of dimension default
toString toBoolean toNumber alternatively
Wird einem der oben aufgelisteten Symbole ein Wert zugeweisen, wird dieses Symbol zu einer Variablen. Dann sind die mit diesem Symbol definierten Funktionen in ihrer ursprünglichen Form nicht mehr aufrufbar. (Der gleiche Effekt tritt auf, wenn diese Symbole als Namen für λscripts verwendet werden.) Das stellt kein prinzipielles Problem dar; der Aufruf dieser Funktionen ist immer noch möglich. Einfacher ist es, die oben genannten Symbole nicht als Variable, Klassennamen oder Devicenamen zu verwenden. Sollte man dies doch tun wollen, ist lediglich zu verhindern, dass beim Aufruf von Funktionen, in denen diese Symbole verwendet werden, der Compiler an die Stelle des Symbols, die Variable einsetzt. Das wird durch das Voranstellen eines Apostrophs '
erreicht.
Wenn z.B. wait
ein Wert zugewiesen wurde, ist wait
damit zu einer Variablen geworden. Dann muss der Befehl für "Warte eine Stunde" als 'wait 1:00;
geschrieben werden.
2.2 Klasse
Eine Klasse ist, wie in allen objektorientiert arbeitenden Programmiersprachen, auch hier ein abstraktes Modell für eine Reihe von ähnlichen Objekten. Es gibt in λscript vordefinierte Klassen (number
, string
, timespan
, stack
, fhemDevice
, ...). Alle Klassen sind eine Instanz der Klasse class
. In λscript können den vordefinierten Klassen Neue hinzugefügt werden. Einfache Vererbung wird unterstützt. Eine Klassendefinition wird durch ihren Namen repräsentiert. Mit der Definition einer neuen Klasse entsteht eine neue Variable die mit dem Namen der Klasse identisch ist.
2.3 Werte
λscript ist streng objektorientiert. Alle Werte sind von Klassen abgeleitete Instanzen. Der Wert 123
ist zum Beispiel eine Instanz der Klasse number
; oder 123 ist vom Typ number
. Der Wert "Hallo Welt"
ist Instanz der Klasse string
und 12:30
eine Instanz der Klasse timespan
.
Für jede Klasse gibt es eine besonderen Wert. empty
. Dieser Wert hat, außer seiner Zugehörigkeit zu einer bestimmten Klasse, keine Eigenschaft. Das Kommando class'empty
liefert als Ergebnis den zur Klasse class
gehörenden Wert empty
.
2.4 Variable
Einem Symbol kann ein Wert zugewiesen werden. Dann wird aus diesem Symbol eine Variable. Die Zuweisung geschieht durch den Zuweisungsoperator :=
. Beispiel:
zahl := 123;
Alles was rechts vom Symbol :=
steht, wird als Kommando interpretiert, ausgeführt und das Ergebnis dem links stehenden Symbol zugewiesen. Steht links bereits ein Variable, wird deren Wert durch den neu Berechneten ersetzt.
Einer Variablen (z.B. v
) kann dier Wert empty
durch v := number'empty
zugewiesen werden. Nach der Ausführung diese Kommandos hat die Variable v
den Wert empty
der Klasse number
. Mit v'isEmpty
kann getestet werden, ob die Variable v
den Wert empty
hat. Der Rückgabewert diese Kommandos ist true
oder false
.
Alle verwendeten Variablen sind statisch typisiert. Das heißt, jede Variable besitzt einen Wert, der Instanz einer Klasse ist. Dieser Variablen kann als neuer Wert nur ein Objekt vom Typ der Klasse zugewiesen werden, mit der die Variable erstmalig definiert wurde.
Es gibt mit dem Start von λscript vordefinierte Variable. Das sind:
- Klassennamen:
number
,string
,fhemDevice
, ... - Devices: Für jedes in der FHEM-Installation existierende Device gibt es eine Variable mit dem Namen des Devices. Diese Variable ist vom Typ
fhemDevice
. my
: In jedem Script wird automatisch eine Variablemy
angelegt. Sie enthält das zum Script gehörende fhemDevice.
2.5 Devices
Unter diesem Begriff wird die zu der Installation gehörende Hardwareumgebung zusammengefasst. In erster Linie sind das bei der Integration in eine fhem-Umgebung die fhem-Devices. Automatisch wird beim Start von λscript zu jedem fhem-Device eine Variable vom Typ fhemDevice
mit dem Namen des Devices angelegt. Während der Zugriff auf Variable in λscript eingeschränkt sein kann, sind Devices systemweit verfügbar.
2.6 Kommando
Ein Kommando ist eine Folge von Symbolen und Variablen, und führt eine definierte Funktion aus. Diese Folge ist entweder in runde Klammern eingeschlossen, oder sie endet mit einem Semikolon. Kommandos können ineinander verschachtelt werden. Beispiel:
z := 3 * (4 + 5);
Hier ist das Kommando (4 + 5)
in z := 3 * (4 + 5);
eingebettet.
Ein Kommando kann einen Rückgabewert haben. Dieser wird nach der Abarbeitung des Kommandos im übergeordneten Kommando an dessen Stelle eingesetzt. Im obigen Beispiel wird das Kommando (4 + 5)
ausgeführt und das Ergebnis 9
an dessen Stelle eingesetzt. Dann wird mit der Abarbeitung des Scripts fortgefahren.
2.7 Block
Ein Block ist eine in geschweifte Klammern eingeschlossene Folge von Kommandos, die nacheinander ausgeführt werden. Beispiel:
{
z := 3 * (4 + 5);
wait z'seconds;
set Lampe on;
}
Das letzte Semikolon als Abschluss des letzten Kommandos vor der schließenden geschweiften Klammer ist optional, kann also weggelassen werden. Blöcke können ineinander verschachtelt werden. D.h.: Blöcke können wiederum Blöcke enthalten.
Innerhalb eines Blocks kann auf die in übergeordneten Blöcken definierten Klassen und Funktionen und auf alle Devices zugegriffen werden.
Es werden zwei Arten von Blöcken unterschieden: Kommandoblöcke und Definitionsblöcke. Beides sind in geschweifte Klammern Folgen von Kommandos. Der Unterschied ist folgender:
- Kommandoblöcke werden im Script im Rahmen der imperativen Deklaration des "unmittelbar zu tuenden" notiert. Die Reihenfolge der Abarbeitung wird nicht geändert. Die beiden nachfolgenden Scriptschemen sind in so weit identisch:
Kommando1;
Kommando2;
Kommando3;
Kommando4;
Kommando1;
{
Kommando2;
Kommando3;
}
Kommando4;
Der Sinn des Einsatzes der zweiten Variante besteht z.B. in der so entstehen Möglichkeit, im untergeordneten Block lokal gültige Variable, Klassen oder Funktionen zu definieren. In einem Kommandoblock kann auf alle Variablen die in übergeordneten Blöcken definiert wurden zugegriffen werden. Soll eine Variable in einem Block eine neue lokale Bedeutung haben, ist so vorzugehen wie im Punkt Gültigkeitsbereich beschrieben ist.
- Definitionsblöcke tauchen innerhalb von Kommandos auf. Z.B. bei der Definition von Funktionen oder Klassen. Sie werden bei Ihrem Auftreten nicht unmittelbar abgearbeitet, sondern erst dann, wenn die Funktion über ein Kommando aufgerufen wird, oder eine Instanz eine Klasse generiert wird.
2.8 Script bzw. Programm
Ein Programm, ein λscript, ist ein Kommandoblock ohne die öffnende und schließende geschweifte Klammer. Diese Klammern werden vom Precompiler eigenständig zugefügt.
Jedes λscript ist in einen Superblock eingebettet. In diesem Superblock werden beim Systemstart alle systemimmanenten Klassen, Funktionen und Devices definiert.
2.9 Zugriffsoperator
Der Zugriffsoperator in λscript ist das Apostroph: '
. Er dient dazu, auf Elemente von Klassen oder Datenstrukturen zuzugreifen. Solche Zugriffe geschehen durch Aufrufe der Form Object'Eigenschaft
. In anderen Programmiersprachen wird dafür auch häufig der Punkt .
oder ->
verwendet. Da der Punkt aber Bestandteil eines Devicenamens in FHEM sein kann, wurde hier auf das Apostroph ausgewichen.
Die Verwendung des Zugriffsoperators ist lediglich eine andere Schreibweise für bestimmte Kommandos: Kommandos die aus zwei Elemeten bestehen: Aus einem Wert und einem nachfolgenden Symbol. Z.B. das Kommando 12 hours;
. Es besteht aus einer Zahl (hours
. Es liefert als Ergebis eine Wert vom Typ timespan
. Das Kommando, das das laufende Script stoppt und nach 12 Stunden fortsetzt, lautet: wait 12'hours;
. Das ist so leichter lesbar als: wait (12 hours);
Das Apostroph als Zugriffsoperator wurde lediglich aus Gründen der kürzeren Schreibweise und der besseren Lesbarkeit eingeführt. Es stellt keine neue Funktionalität bereit. λscript wandelt beim Einlesen des Scripts jeden Ausdruck der Form Wert'Symbol;
in (Wert 'Symbol)
um – macht daraus also ein "normales" Kommando. Dem entsprechend wird aus
Wert'Symbol1'Symbol2'Symbol3;
beim Einlesen
(((Wert 'Symbol1) 'Symbol2 ) 'Symbol3)
Wie dieses Beispiel zeigt, ist, bei einem mehrstufigen Zugriff auf die Eigenschaften eines Objektes, oder bei der Verwendung bestimmter Funktionen, die Schreibweise mit der Verwendung des Apostrophs besser lesbar.
2.10 Kommentierung
Skriptzeilen, die mit einem #
(es können Leerzeichen vorangehen) beginnen, werden beim Einlesen des Scripts übergangen.
# Das ist eine Kommentarzeile;
Für eine ausführliche Kommentierung des Scripts kann ein Kommentarblock definiert werden. Die erste Zeile eines Kommentarblocks muss mit /#
beginnen. (Leerzeichen können vorangehen.) Der Block endet mit einer Zeile, die mit #/
abschließt. Auch hier können Leerzeichen vorangehen denen beliebig viele #
folgen.
/###############
Das ist eine Kommentarblock.
1. Zeile
...
...
###############/
3 λscript und FHEM
Die hier beschriebene Version ist eine in Perl für FHEM adaptierte Version der Programmiersprache λscript. Die Integration in eine bestehende FHEM-Installation erfolgt durch das Einbinden des Moduls 98_lambda.pm. Das nachfolgende Schema visualisiert das Prinzip dieser Einbindung:
u
p
e
r
b
l
o
c
k
Bereitstellung der Devices, Definition von globalen Klassen und Funktionen
S
c
r
i
p
t
# Code für das 1. Script
# Inhalt des Internals DEF des 1. Devices vom Typ lambda
Codezeile 1;
…
thread {
…
};
…
S
c
r
i
p
t
# Code für das 2. Script
# Inhalt des Internals DEF des 2. Devices vom Typ lambda
…
repeat {
wait 1:00;
…
set WZ_Lampe on;
…
set WZ_Lampe off;
};
Ein λscript ist eine Folge von Kommandos. Der Compiler fasst diese Kommandos mit einem Paar geschweifter Klammern zu einem Block zusammen. Alle Scripte werden in einen Superblock eingebettet. In diesem Superblock werden beim Start von FHEM die vorhandenen Devices, die vordefinierten Klassen und Funktionen bereitgestellt. Devices, Klassennamen, Funktionen und Variable werden im Allgemeinen von einem übergeordneten Block in alle untergeordneten Blöcke durchgereicht. (Näheres im Punkt Gültigkeitsbereich. Daher sind die Devices in allen λscripts verfügbar. Das trifft auch auf die vordefinierten Klassen und Funktionen zu.
Ein λscript wird mit dem Kommando "define scriptname lambda" erstellt. Danach kann es mit dem DEF-Editor bearbeitet werden.
In den Devices vom Typ lambda ist ein Attribut mode definiert. Wenn das auf autostart gesetzt wurde, wird das entsprechende λscript mit dem Start von FHEM automatisch gestartet. Steht das Attribut mode auf disabled (Standardwert) kann das Script nur manuell oder durch ein anderes steuerndes Device mit "set scriptname start", gestartet werden. Für das Anhalten des Scripts existiert das Kommando "set scriptname stop". Der Neustart eines laufenden Scripts kann mit dem Kommando "set scriptname restart" ausgelöst werden.
Jedes Script läuft als eigener Thread. In jedem Script können beliebig viele weitere untergeordnete Threads gestartet werden.
Mit dem Start eines ersten λscripts wird ein Device lambdaDevices vom Typ lambdaDevices angelegt. Es bleibt die einzige Instanz des Typs. lambdaDevices hat die Aufgabe, die Verbindung von λscripts mit den anderen Devices der FHEM-Installation herzustellen.
4 Klassen
Auf eine definierte Klasse kann in dem Block, in dem sie definiert wurde, und in allen untergeordneten Blöcken zugegriffen werden. Da die nun aufgelisteten Klassen auf höchster Ebene definiert wurden, sind sie global verfügbar – wenn sie nicht durch den Nutzer neu definiert wurden.
Warning: filemtime(): stat failed for 10 index.php in /customers/8/7/e/lambda-script.org/httpd.www/index.php on line 874.1 Vordefinierte Klassen
4.1.1 value
Die Klasse value
ist eine Klasse ohne Eigenschaften und gleichzeitig die Basisklasse aller Werte. value
ist die Urahnin jedes Werts in λscript. Alle anderen Werteklassen sind von value
abgeleitet.
Die Klasse value
kann nicht instanziiert werden. D.h., es gibt keine Werte oder Objekte von Typ value
.
4.1.2 number
Zahlen in λscript sind sämtlich von der Klasse number
abgeleitet. Die in einem Script auftauchenden Zeichenfolgen 123
, 3.14
, -12e-5
werden in der üblichen Art und Weise als Zahlen interpretiert.
Besonders bei λscript ist, das auch gemeine Brüche z.B. -2/7
als Zahlen erkannt werden. Mit solchen Brüchen wird entsprechend den Regeln der Bruchrechnung exakt und ohne Rundungsfehler gerechnet.
4.1.3 string
Eine Instanz der Klasse string
ist eine beliebig lange Zeichenkette. In einem Script werden Zeichenketten in Anführungszeichen "
eingeschlossen. So ist "Hallo Welt"
eine Zeichenkette.
Soll ein Anführungszeichen in einer Zeichenkette enthalten sein, ist ihm das Zeichen \
voranzustellen. Das Kommando zur Ausgabe von Goethe schrieb "Faust"! in die Log-Datei lautet demnach:
log "Goethe schrieb \"Faust\"!";
Goethe schrieb "Faust"!
4.1.3.1 word
Die Klasse word
ist eine von der Klasse string
abgeleitete Klasse. Ein Objekt von Typ word
ist eine nicht leere Zeichenkette die kein Leerzeichen (auch keinen Tabulator und keinen Zeilenumbruch) enthält. Analog string
werden sie in einem Script in Anführungszeichen eingeschlossen. So wird "Hallo"
automatisch als Zeichenkette vom Typ word
erkannt.
4.1.4 regExpr
Diese Klasse dient zur Aufnahme von regulären Ausdrücken. Ein regulärer Ausdruck ist eine Zeichenkette, die der Beschreibung von Mengen von Zeichenketten mit Hilfe bestimmter syntaktischer Regeln dient. Reguläre Ausdrücke finden vor allem in der Softwareentwicklung Verwendung.
Beispiele für regulären Ausdrücke in λscript sind:
m/on/
m/^set_/
Das vorangestellte m
ist in beiden Fällen optional. Mit Hilfe diese Werte kann getestet werden, ob ein Zeichenkette die Zeichenfolge on
enthält oder mit der Zeichenfolge set
beginnt. Enthält die Variable x
eine Zeichenkette erfolgt der Test unter Verwendung des Symbols ~
:
isOn := x ~ m/on/;
oder
isOn := x ~ /on/;
Beginnt der reguläre Ausdruck mit einem s
werden die untersuchten Zeichenketten, entsprechend der im regulären Ausdruck codierten Vorschrift, modifiziert.
s/on/off/g
s/^set_//
Mit Hilfe diese Werte wird in der Zeichenkette die Zeichfolge on
durch off
ersetzt bzw. die führende Zeichenfolge set_
entfernt.
Über die Arbeitsweise von regulären Ausdrücken gibt es eine Vielzahl von Dokumentationen. (z.B: https://wiki.selfhtml.org/wiki/Regulärer_Ausdruck ) Deshalb wird sie an dieser Stelle nicht vertieft.
4.1.5 measurement
Diese Klasse modelliert die Messwerte physikalischer Größen. Ein Messwert besteht immer aus einer Maßzahl und einer Maßeinheit. Neben dem Zahlenwert, der vomTyp number
ist, hat measurement
eine weitere Eigenschaft: die Maßeinheit. Die Initialisierung einer Variablen l
vom Typ measurement
, die den physikalischen Wert "10 m" enthalten soll, erfolgt deshalb nach folgendem Schema:
l := [10 m];
Ein Kommando bestehend aus eckiger Klammer auf, der Maßzahl von Typ number
, einem Symbol für die Maßeinheit und eckiger Klammern zu, liefert einen Wert vom Typ measurement
. Als Maßeinheiten sind alle SI-Einheiten (inkl. der gesetzlich zulässigen Vorsätze) erlaubt. Daneben sind auch einige nicht-SI-Einheiten zulässig: °C, °F, ... Eventuell notwendige Exponenten werden (wenn nötig mit Vorzeichen) hinter der Maßeinheit notiert.
v := [10 m3];
Damit hat die Variable v
den Wert 10 m3. Das Kommando
g := [9.81 m1s-2];
definierte eine Variable g
mit dem Wert 9.81 m/s2 Hier ist der Exponent 1
nach m
notwendig, da die Einheit sonst als Millisekunde interpretiert werden würde. Eine zweite alternative Schreibweise wäre: [9.81 s-2m];
Einer Variable vom Typ measurement
kann nach ihrer Initialisierung nur wieder ein Wert mit der gleichen physikalischen Dimension zugewiesen werden. Soll der oben definierten Variablen l
, die die Dimension Länge repräsentiert, der Wert "12 Sekunden" (Dimension Zeit) zugewiesen werden, führt die Anweisung l := [12 s];
zu einer Fehlermeldung.
Mit Werten vom Typ measurement
kann wie mit Zahlen gerechnet werden. Bei Multiplikation und Division werden außer neben den Maßzahlen auch die Maßeinheiten miteinander verknüpft. Addition und Subtraktion sind nur möglich, wenn beide Operanden von der selben physikalischen Dimension sind. In dem folgenden Beispiel werden 1 m und 1 mm addiert.
l1 := [1 m];
l2 := [1 mm];
l3 := l1 + l2;
log l3;
1.001 m
Wie man an diesem Beispiel sieht, wird die Operation unter Berücksichtigung der konkreten physikalischen Einheit durchgeführt. Möchte man die Ausgabe in einer anderen Einheit (z.B. in cm) haben, wird dem Ergebnis die gewünschte Einheit zugewiesen.
l1 := [1 m];
l2 := [1 mm];
l3 := l1 + l2;
log l3;
1.001 m
l3 unit [cm];
log l3;
100.1 cm
Der Versuch der Zuweisung einer nicht passenden Einheit wird von Compiler erkannt und führt zu einer Fehlermeldung.
4.1.6 boolean
Von der Klasse boolean
sind genau zwei Werte instanziierbar: true
und false
.
Will man z.B. eine Variable t
mit dem Wert true
, und eine Variable mit dem Wert false
belegen, notiert man im Script:
t := true;
f := false;
4.1.7 moment
Die Klasse moment
repräsentiert Objekte, die einen bestimmten Zeitpunkt definieren. Intern wird dieser Zeitpunkt durch die entsprechende Unix-Zeit festgelegt. (Die Zahl der vergangenen Sekunden seit Donnerstag, dem 1. Januar 1970, 00:00 Uhr UTC.)
Konkrete Zeitpunkte sind: Der Beginn und das Ende eines Fußballspiels, oder der Moment in dem der erste Mensch den Mond betreten hat.
Ein fester Zeitpunkt kann z.B. durch die Angabe von Tag, Monat und Jahr definiert werden:
m := 31.1.2019
log m;
28.2.2019 00:00,00
Oder mit Uhrzeit:
m := 31.1.2019 12:34,56
log m;
28.2.2019 12:34,56
Zu letzten Beispiel ist anzumerken, dass es sich hier im Grunde um die Addition eines Objekts moment
31.1.2019
und eines Objekts timespan
12:34,56
handelt.
In λscript gibt es eine Reihe von Funktionen mit denen ein solcher bestimmter Zeitpunkt erzeugt werden kann. Dazu zählt z.B. die Einwort-Funktionen now
. Das Kommando t := now;
weist der Variablen t
die aktuelle Zeit zu.
Analog arbeiten z.B. die Funktionen today
und hour
. Nach der Abarbeitung des Kommandos
t := today;
h := hour;
hat t
den Wert "heute 0:00 Uhr" – den Zeitpunkt des Beginns des aktuellen Tages und h
den Wert des Beginns der laufenden Stunde.
4.1.8 timespan
Die Klasse timespan
repräsentiert Objekte, die eine bestimmte Zeitspanne beschreiben. Eine Zeitspanne ist die Differenz zwischen zwei Objekten von Typ moment
. So ist die aktuelle Uhrzeit ein timespan
und genau die Zeitspanne vom Beginn des heutigen Tages bis jetzt. Konkrete Zeitspannen sind auch: Die 90 Minuten Spieldauer eines Fußballspiels, oder die Zeit die man braucht, um von der Erde bis zum Mond zu fliegen.
Im Script auftauchende Zeichenketten der Gestalt m/d/h:min,sec
werden als eine Zeitspanne von m Monaten, d Tagen, h Stunden, min Minuten und sec Sekunden interpretiert. sec
kann ein mit Punkt geschriebener Dezimalbruch sein. Es ist möglich mit Bruchteilen von Sekunden zu operieren.
Dabei ist die Verwendung des Doppelpunktes, des Kommas oder die Verwendung von zwei Schrägstrichen zwingend notwendig, damit λscript die Zeichenfolge als timespan
interpretiert. Beispiele:
12/0/0
- ein Jahr0/7/0
oder7/0:0
oder168:0
- eine Woche1/0/0
oder24:00
- ein Tag1:0
oder60,0
- eine Stunde1,30
- 1,5 Minuten0,0.5
- 500 Millisekunden
4.1.9 stack
Ein Objekt von Typ stack
ist eine geordnete Menge von Objekten. Objekte können oben auf den Stapel gelegt werden oder unter den Anfang des Stapels geschoben werden. Der lesende und der löschende Zugriff ist nur auf das oberste oder das unterste Objekt möglich.
stack
hat eine Eigenschaft of
. Diese Eigenschaft definiert die Klasse, der alle Objekte des Stapel angehören müssen. (Instanzen von davon abgeleiteten Klassen sind hier eingeschlossen.) Der Standardwert von of
ist value
. Damit kann ein mit s := new stack
definierter Stapel sämtliche Werte aufnehmen, da alle Objekte in λscript Abkömmlinge von value
sind.
Eine Stapel s
der nur Zahlen enthalten kann, wird mit
s := new stack of number;
definiert.
Auf diesen leeren Stapel können mit dem Kommando
s << 3 4;
die Zahlen 3 und 4 obendrauf gepackt werden. Das Kommando
1 2 >> s;
schiebt die Zahlen 2 und 1 unter den Stapel.
log s;
würde jetzt 1 2 3 4 ausgeben.
Das Auslesen eines Stacks funktioniert ähnlich. Die Kommandos
top := s >;
bottom := < s;
weisen den Variablen top
und bottom
den obersten bzw. untersten Wert des Stapels zu.
Die Kommandos
top := s >>;
bottom := << s;
bewirken dasselbe, aber das unterste bzw. oberste Element wird zusätzlich vom Stapel gelöscht.
Um alle Elemente eines Stacks in einer Schleife zu durchlaufen, dienen Kommandos mit dem Schlüsselwort forEach
(siehe auch hier):
s := new stack of word;
s << "Montag" "Dienstag" "Mittwoch" "Donnerstag" "Freitag";
n := 1;
forEach s d {
log "Der " n ". Tag ist " d;
n'+
}
4.1.10 collection
Ein Objekt von Typ collection
ist eine indizierte Menge von Objekten. Als Indizes dienen Objekte von Typ word
oder Symbole.
collection
hat wie stack
eine Eigenschaft of
. Sie definiert ebenfalls die Klasse, der alle Objekte des collection
angehören müssen. Der Standardwert von of
ist auch hier value
.
Eine Collection c
in der nur Zeichenketten gespeichert werden, wird mit
c := new collection of string;
definiert. Der Zugriff auf die Elemente einer collection
passiert unter zuhilfenahme eckiger Klammern. Sowohl die collection
als auch der Index (ein Symbol oder ein word
) werden dazu in eckige Klammer geschrieben. Das Einspeichern von Werten geht unter dann so:
[c x] := "Hallo";
[c y] := " ";
[c z] := "FHEM";
log [c x] [c y] [c z];
Hallo FHEM
Das Kommando c keys;
gibt die aktuell in der collection c
enthaltenen Indizes in einem Stack vom Typ "stack of word
" zurück. Dem entsprechend liefert
[c x] := "Hallo";
[c y] := " ";
[c z] := "FHEM";
log c'keys;
(word|x,z,y)
Dabei ist anzumerken, dass die Reihenfolge der Schlüsselwerte nicht determiniert ist.
4.1.11 vector
Ein Objekt von Typ vector
ist ebenfalls eine indizierte Menge von Objekten. Als Indizes dienen die natürliche Zahlen 1, 2, 3, ...
vector
hat eine Eigenschaft dimension
. Der Wert von dimension
muss eine natürliche Zahl sein. Er definiert den höchsten erlaubten Index. Der muss zwingend bei der Definition eines Objekte vom Typ vector
angegeben werden. Eine zweite Eigenschaft ist default
. Das der Wert, den jedes Element des Vektors hat, solange ihm kein Anderer zugewiesen wurde. default
legt auch den Typ der in dem Vektor gespeicherten Daten fest. Ist default
z.B. vom Typ string
dürfen nur Zeichenketten in dem Vektor gespeichert werden.
Ein Vektor v
mit zehn Elementen, die zunächst alle den Wert 1 haben, wird mit
v := new vector dimension 10 default 1;
definiert. In diesen Vektor können dann auch nur Werte vom Typ number
gespeichert werden.
4.1.12 fhemReading
Ein Objekt von Typ fhemReading
hat zwei Eigenschaften: val
und time
.
val
ist vom Typ string
und time
vom Typ moment
. Sie stehen für den Wert und den Zeitpunkt der letzten Änderung des Readings eines FHEM-Devices.
Ist R
eine Variable die ein Objekt vom Typ fhemReading
enthält, dann ist R'val
der Inhalt des Readings und R'time
der Zeitpunkt der letzten Änderung.
Grundsätzlich kann auf ein Reading kann nur lesend zugegriffen werden. Eine Ausnahme bilden mit dem Kommando userReading
freigeschaltene Readings.
4.1.13 fhemDevice
Ein Objekt von Typ fhemDevice
bildet ein Device der FHEM-Installation ab. Objekte dieses Typs können innerhalb eines λscript nicht instanziiert werden, da sie quasi die unveränderliche Hardwareumgebung abbilden. Beim Systemstart wird für jedes gefundene Device eine Variable vom Typ fhemDevice
oder einer passenden definierten Unterklasse angelegt. Der Name der Variablen stimmt mit dem Namen des Devices überein. Existiert also ein Device WZ_Lampe, dann wird eine Variable WZ_Lampe
vom Typ fhemDevice
angelegt. Ist WZ_Lampe ein Homematic-Gerät ist WZ_Lampe
vom Typ fhemHomematicDevice
WZ_Lampe'name
liefert den Namen des Devices als ein word
-Objekt.
Eine wichtige Eigenschaft eine Objektes dieses Typs ist: readings
. Diese Eigenschaft enthält den aktuellen Zustand des Devices. Es handelt sich dabei um ein Objekt vom Typ collection of fhemReading
.
Den Wert des Readings level
von WZ_Lampe erhalte ich demzufolge durch:
l := WZ_Lampe'readings'level'val;
Da auf die Werte von Readings in der Praxis häufig zugegriffen wird, gibt es dafür unter Verwendung eckiger Klammern die alternative Schreibweise:
level := [WZ_Lampe level];
Ist der Name des Readings state
, reicht die Schreibweise:
state := [WZ_Lampe];
Danach stehen in level
der Wert des Readings level und in state
der Wert des Readings state von WZ_Lampe. Beides sind Objekte vom Typ string
.
4.1.13.1 fhemLambdaDevice
Ein fhemDevice
, das ein λscript enthält, gehört automatisch zu dieser Klasse. Es kann seine eigenen Readings setzen, so wie jedes Device seinen internen Zustand verwaltet. Das Kommando zum Setzen eines Readings lautet:
setReading [name] [wert] ([name] [wert]);
Dabei steht [name] für den Namen des Readings und [wert] für den zu setzenden Wert. [name] kann ein Symbol oder ein Objekt von Typ word
sein. Die dem ersten Parameterpaar folgende Klammer symbolisiert, das beliebig viele weitere Parameterpaare folgen können. Beispiel für das Setzen der Readings temperature und door:
setReading
temperature 12
door "closed"
;
Da der Wert eines Readings immer ein string
sein muss, wandelt das Kommando setReading
alle Werte (hier die Zahl 12) automatisch unter Verwendung der Funktion toString
in eine Zeichenkette um. (Siehe: Typumwandlungen)
Es gibt auch die Möglichkeit, Readings eines Devices dieses Typs von Außen zu setzen. Ein solches Reading wird mit dem Kommando:
userReading name;
oder
userReading name widgetOverride;
definiert. Dabei ist name
der Name des Readings und der optionale Parameter widgetOverride ein definiertes Element der grafischen Benutzeroberfläche von FHEM. (Siehe hier.) Ein so spezifiziertes Reading kann per FHEM-Kommando
set fhemLambdaDevice reading name value;
gesetzt werden. Für fhemLambdaDevice
ist hier der Name des Devices einzusetzen. Dieses Kommnado kann in der Kommandozeile von FHEM oder innerhalb eines anderen λscripts abgesetzt werden.
4.1.13.2 fhemHomematicDevice
Ein fhemDevice
das ein λscript Homematic-Gerät (TYPE: CUL_HM) verwaltet, gehört automatisch zu dieser Klasse.
Hier sind Kommandos möglich, die spezifische für diese Geräteklasse sind. Z.B. Kommandos der Form:
device'channel_??
Dieses Kommando gibt das Gerät dass dem angegebenen Channel von device
entspricht zurück. Existiert der Kanal nicht, bricht der Compiler mit einer Fehlermeldung ab. Ist TH-Bad
ein Thermostat vom Typ "HM-TC-IT-WM-W-EU" denn liefert das folgende Kommando die aktuelle Luftfeuchtigkeit aus dem Weather-Kanal des Thermostats als Zahl in die Variable humidity
.
humidity := [TH-Bad'channel_01 humidity]'toNumber;
4.1.14 astronomy
Ein Objekt von Typ astronomy
dient dazu, astronomische Daten bereitzustellen. Mit
mySun := new astronomy;
wird eine Variable mySun
angelegt. Dabei werden der Readings latitude und longitude aus dem Device global herangezogen. Die Ausdrücke
azimut := mySun'azimut;
elevation := mySun'elevation;
liefern dann denn aktuellen Sonnenstand in ° (Typ: number).
nextSunrise := mySun'sunrise;
nextSunset := mySun'sunset;
liefern die Zeitpunkte (Typ: moment) des nächsten Sonnenauf- bzw. untergangs.
Warning: filemtime(): stat failed for 10 index.php in /customers/8/7/e/lambda-script.org/httpd.www/index.php on line 874.2 Typumwandlungen
In λscript werden Objekte in der Regel nicht automatisch in andere Objektypen transformiert. Es gibt eine einzige Ausnahme: Die besteht in der Konvertierung eines Wertes vom Typ word
in den Typ string
bei einer Wertzuweisung an ein Objekt das als string
deklariert wurde. Soll heißen:
t := "Hallo Welt!";
…
t := "lambda";
In diesem Beispiel wird eine Variable t
von Typ string
initialisiert. Der soll später ein Wert von Typ word
zugewiesen werden. In diesem, und nur in diesem, Fall wird der Compiler eine automatische Typumwandlung vornehmen und den Übersetzungsprozess nicht mit einer Fehlermeldung beenden.
Für die Umwandlung eines Wertes in eine Wert eines anderen Typs sind einige Funktionen eingebaut. Für die anderen Fälle muss der User weitestgehend selbst sorgen. Für die Durchführung der Umwandlung eignen sich Kommandos der Gestalt:
new := old toClassname;
oder gleichwertig
new := old'toClassname;
old
ist hier ein Wert, ein Repräsentant einer bestimmten Klasse. Dieser Wert wird in eine Variable new
vom Typ classname
umgewandelt.
4.2.1 Umwandlung nach string
Für alle Klassen, gibt es eine Funktion toString
, die die Werte dieser Klassen in den Typ string
umwandelt. Diese Funktion wird automatisch aufgerufen, wenn ein Wert bei Ausgabekommandos (log
, setReading
, ...) ausgegeben werden soll.
n := 123;
s := n toString;
Diese Kommandos weisen der Variablen s
die Zeichenkette "123" zu.
Für neue Klassen, die der User selbst erstellt hat, wird automatisch eine entsprechende Funktion generiert. Diese vordefinierten Funktionen können durch eigene Funktionen ersetzt und so an die jeweiligen Bedürfnisse angepasst werden.
Für die Klasse number
gibt es eine Besonderheit: Sie enthält eine Eigenschaft format
von Typ string
. Dieser Eigenschaft kann ein Formatstring zugewiesen werden, mit der die Umwandlung in einen Strings beeinflusst wird. Dieser Formatstring ist der, den man in der Programmiersprache Perl zur formatierten Ausgabe von Zahlen verwendet. Für eine ausführliche Darstellung der Möglichkeiten gibt es zahlreichen Quellen (z.B. https://perldoc.perl.org/functions/sprintf.html). Wird dieser Formatstring einer Zahl zugewiesen, wird die Zahl bei der Umwandlung in einen String entsprechend formatiert:
pi := 3.14159265359;
pi'format := "%.3f";
log "gerundet: " pi;
gerundet: 3.142
pi'format := "";
log "original: " pi;
original: 3.14159265359
In diesem Beispiel wurde die Variable pi
zunächst auf 3 Dezimalstellen gerundet ausgegeben. Nach dem Löschen des Formatstrings erfolgt die Ausgabe des originalen Wertes. Die Angabe eines Formatstrings beeinflusst also nicht die interne Darstellung der Zahl, sondern wirkt sich nur bei der Umwandlung in einen String aus.
4.2.2 Umwandlung nach boolean
Neben toString
gibt es eine weitere für alle Klassen definierte Typumwandlungsfunktion: toBoolean
. Der Aufruf
y := x toBoolean;
setzt y
auf den Wert false
wenn:
- Wenn
x
vom Typnumber
ist und den Wert0
hat. - Wenn
x
vom Typstring
und leer ist. - Wenn
x
vom Typstring
ist und den Wert"false"
hat. (Die Verwendung von Großbuchstaben ist möglich. Auch"FALSE"
,"False"
, etc. werden zufalse
ausgewertet.) - Wenn
x
ein Array (also vom Typstack
,collection
odervector
) ist, und keine Elemente enthält.
In allen anderen Fällen setzt das Kommando y
auf den Wert true
.
4.2.3 Umwandlung von string nach number
In der Praxis kommt es häufig vor, dass man Zeichenketten, die Zahlen darstellen, in echte Zahlen (Werte vom Typ number
) umwandeln möchte. Dafür gibt es das Kommando toNumber
n := "123";
log n'toNumber;
123
Eine leere Zeichenkette gibt den Wert 0
zurück. Das Script bricht mit einer entsprechenden Fehlermeldung ab, wenn s
nicht die String-Darstellung einer Zahl ist. Ob das der Fall ist, lässt sich mit einer Funktion (Diese besteht aus dem string
gefolgt vom Symbol isNumber
.) prüfen:
n := "123";
log n'isNumber;
true
s := "12 m";
log s'isNumber;
false
Für den Fall, dass man nicht sicher ist, ob der String eine Zahl darstellt, möchte man vielleicht einen Standardwert zurückgeben. Das wird mit folgendem Kommando erreicht:
s := "xyz";
n := s toNumber alternatively 123;
log n;
123
4.2.4 Umwandlung von string nach timespan
Die Umwandlung von Zeichenketten, die Werte vom Typ timespan
darstellen, in Werte vom Typ timespan
ist funktioniert analog der Umwandlung in Zahlen. Das Kommando hierfür ist toTimespan
.
s := "12:30";
log s'toTimespan;
12:30,0
Eine leere Zeichenkette gibt den Wert 0,0
zurück. Das Script bricht mit einer entsprechenden Fehlermeldung ab, wenn s
nicht die String-Darstellung einer Zeitspanne ist. Ob das der Fall ist, lässt sich mit der Funktion isTimespan
prüfen:
s := "12:30";
log s'isTimespan;
true
s := "12 m";
log s'isTimespan;
false
Für den Fall, dass man nicht sicher ist, ob der String eine Zeitspanne darstellt, kann ein Standardwert zurückgeben werden. Das wird mit der Verwendung von alternatively
erreicht:
s := "12 m";
log (s toTimespan alternatively 12:30);
12.30
4.3 Definition neuer Klassen
Eine neue Klasse kann mit folgendem Kommando definiert werden:
newClass := oldClass
Eigenschaft1 Wert1
Eigenschaft2 Wert2
…
{ Initialisierungsblock }
;
newClass
ist der Name der neuen Klasse. oldClass
ist der Name der Klasse von der die neue Klasse sämtliche Eigenschaften erbt. Für eine komplett neue Klasse, ist für oldClass
value
- die Mutter aller Werteklassen - einzusetzen.
Danach folgt eine Liste der Eigenschaften, die die neue Klasse, zusätzlich zu denen von oldClass
, haben soll. Dem Namen der Eigenschaft (ein Symbol) folgt ein initialer Wert. Dieser Wert kann auch empty
sein. Sowohl durch die Angabe eines Wertes, als auch durch die Definition mit empty
ist die Klassenzugehörigkeit dieser Eigenschaft festgelegt. Damit ist z.B. Wert1
die Instanz eine bestimmten Klasse, und wie bei einer Variablen, der Typ dier Eigenschaft festgelegt. Bei der Instanziierung der neu definierten Klasse kann einer Eigenschaft nur ein Wert zugewiesen werden, der einen entsprechenden Typ besitzt.
Nach der der Angabe der Eigenschaften kann ein Block (hier als Initialisierungsblock bezeichnet) folgen. Die Angabe des Blocks ist optional. Ist er bei der Definition der Klasse angegeben worden, dann werden die Kommandos des Blocks bei jeder Instanzierung eines Objektes von newClass
abgearbeitet. Innerhalb dieses Blocks existiert eine Variable my
, die den Wert des aktuellen Objektes enthält. Wurde bei der der Definition der ElternKlasse oldClass
ebenfalls ein Initialisierungsblock definiert, wird dieser vor jedem abgearbeitet.
Ein Beispiel:
clsShutter := fhemDevice
direction number
shading 50
;
Hier wird eine neue Klasse, mit denen Rollläden gesteuert werden sollen, definiert. Die neue Klasse clsShutter
hat dann alle Eigenschaften von fhemDevice
, plus zwei Weitere, mit denen eine Beschattung gesteuert werden soll: direction
- eine Zahl die die Himmelsrichtung angibt in die das zugehörige Fenster zeigt und shading
- das Level auf das der Rollladen abgesenkt wird, wenn alle Voraussetzungen für ein notwendige Beschattung erfüllt sind. Der Wert für shading
wird auf 50 %
vordefiniert der Wert für direction
muss bei der Instanziierung für jedes Rollo mit angegeben werden. (siehe hier)
Innerhalb des Blocks, in dem eine neue Klasse definiert wurde, kann die Definition um weitere Eigenschaften ergänzt werden. Dazu werden hinter dem Klassennamen die Eigenschaftsnamen und die entsprechenden initialen Werte notiert - völlig anlog zu Definition der neuen Klasse. Auch die Angabe eines weiteren Initialisierungsblocks ist optional möglich.
newClass
Eigenschaftx Wertx
Eigenschafty Werty
…
{ Initialisierungsblock }
;
Diese Möglichkeit ist wichtig, wenn zwei Klassen definiert werden sollen, und jede eine Eigenschaft vom Typ der anderen Klasse enthalten soll. Dazu ein Beispiel:
Es soll eine Klasse clsRoom
definiert werden. Diese Klasse soll unter anderem eine Eigenschaft shutters
enthalten. Diese Eigenschaft ist eine Auflistung aller Rollos in einem Raum. Ein Raum sollen Instanz einer Klasse clsRoom
sein. Die Klasse clsShutter
soll eine Eigenschaft room
besitzen die auf den Raum verweist in dem sich das Rollo befindet. Beide Klassen "in einem Zug" definieren zu wollen, ist nicht möglich, da bei der Definition der ersten Klasse clsShutter
auf die ZweiteclsRoom
noch nicht zugegriffen werden kann. Deshalb legt man die erste Klasse zunächst ohne die noch nicht zu definierende Eigenschaft an, und ergänzt diese nach der Definition der zweiten Klasse. Für obiges Beispiel könnte das wie folgt aussehen:
clsRoom := value name string;
clsShutter := fhemDevice room clsRoom direction number shading 50;
clsRoom shutters (stack of clsShutter);
In der ersten wird die Klasse clsRaum
, nur mit der Eigenschaft name
definiert. Die zweite Zeile enthält die Definition der Klasse clsShutter
analog oben und der zusätzlichen Eigenschaft room
. Jetzt, da dem System die Klasse clsShutter
bekannt ist, kann diese bei der Erweiterung der Klasse clsRoom
verwendet werden. Dieser Klasse wird jetzt eine Eigenschaft shutters
zugefügt, die für einen Stapel von Werten des Typs clsShutter
steht.
4.4 Instanziierung von Klassen
Werte sind Objekte die von Klassen abgeleitet werden. Dieser Prozess heißt Instanziierung. Charakteristisch dafür ist die Verwendung des Schlüsselwortes new
. Hier gibt es mehrere Möglichkeiten:
4.4.1 Explizite Instanziierung
new cls p1 v1 p2 v2 …;
cls
ist der Name der Klasse von der eine Instanz gezogen werden soll. pn
und vn
sind ein Symbol und ein Wert. Beide definieren die entsprechende Eigenschaft des neu zu kreierenden Objektes. Wichtig ist, dass die Eigenschaften, die in der Klassendefinition nicht mit einem Standardwert belegt sind, in dem new
-Kommando festgelegt werden.
Hier gibt es eine Ausnahme: Ist eine Eigenschaft p
ein Array (stack
, collection
oder vector
) und wird diese Eigenschaft nicht explizit durch ein entsprechendes v
festgelegt, dann wird während der Instanziierung für p ein leeres Array des vorgesehenen Typs erzeugt. Beispiel:
cls := value names (stack of string);
var := new cls;
Die Klasse enthält eine Eigenschaft names
das aus einem Stack mit Werten vom Typ string
bestehen soll. Die Variable var
enthält dann nach ihrer Erzeugung einen entsprechenden leeren Stack.
4.4.2 Erweiterte explizite Instanziierung
new cls var p1 v1 p2 v2 …;
Auch hier ist cls
der Name der Klasse von der das Objekt abgeleitet werden soll. Auch das eben zu den pn
und vn
Gesagte gilt analog. Die Variable var
steht für eine Variable, die eine Instanz der Klasse cls
oder einer ihrer Vorfahren ist. Dann werden zunächst alle Eigenschaften von var
auf das neue Objekt übertragen und danach die Paare pn
und vn
an das Objekt übergeben.
4.4.3 Instanziierung von Klassen
Werte sind Objekte die von Klassen abgeleitet werden. Dieser Prozess heißt Instanziierung. Charakteristisch dafür ist die Verwendung des Schlüsselwortes new
. Hier gibt es mehrere Möglichkeiten:
- Explizite Instanziierung
new cls p1 v1 p2 v2 …;
cls
ist der Name der Klasse von der eine Instanz gezogen werden soll.pn
undvn
sind ein Symbol und ein Wert. Beide definieren die entsprechende Eigenschaft des neu zu kreierenden Objektes. Wichtig ist, dass die Eigenschaften, die in der Klassendefinition nicht mit einem Standardwert belegt sind, in demnew
-Kommando festgelegt werden.
Hier gibt es eine Ausnahme: Ist eine Eigenschaftp
ein Array (stack
,collection
odervector
) und wird diese Eigenschaft nicht explizit durch ein entsprechendesv
festgelegt, dann wird während der Instanziierung für p ein leeres Array des vorgesehenen Typs erzeugt. Beispiel:
Die Klasse enthält eine Eigenschaftcls := value names (stack of string);
var := new cls;names
das aus einem Stack mit Werten vom Typstring
bestehen soll. Die Variablevar
enthält dann nach ihrer Erzeugung einen entsprechenden leeren Stack.
- Implizite Instanziierung durch die Verwendung eines Wertes
Auch hier ist
new cls var p1 v1 p2 v2 …;
cls
der Name der Klasse von der das Objekt abgeleitet werden soll. Auch das eben zu denpn
undvn
Gesagte gilt analog. Die Variablevar
steht für eine Variable, die eine Instanz der Klasseclass
oder einer ihrer Vorfahren ist. Dann werden zunächst alle Eigenschaften vonvar
auf das neue Objekt übertragen und danach die Paarepn
undvn
an das Objekt übergeben.
5 Variable
Variable werden definiert, indem man einem Symbol einen Wert zuweist. (Z.B. durch das Kommando: zahl := 123;
) Das Symbol ist dann, bildlich ausgedrückt, der Name dieses Wertes. In λscript sind Variable statisch typisiert. D.h.: Jeder Wert ist Instanz einer Klasse. Einer Variablen kann als neuer Wert grundsätzlich nur eine andere Instanz der gleichen Klasse zugewiesen werden, mit der sie definiert wurde.
Die Kommandofolge:
zahl := 123;
…
zahl := "Hallo";
würde vom Compiler mit einer Fehlermeldung quittiert und die Überprüfung des Scriptes abgebrochen werden.
5.1 Direkte Definition einer Variablen
Für einige Klassen (number
, boolean
, string
, word
, timespan
) ist die Definition einer Variablen durch ein bestimmtes Zeichenmuster im Script möglich:
zahl := 123;
text := "Hallo Welt";
wort := "Hi";
pause := 1:30,45;
offen? := false;
Das ist weitgehend selbsterklärend. Die Definition von Ausdrücken des Typs timespan
ist hier beschrieben. Genau so einfach ist die Definition einer Variablen indem man ihr den Wert einer anderen Variablen oder eines Ausdrucks zuweist.
zahl := 3 + 12 ^ 2;
text := "Ergebnis: " zahl'toString;
wort := 5 * "Hi" ;
uhrzeit := now - today;
offen? := 3 > 5;
5.2 Variable sind Zeiger
Das Prinzip der Speicherung von Objekten in einer Variablen ist im Grunde simpel: Bei der Zuweisung eines Wertes an ein Symbol wird ein leere Speicherzelle gesucht. An dieser Stelle wird das Objekt gespeichert und die Position der Speicherzelle im Symbol abgelegt. Dadurch wird das Symbol zu einer Variablen.
Ein Beispiel: Bei der Initialisierung des Symbols x
mit dem Wert "Hallo"
wird dem Symbol x
ein Speicherplatz zugewiesen. Hier zum Beispiel der Platz mit der Nummer 178.
x := "Hallo";
Schematisch stellt sich die Situation so dar:
Variable | Position | Inhalt |
---|---|---|
… | 177 | … |
x | 178 | Hallo |
… | 179 | … |
Wird der Variablen danach ein neues Objekt zugewiesen wird dieses Objekt an die Stelle geschrieben auf die die Variable zeigt. Genauer betrachtet liegen in diesem Konzept einige bedenkenswerte Sonderfälle versteckt. Die treten auf, wenn die Objekte, die einer Variablen zugewiesen werden sollen, Eigenschaften von anderen Objekten oder Werte anderen Variablen sind.
5.2.1 Zuweisung einer Variablen an eine Andere
Wird dem Symbol y
der Wert von x
mit y := x;
zugewiesen muss y danach auf eine andere Speicherposition als x weisen, und auf der Speicherposition von y muss der gleiche Wert wie bei x stehen.
x := "Hallo";
y := x;
Variable | Position | Inhalt |
---|---|---|
… | 177 | … |
x | 178 | Hallo |
y | 179 | Hallo |
… | 180 | … |
Das bedeutet, dass bei der Zuweisung einer Variablen x
an eine Variable y
ein Kopie des Wertes von x
erstellt und diese dann y
zugeordnet werden muss. Anderenfalls würden sowohl x
als auch y
auf den Speicherplatz 178 verweisen. Das hätte zur Folge, das jede Änderung von x
in gleichem Maß auch y
betreffen würde.
Dieses Verhalten ist im Allgemeinen nicht erwünscht. Deshalb wird in λscript bei der Zuweisung einer Variablen an eine Variable eine Objektkopie erstellt und diese mit der Zielvariablen verknüpft. Um den Kopieraufwand zu minimieren, wird das Kopieren eines Objektes als sogenannte flache Kopie realisiert. Erst wenn ein Schreibzugriff auf eine kopierte Eigenschaft stattfindet, wird die Kopie dieser Eigenschaft vollzogen. Dadurch wird sichergestellt, dass diese Änderung nur die Kopie erreicht und gleichzeitig werden unnötige Kopiervorgänge vermieden.
Ohne diese Vorgehensweise würde die Kommandofolge:
x := "Hallo";
y := x;
x := "Welt";
log y;
Hallo
log x;
Welt
dazu führen, dass beide Variable den Wert Welt
hätten.
5.2.2 Zuweisung der Eigenschaft eines Objeks an eine Variable
Im Abschnit Umwandlung nach string wird erläutert, das Werte vom Typ number
eine Eigenschaft format
besitzen mit der die Formatierung bei der Ausgabe der Zahlen gesteuert wird. Standardmäßig ist dieser String leer. Im nachfolgenden Beispiel wird diese Eigenschaft (von Typ string
) an eine Variable s
übergeben, geändert und die Zahl ausgegeben. Ziel ist, die Zahl auf drei Nachkommastellen zu runden:
pi := 3.14159265359;
s := pi'format;
s := "%.3f";
log "gerundet: " pi;
3.14159265359
Das Script zeigt nicht das erwartete Verhalten: pi wird nicht gerundet auf 3 Nachkommastellen ausgegeben. Nach dem oben Ausgeführten ist das Ergebnis erklärbar. pi'format
und s
zeigen nicht auf die selbe Speicherzelle. Deshalb wirkt sich die Änderung von s
im Kommando s := "%.3f"
nicht auf die Eigenschaft format
in pi
aus. Die schematische Speicherkonfiguration sieht also so aus:
Variable | Position | Inhalt |
---|---|---|
… | 212 | … |
pi | 213 | 3.14159265359… |
p'format | 214 | |
s | 215 | %.3f |
… | 216 | … |
Um zu Erreichen, dass das Script wie gewünscht funktioniert, muss erzwungen werden, dass s
auf den selben Speicherplatz zeigt wie pi'format
. In λscript gibt es dafür den Operator ==
. Der bewirkt, das das links von ihm stehende Symbol zu einer Variablen wird, die auf den rechts stehenden Wert zeigt. Die beiden Terme sind praktisch, bis auf den Namen, identisch — mehr als nur gleich. Mit der Verwendung dieses Operators wird das erwartete Resultat erzielt:
pi := 3.14159265359;
s == pi'format;
s := "%.3f";
log "gerundet: " pi;
3.142
Jetzt sieht die schematische Speicherkonfiguration so aus:
Variable | Position | Inhalt |
---|---|---|
… | 212 | … |
pi | 213 | 3.14159265359… |
s, p'format | 214 | %.3f |
… | 215 | … |
5.3 Neudefinition durch implizite Instanziierung
Das Kommando dafür lautet:
new var cls p1 v1 p2 v2 …;
Diese Variante unterscheidet sich syntaxtisch leicht von der erweiterten expliziten Definition eines Objektes. Vor dem Klassennamen steht eine Variable. Hier ist es die Variable var
selbst, die zu einer Instanz der Klasse cls
angehoben, und um die in den pn
und vn
bezeichneten Eigenschaften erweitert wird. Zu beachten ist hier, dass die Variable var
ihren Typ ändert. Das führt nur dann nicht zu einem Konflikt, wenn var
ursprünglich in einem übergeordneten Block definiert wurde. Dazu ein Beispiel:
Weiter oben wurde ein Klasse clsShutter
definiert. Existiert in unserer FHEM-Installation ein Device mit dem Namen WZ_Rollo so gibt es automatisch auch eine Variable WZ_Rollo
vom Typ fhemDevice
. Das Kommando
new WZ_Rollo clsShutter direction 45;
macht aus einem einfachen WZ_Rollo
vom Typ fhemDevice
ein Objekt von Typ clsShutter
. Das führt nicht zu einem Widerspruch bezüglich der statischen Typisierung, weil die Variablen, die die Devices der FHEM-Installation abbilden, im übergeordneten Superblock, der Block in dem das gesamte Script eingebettet ist, definiert werden.
5.4 Definition durch Zuweisung eines Funktionsaufrufs
Eine Besonderheit der funktionalen Programmierung ist, das Variable für einen Funktionsaufruf stehen können. Das geschieht in der Weise, dass einem Symbol ein Funktionsaufruf zugewiesen wird. Diese Variable kann, wie jede andere Variable, in Kommandos verwendet werden. Mit dieser Variablen kann gerechnet und sie kann mit anderen Variablen verglichen werden. Sie kann auch als Parameter in Funktionsaufrufen dienen. Von außen ist ihr nicht anzusehen, dass sie keinen festen Wert hat. Einen festen Wert bekommt sie erst dann, wenn er für eine Kommunikation mit der Hardwareperipherie des Systems benötigt wird. Z.B. wenn der Wert für die Ansteuerung eines Devices oder eine Ausgabe in das Protokoll benötigt wird.
Das folgende Beispiel soll das demonstrieren:
x := '(random 10 20);
log x;
log x;
log x;
Das Kommando random 10 20;
ruft ein Funktion auf, die eine ganzzahlige Zufallszahl zwischen 10 und 20 liefert. Dieser Funktionsaufruf wird dem Symbol x
zugewiesen. (Das Apostroph verhindert die sofortige Ausführung des Kommandos. Genau so, wie ein Symbol ein Symbol bleibt wenn ihm ein Apostroph vorangestellt ist, verhindert ein Apostroph vor der Liste die Abarbeitung des in der Liste stehenden Kommandos.) Damit wird x
zu einer Variablen vom Typ number
- allerdings ohne einen festen Wert. Der in x
gespeicherte Funktionsaufruf wird bei jeder Verwendung der Variablen x
ausgeführt. In unserem Beispiel ist die Funktion random 10 20;
nicht deterministisch. Sie hat einen undefinierten Rückgabewert. Deshalb sind die Ausgaben von log x;
in der Regel voneinander verschieden.
Die Auswertung der Funktion kann durch das Kommando
var evaluate;
erzwungen werden. Ist var
eine Variable, der ein Funktionsaufruf zugewiesen wurde, oder ist var
ein berechneter Wert, in dem mindestens ein unaufgelöster Funktionsaufruf steckt, wird die Berechnung jetzt durchgeführt.
Die Stärke dieses Konzeptes zeigt sich besonders beim Einsatz in einer Prozesssteuerung. Hier ist charakteristisch, das Variable von den Zuständen äußerer Parameter abhängen und der aktuelle Wert sich mit der Zeit ändert. Trotzdem kann mit diesen noch unbekannten Werten "normal" gerechnet und das Ergebnis zu dem Zeitpunkt wenn es benötigt wird, konkretisiert werden. Dass soll nachfolgend an einem Beispiel erklärt werden:
Angenommen es gibt im System ein Device mit dem Namen LightSensor
. Das Reading state
dieses Sensors hat einen Wert zwischen 0 und 255, der mit der Umgebungshelligkeit des Sensors korreliert. 0 steht für absolut dunkel und 255 für helles Sonnenlicht. Ziel soll es sein, eine Device lamp
einzuschalten wenn der Wert kleiner als 40 und ansonsten auszuschalten.
Diese Funktionalität soll in einer Funktion gekapselt werden, da sie für eine Vielzahl von Kombinationen von Helligkeitssensoren, Lampen und Schwellwerten benötigt wird.
Die zu definierende Funktion soll mit einem Kommando der Gestalt
lamp on if brightness < threshold;
aufgerufen werden können. Dabei sind lamp
, brightness
und threshold
die Parameter des Aufrufs. Die Definition einer solchen Funktion wäre z.B. (siehe selbstdefinierte Funktionen):
'(fhemDevice'lamp on if number'brightness < number'threshold) {
repeat {
state := if (brightness < threshold) "on" "off";
set lamp state;
wait brightness;
};
};
Die erste Zeile enthält die Signatur aus den Symbolen on
, if
und <
sowie den Parametern lamp
von Typ fhemDevice
und brightnes
und threshold
vom Typ number
. Mit dem Symbol repeat
wird eine Wiederholschleife gestartet. In dem Wiederholblock stehen zwei Kommandos. Das Erste state := if (brightness < threshold) "on" "off";
berechnet aus den Parameter brightness
und threshold
den gewünschten Schaltzustand der Lampe - "on"
oder off
. Dieser Zustand ist aber, wenn brightness
oder threshold
keine festen Werte darstellen, sondern Funktionsaufrufe sind, ebenfalls unbestimmt. Erst mit dem nächsten Kommando set lamp state;
, mit dem die Hardware angesprochen wird, werden die hinterlegten Funktionen aufgerufen, die Berechnungen durchgeführt und das Ergebnis an das Device übergeben. Daran schließt sich ein wait
-Kommando an, mit dem auf eine Veränderung des Parameters brightness
gewartet wird. Steckt hinter brightness
kein Funktionsaufruf sondern ein fester Wert ist der Wartebefehl natürlich wirkungslos. (Ein übergebener konstanter Wert ändert sich ja nicht.)
5.5 Gültigkeitsbereich
Auf eine Variable kann in dem Block, in dem sie definiert wurde, und in allen untergeordneten Kommandoblöcken zugegriffen werden. Überall wo der Name der Variablen in Ausdrücken auftaucht, wird er durch den Wert der Variablen ersetzt.
Nach der Abarbeitung der Anweisungen:
a := 3;
b := 5;
c := a * (8 - b);
hat die Variable c den Wert 9
.
Es gibt Situationen in denen dieses Verhalten nicht gewünscht ist. Dann soll das Symbol als ein solches in einem Kommando stehen bleiben. Das ist z. B. der Fall, wenn ich in einem untergeordneten Kommandoblock einer Variablen lokal einen anderen Wert zuweisen (auch eines anderen Typs) zuweisen möchte:
a := 3;
...
{
a := "Hi!";
...
}
Die Zeile a := "Hi!";
ließt sich wie folgt: Weise der Variablen a
, die jetzt den Wert 3
hat den Wert "Hi!"
zu. Das steht zum einen im Widerspruch zur statischen Typisierung (und führt zu einer Fehlermeldung) und zum anderen hätte die Variable a
nach dem Verlassen des inneren Blockes einen geänderten Wert. Der Weg um das zu Verhindern ist, ein Apostroph vor das Symbol zu setzen:
a := 3;
...
{
'a := "Hi!";
...
}
Ein vorangestelltes Apostroph verhindert, das 'a
als Variable a
interpretiert wird, sondern es bleibt das Symbol a
. Dann ließt sie die Zeile als: Definiere eine Variable a
mit den Wert "Hi!"
. Diese Variable "verschwindet" mit dem Verlassen des inneren Blocks. Danach hat die Variablen a
, wieder den Wert 3
.
Um einen Block zu kapseln, d.h. um zu Verhindern, dass in einem Block auf Variable zugegriffen werden kann, die in übergeordneten Blöcken definiert wurden, dient das Kommando: encapsulate {... block ...};
. Auf in übergeordneten Blöcken definierte Klassen, Funktionen und Devices kann weiterhin zugegriffen werden. Soll auf bestimmte Variable in {... block ...}
verfügbar gemacht werden, müssen diese nach dem Symbol encapsulate
notiert werden:
a := 3;
b := "Hallo Welt!";
...
encapsulate a {
a := 3 * a;
b := a * a;
log b;
81
...
};
log b;
Hallo Welt!
Die Variable a
wird nach "unten" durchgereicht und die Variable b
im gekapselten Block ist unabhängig von der Variablen b
in übergeordneten Block.
6 Kommandos und Funktionen
Er gibt keinen prinzipiellen Unterschied zwischen Funktionen die in λscript vordefiniert sind und selbst definierten Funktionen. Vordefinierte Funktionen können durch Eigene ersetzt werden. Selbst definierte Funktion fügen sich harmonisch in die Sprache ein. Das wichtigste Prinzip aber ist: Funktionen haben Inputs und Outputs, aber keine Nebenwirkungen! Sie bearbeiten keine Daten, die ihnen nicht explizit übergeben wurden. Die Kommandos aus denen eine Funktion besteht, bilden zwar einen Block, aber es ist nicht möglich auf Variable die außerhalb des Block definiert wurden zuzugreifen. (Ein Ausnahme bilden Devices. Auf die umgebende Hardware kann immer lesend und steuernd zugegriffen werden.) Sehr wohl können aber in einer Funktion alle in übergeordneten Blöcken definierten Klassen und Funktionen verwendet werden.
6.1 Signatur eines Kommandos
Funktionen werden durch Kommandos aufgerufen. Jedes Kommando ist eine Folge von Werten und Symbolen. Werte sind Instanzen von Klassen und damit kann jedem Kommando eindeutig eine Folge von Klassen und Symbolen zugeordnet werden. Diese Folge nennt man die Signatur eines Kommandos. Ein Beispiel:
(12 + 18)
Diese Kommando hat die Signatur 'number + number'. Stößt λscript auf ein Kommando, sucht es im aktuellen Block, ob für diese Signatur eine Funktion gefunden wird. Wird keine gefunden, wird im übergeordneten usw. gesucht. Wenn eine Funktion gefunden wird, dann wird sie mit den aktuellen Werten, hier 12
und 18
, aufgerufen. Wird keine Funktion gefunden, beendet der Compiler seine Arbeit mit einer Fehlermeldung. In diesem Fall aber, wird das Ergebnis 30
zurückgegeben.
6.2 Definition eigener Kommandos
Eine neue Funktion wird in dem Block definiert, in dem sie gelten soll. (Ihr Gültigkeitsbereich umfasst auch alle untergeordneten Blöcke.) Dazu wird zum einen die sie beschreibende Signatur, und zum anderen der Code der sie am Ende ausmacht, in einem Definitionsblock notiert.
Ersteres, die Signatur, wird in runden Klammer eingefasst. Damit λscript diese Liste nicht als ein Kommando interpretiert, wird ihr ein Apostroph '
vorangestellt. Wie, wenn einem Symbol ein Apostroph vorangestellt wird, verhindert es die Abarbeitung und die Liste bleibt unverändert. Die Klassennamen werden noch um den symbolischen Parameternamen, ebenfalls unter Benutzung von '
erweitert. Daran schließt sich der Definitionsblock mit den Kommandos an. Der Rückgabewert einer Funktion ist das Ergebnis der letzten Operation im Definitionsblock. Zur Verdeutlichung ein simples Beispiel:
Es soll eine Funktion geschrieben werden, die eine Zahle quadriert, also mit sich selbst multipliziert. Wenn a
die zu quadrierende Zahl ist, soll der Funktionsaufruf a'qrt
oder was identisch ist (vgl. Anmerkung um Zugriffsoperator) (a qrt)
sein. Die Signatur ist also die Klasse number
und das Symbol qrt
. Das folgende Kommando definiert die Funktion:
'(number'x qrt) := {x x};
Die gequotete Liste enthält die Signatur: Die Klasse number
mit dem symbolischen Parameter x
gefolgt von dem Symbol qrt
. Der symbolische Parameter ist frei wählbar. Daran schließt sich der Deklarationsblock {x x}
mit den Kommandos an. Hier ist es nur ein Kommando, und weil das Multiplikationszeichen zwischen zwei Zahlen ist optional ist, wurde es weggelassen.
Diese Funktion kann nun im nachfolgenden Script eingesetzt werden. Man kann das Ergebnis anderen Variablen zuweisen oder in anderen Ausdrücken verwenden.
'(number'x qrt) := {x x};
log 25'qrt;
625
a := 3;
b := 4;
c := 5;
log (a'qrt + b'qrt - c'qrt);
0
log 2'qrt'qrt'qrt;
256
Diese Funktion kann auch in Weiteren neu definierten Funktionen verwendet werden. Nachfolgend wird eine Funktion isRrightTriangle
definiert, die überprüft, ob die drei übergebenen Parameter die Seitenlängen eines rechtwinkligen Dreiecks sind. Der Rückgabewert ist vom Typ boolean
.
'(number'x qrt) := {x x};
'(isRrightTriangle number'a number'b number'c) := {
(a'qrt + b'qrt) = c'qrt
};
log (isRrightTriangle 3 4 5) ;
true
log (isRrightTriangle 56 90 105);
false
log (isRrightTriangle 68 285 293);
true
Wird die Funktion '(number qrt)
im Deklarationsblock der zweiten Funktion '(isRrightTriangle number number number)
definiert, funktioniert sie nur dort und ist von außen nicht sichtbar.
'(isRrightTriangle number'a number'b number'c) := {
'(number'x qrt) := {x x};
(a'qrt + b'qrt) = c'qrt
};
log (isRrightTriangle 3 4 5);
true
log (isRrightTriangle 56 90 105);
false
log (isRrightTriangle 68 285 293);
true
6.3 Vordefinierte Kommandos
6.3.1 Kommandos mit einem Element
Besteht ein Kommando nur aus einer Variablen, dann ist Wert dieser Variablen der Rückgabewert des Kommandos.
In der folgenden Tabelle sind die Kommandos aufgelistet, die nur aus einem Symbol bestehen. Hat ein Kommando einen Ergebniswert, dann kann dieses Kommandosymbol, ohne es explizit in Klammern zu setzen, in Kommandos verwendet werden. Beispiel:
m := today 12:00;
log m;
23.1.2020 12:00
Das Symbol today
in der ersten Zeile wird als Kommando interpretiert, ausgeführt und zu dem Ergebnis werden 12 Stunden addiert.
Symbol | Ergebnis | Bemerkung |
---|---|---|
now | moment | Rückgabe der aktuellen Systemzeit |
second | moment | Rückgabe der aktuellen Systemzeit, auf volle Sekunden abgerundet |
minute | moment | Zeitpunkt des Beginns der aktuell laufenden Minute |
hour | moment | Zeitpunkt des Beginns der aktuell laufenden Stunde |
day | moment | Zeitpunkt des Beginns des aktuell laufenden Tages |
today | moment | analog day |
tomorrow | moment | Zeitpunkt des Beginns des kommenden Tages |
yesterday | moment | Der Zeitpunkt des Beginns des vorigen Tages |
week | moment | Zeitpunkt des Beginns der aktuell laufenden Woche (letzter Montag 0:00 Uhr) |
month | moment | Zeitpunkt des Beginns des aktuell laufenden Monats |
year | moment | Zeitpunkt des Beginns des aktuell laufenden Monats |
easter | moment | Der Zeitpunkt des Beginns des Ostersonntags im laufenden Jahr |
loop | Der aktuell laufende Schleifenabarbeitung (forEach ... ) wird beendet und mit dem nächsten Durchlauf begonnen. | |
quit | Die aktuell laufende Schleife (forEach ... ) wird verlassen und das Script mit dem der Schleife folgenden Kommando fortgesetzt. | |
terminate | Der aktuell laufende Thread wird sofort beendet. | |
stop | Das λscript wird beendet. |
6.3.2 Kommandos mit zwei Elementen
Besteht ein Kommando nur aus zwei Werten, dann handelt es sich um eine zweistellige Verknüpfungsrelation in der das Operationssymbol optional ist. Diese Operationen sind unter Zweistellige Verknüpfungsoperationen aufgelistet.
Die nachstehende Tabelle Listet eine Auswahl an Kommandos auf, die aus einem Wert (Instanz einer bestimmten Klasse) und einem Symbol bestehen. Manchmal ist die ausgeführte Operation unabhängig von der Reihenfolge der beiden Operanden. Dann bietet die Version mit dem hinten stehenden Symbol den Vorteil, dass sie unter Verwendung des Zugriffsoperators '
kürzer geschrieben werden kann.
Kommando | Ergebnis | Bemerkung |
---|---|---|
not boolean | boolean | Negation: Der Ergebnisausdruck ist genau dann true , wenn der boolsche Wert false ist. |
boolean not | ||
! boolean | ||
boolean ! | ||
- number | number | Zahl mit umgekehrtem Vorzeichen. |
number + | number | Zur Zahl wird 1 addiert. |
number - | number | Von der Zahl wird 1 subtrahiert. |
value toString | string | Umwandlung eines Wertes in einen Wert vom Typ string . Siehe Umwandlung nach string |
value toBoolean | boolean | Umwandlung eines Wertes in einen Wert vom Typ boolean . Siehe Umwandlung nach boolean |
count collection | number | Anzahl der Elemente des Arrays vom Typ collection , stack oder vector . |
collection count | ||
stack count | ||
count stack | ||
vector count | ||
count vector | ||
number seconds | timespan | Zeitspanne mit der Länge der angegebenen Anzahl von Sekunden. |
number minutes | timespan | Zeitspanne mit der Länge der angegebenen Anzahl von Minuten. |
number hours | timespan | Zeitspanne mit der Länge der angegebenen Anzahl von Stunden. |
number days | timespan | Zeitspanne mit der Länge der angegebenen Anzahl von Tagen. |
number weeks | timespan | Zeitspanne mit der Länge der angegebenen Anzahl von Wochen. |
number months | timespan | Zeitspanne mit der Länge der angegebenen Anzahl von Monaten. Die Zahl muss eine ganze Zahl sein. |
number years | timespan | Zeitspanne mit der Länge der angegebenen Anzahl von Jahren. Die Zahl muss eine ganze Zahl sein. |
6.3.3 Zweistellige Verknüpfungsoperationen
Mit Objekten vom Typ number
kann in der üblichen Weise gerechnet werden. Verwendet werden können die Grundrechenarten + - * / ^
. Das Multiplikationszeichen kann, wie in der Mathematik üblich, auch weggelassen werden. Das Symbol ^
zwischen zwei Zahlen potenziert die erste Zahl. Die zweite Zahl, der Exponent, muss eine ganze Zahl sein. Es gelten die üblichen Vorrangregeln. Diese Reihenfolge kann mit Hilfe von Paaren runder Klammern ( )
verändert werden. Eine Division durch 0
oder der Versuch der Potenzierung mit einer gebrochenen Zahl führen zu einem definierten Programmabbruch. Beispiele:
a := 3 + 5 * 8;
a := 3 + 5 8;
Ergebnis ist in beiden Fällen 43
c := 3 ^ 2 + 4 ^ 2;
Ergebnis: 25
a := (3 + 5) * 8;
a := (3 + 5) 8;
Ergebnis ist in beiden Fällen 64
Es gibt eine Vielzahl von Operationen bei denen zwei Operanden (Op1 und Op2) mit Hilfe eines dazwischen stehenden Symbols (Smbl) miteinander verknüpft werden. Diese so genannten binären Operationen werden häufig, wie bei den oben erwähnten arithmetischen Operationen, einfach hintereinander geschrieben. Die Reihenfolge mit der die Operanden verknüpft werden, hängt von der Priorität ab, die dem Verknüpfungssymbol zugeordnet ist. Zuerst werden die Operanden verknüpft die das Symbol mit der höchsten Priorität verbindet. Folgen Symbole mit der gleichen Priorität aufeinander (nur durch einen Operanden getrennt) werden die Operationen von links nach rechts ausgeführt.
Die nachfolgende Tabelle verschafft einen Überblick über die möglichen Verknüpfungsoperationen. Sie ist nach der Priorität der definierenden Symbol geordnet. Das Symbol mit der niedrigsten Priorität steht ganz oben. In den Spalten OP 1 und OP 2 sind die Klasse der der Operand angehören muss notiert. Dort kann auch einen von diesen Klasse abgeleitete Klasse stehen. (Wo string
steht, könnte auch word
stehen, und wo number
steht könnte auch measurement
stehen, da diese Klassen von jenen abgeleitet sind. Der Eintrag value
bedeutet, da alle Werte Abkömmlinge der Klasse value
sind, dass hier ein beliebiger Wert stehen kann.
Smbl | Op1 | Bemerkung |
---|---|---|
Op2 | ||
Ergebnis | ||
or | | boolean | oder- Verknüpfung. Der Ergebnisausdruck ist genau dann true , wenn einer der beiden Operanden true ist. |
➩ | ||
➩ | ||
and & | boolean | und - Verknüpfung. Der Ergebnisausdruck ist genau dann true , wenn beiden Operanden true sind. |
➩ | ||
➩ | ||
= | value | Test auf Gleichheit. Objekte sind gleich, wenn sie der selben Klasse angehören und in allen Eigenschaften übereinstimmen. Gehören die Operanden unterschiedlichen Klassen an, beendet der Compiler das Programm mit einer Fehlermeldung. |
➩ | ||
boolean | ||
!= | value | Test auf Ungleichheit. Objekte sind ungleich, wenn sie der selben Klasse angehören und in mindestens einem Merkmal nicht übereinstimmen. Gehören die Operanden unterschiedlichen Klassen an, beendet der Compiler das Programm mit einer Fehlermeldung. |
➩ | ||
boolean | ||
<= < > => <=> <> | value | Vergleich von Objekten des Typs number, moment, string und aller Instanzen von Klassen, über denen die Operation <= definiert wurde. (Siehe auch hier: Vergleichsoperationen) Strings werden lexikografisch verglichen. Der erste Zeitpunkt ist kleiner als der Zweite, wenn er vor diesem liegt. |
➩ | ||
boolean | ||
~ | string | Die Zeichenkette wird darauf getestet ob der Wert des regExpr auf sie passt. |
regExpr | ||
boolean | ||
!~ | string | Die Zeichenkette wird darauf getestet ob der Wert des regExpr nicht auf sie passt. |
regExpr | ||
boolean | ||
+ | number | Zahlen werden addiert. |
➩ | ||
➩ | ||
+ | string | Zeichenketten zusammengefügt. Das Symbol + ist optional. |
➩ | ||
➩ | ||
+ | timespan | Die Zeitspannen werden addiert. Das Symbol + ist optional. |
➩ | ||
➩ | ||
+ | timespan | Das Ergebnis ist der Zeitpunkt der gegenüber moment um timespan versetzt ist. Das Symbol + ist optional. |
moment | ||
moment | ||
+ | moment | Das Ergebnis ist der Zeitpunkt der gegenüber moment um timespan versetzt ist. Das Symbol + ist optional. |
timespan | ||
moment | ||
- | number | Zahlen werden subtrahiert. |
➩ | ||
➩ | ||
- | moment | Berechnung der Zeitspanne zwischen zwei Zeitpunkten. |
moment | ||
timespan | ||
- | moment | Berechnung eines Zeitpunktes der um den Wert von timespan vor moment liegt. |
timespan | ||
moment | ||
* | number | Multiplikation von Zahlen. Das Symbol * ist optional. |
➩ | ||
➩ | ||
* | number | Vervielfachung eines Strings. number muss ganzzahlig und 0 oder größer sein. |
string | ||
➩ | ||
/ | number | Division von Zahlen. Der Versuch einer Division durch 0 führt zu einem Programmabbruch. |
➩ | ||
➩ | ||
^ | number | Potenzierung mit einem ganzzahligen Exponenten. Ist der zweite Operand nicht ganzzahlig, oder sind beide Operanden gleich 0 wird das Programm mit einer Fehlermeldung abgebrochen. |
➩ | ||
➩ |
6.3.4 Operationen über bestimmte Klassen
6.3.4.1 boolean
Hier werden einerseits die Kommandos aufgeführt, die logische Werte als Ergebnis haben. Andererseits die Kommandos, die logische Werte als Ausgangspunkt benutzen. Eine wichtige Gruppe von Funktionen mit einem boolschen Wert als Ergebnis sind Vergleichsoperationen.
Warning: filemtime(): stat failed for 10 index.php in /customers/8/7/e/lambda-script.org/httpd.www/index.php on line 876.3.4.1.1 Vergleichsoperationen
6.3.4.1.1.1 Test auf Gleichheit von Objekten
Zum Vergleich von Objekten werden die Symbole =
und !=
verwendet. Das Ergebnis eines solchen Vergleiches ist immer von Typ boolean
. Die Bedeutung der Symbole bei Zahlen liegt auf der Hand. Die Operationen für =
und !=
sind für alle Klassen, auch für vom Nutzer angelegte, automatisch definiert.
Zwei Objekte sind gleich, wenn sie der selben Klasse entstammen und alle Eigenschaften, die die Objekte ausmachen, paarweise gleich sind. In diesem Fall ist das Ergebnis des Vergleichs true
. Stammen zwei Objekte aus unterschiedlichen Klassen oder unterscheiden sie sich in mindestens einem Merkmal, sind sie ungleich, und das Ergebnis ist der Wert false
.
6.3.4.1.1.2 Ordnen von Objekten
Über den Objekten einiger Klassen ist die Operation <=
definiert. Z.B. für Zahlen (number measurement
), Zeichenketten (string word
) und Zeitpunkten (moment
). Hier ist die Bedeutung des Symbols <=
unmittelbar einleuchtend. Diese Operation definiert eine Ordnung über die Objekte.
Es kann sehr hilfreich sein, eine solche Funktion auch über andere Klassen zu definieren.
Die Operation mit dem Symbol <=
muss, in der Sprache der mathematischen Algebra gesprochen, eine "totale Quasiordnung" sein. Dazu muss bei der Definition sicher gestellt werden, dass für alle möglichen Objekte x
, y
und z
einer Klasse, folgende Bedingungen gelten:
- Totalität
- Der Vergleich
x <= y
muss immer einen boolschen Wert (true
oderfalse
) liefern. - Reflexivität
- Für Objekte
x
undy
, die gleich im Sinne des vorhergehenden Abschnitts sind, mussx <= y
den Werttrue
ergeben. - Transitivität
- Für alle Objekt
x
,y
undz
der Klasse muss gelten: Liefern die Vergleichex <= y
undy <= z
den Werttrue
, muss auch der Vergleichx <= z
den Werttrue
ergeben.
Wurde die Operation <=
definiert, existieren automatisch auch die Operationen mit den Symbolen <
, =>
und >
und müssen nicht explizit definiert werden. Diese sind dann wie folgt definiert:
x < y
ist genau dann wahr, wenny <= x
falsch ist.x => y
ist genau dann wahr, wenny <= x
wahr ist.x > y
ist genau dann wahr, wennx <= y
falsch ist.
Außerdem sind zwei weitere Operationen definiert. Dazu führt folgende Überlegung: Für zwei beliebige Werte x
und y
kann sowohl x <= y
als auch y => x
gelten. Das bedeutet nicht, dass x = y
gilt. Sondern: Sie sind nur gleich bezüglich der definierten Ordnungsrelationelation <=
.
Dieser Fall lässt sich mit dem Kommando x <=> y
testen. Der Rückgabewert dieses Ausdrucks ist genau dann wahr, wenn sowohl x <= y
als auch y <= x
zutreffen. D.h., x
und y
sind im Sinne der Ordnungsrelation gleich.
Trifft genau nur eine der beiden zu Aussagen, x <= y
oder y <= x
, zu, liefert das Kommando x <> y
den Wert true
. D.h., x
und y
sind im Sinne der Ordnungsrelation ungleich.
x <> y
ist genau dann wahr, wenn entwederx <= y
odery <= x
falsch ist.x <=> y
ist genau dann wahr, wenn sowohlx <= y
als auchy <= x
wahr sind.
Für die Klassen number
, string
, moment
, measurement
, moment
ist jeweils ein Kommando x <= y
definiert. Diese Definitionen sind mit dem üblichen Verständniss des Zeichens <= in Übereinstimmung.
Die Definition einer solchen Ordnungsrelation <=
ist nicht in jeder Klasse sinnvoll möglich. Z.B. bei dem Datentyp timespan
. Es lässt sich nämlich nicht allgemein sagen, ob ein Monat (1/0/0:00
) mehr oder weniger als 30 Tage (0/30/0:00
) hat. Deshalb führen die Kommandos:
a := 1/0/0:00;
b := 30/0:00;
v := a > b;
zu einer Fehlermeldung des Compilers und das λscript wird nicht gestartet.
Ist über einer Klasse (z.B. cls
) eine Ordnungsrelation definiert, dann existieren neben den Operationen <
, =>
, >
, <=>
und <>
automatisch auch einige weitere Funktionen die auf der Existenz einer Ordnungsrelation beruhen:
a := min x1 x2 ... xn;
b := max x1 x2 ... xn;
Die erste Funktion liefert das kleinste Objekt der Werte x1
bis xn
. Die zweite Funktion entsprechend das größte Objekt. Dabei müssen die Argumente sämtlich vom der Klasse cls
abgeleitet worden sein.
Nachfolgend ein Beispiel für die Verwendung von selbst definierten Ordnungsrelationen:
Betrachtet werden soll die Menge der Schüler eine Schule. Jeder Schüler ist Repräsentant einer Klasse clsSchüler
. Jeder Schüler hat zwei Merkmale: seinen Namen und die Klassenstufe. Der Name ist von Typ string
und die Klassenstufe von Typ number
. Der Standardwert ist hier 1
. Die Definition der Klasse Schüler sieht wie folgt aus.
clsSchüler := value
name string
stufe 1
;
Die einzelnen Schüler (Max Meier in Klasse 1; Ina Schröder in Klasse 3; ...) werden nun wie folgt angelegt:
S1 := new clsSchüler name "Max Meier";
S2 := new clsSchüler name "Ina Schröder" stufe 3;
S3 := new clsSchüler name "Petra Krüger Dingelstedt";
...
Nun wird eine Vergleichsrelation definiert, die sich auf die Klassenstufe bezieht.
'(clsSchüler'x <= clsSchüler'y) := {x'stufe <= y'stufe};
Dabei wird auf die vordefinierte Vergleichsrelation bei Zahlen zurückgegriffen. Nun lassen sich unter Verwendung der zusätzlich zur Verfügung stehenden Kommandos einige Fragen beantworten:
# Sind Schüler 1 und Schüler 3 in der selben Klasse?
log (S1 <=> S3);
true
# Ist Schüler 2 in einer Klasse unter Schüler 1?
log (S2 < S1);
false
# Sind Schüler 1 und Schüler 2 in verschiedenen Klassen?
log (S1 <> S2);
true
…
6.3.4.1.2 Verknüpfung boolscher Werte
Logische Werte können mit den Symbolen ! & |
verknüpft bzw. verändert werden. Anstelle von &
kann auch and
, statt |
das Symbol or
und satt ! auch not
verwendet werden. Wie allgemein üblich führt das Symbol &
zu einer und-Verknüpfung und |
zu einer oder-Verknüpfung. Mit dem Zeichen !
ist die Negation verknüpft. Es kann dem zu negierenden boolschen Ausdruck sowohl vor- als auch nachgestellt sein.
a := true;
b := false;
log (a & b);
false
log (a | b);
true
log (! b);
true
log a'!;
false
6.3.4.1.3 Ablaufsteuerung durch boolsche Werte
Kommandos mit dem Symbol if
dienen z.B. dazu, in Abhängigkeit vom Wert eines boolschen Ausdrucks, einer Variablen den einen oder einen anderen Wert zuzuweisen, oder um das Script mit dem einen oder einem anderen Programmzweig fortzusetzen. Dafür gibt es mehrere Varianten.
Variante 1:
if boolean
{ Block1 }
{ Block2 }
;
weitere Kommandos
Hier wird in Abhängigkeit davon, ob der dem Symbol if
folgende Wert true
oder false
ist, das Script mit dem Block1 oder dem Block2 fortgestzt. Danach werden, falls vorhanden, die weiteren Kommandos abgearbeitet. Fehlt der Block2 und ist der boolsche Wert false
wird sofort mit den Kommandos nach dem if
-Befehl fortgesetzt.
Variante 2:
if
boolean1
{ Block1 }
...
booleank
{ Blockk }
{ Block }
;
weitere Kommandos
Diese Variante ist eine Verallgemeinerung der Variante 1. Auch hier wird, wenn der boolsche Wert true
ist, der unmittelbar folgende Kommandoblock ausgeführt und danach mit den weiteren Kommandos fortgesetzt. Ist keiner der boolschen Werte true
wird der letzte Block ausgeführt - wenn er angegeben ist. Dieser Block ist optional. Fehlt er, und ist kein einziger boolscher Wert true
, werden die weiteren Kommandos abgearbeitet.
Variante 3:
if wert
wert1
{ Block1 }
...
wertk
{ Blockk }
{ Block }
;
weitere Kommandos
Diese Variante arbeitet ähnlich wie Variante 2. Der Block1 wird ausgeführt wenn der Vergleich wert = wert1
zu true
ausgewertet wird. Analog Block2 usw.
Ähnlich wie in den obigen Varianten der Programmablauf mit boolschen Werten gesteuert werden kann, ist auch eine gesteuerte Wertzuweisung möglich. Auch hier gibt es wieder mehrere Optionen.
Variante 1:
Objekt := if bool wert1 wert;
Hier wird in Abhängigkeit davon, ob bool
den Wert true
oder false
hat, dem Objekt
der Wert wert1
oder wert2
zugewiesen. Wichtig ist, das die Angabe des alternativen Wertes wert
nicht optional ist. Diese Form ist ein Spezialfall der
Variante 2:
Objekt:= if bool1 wert1 ... booln wertn wert;
Hier wird, wenn ein boolscher Wert true
ist, Objekt
der nachfolgende Wert zugewiesen. Sind alle boolschen Werte false
, bekommt Objekt
den Wert wert
. Auch hier ist die Angabe von wert
zwingend erforderlich.
Variante 3:
Objekt := if test
test1 wert1
...
testn wertn
wert
;
Auch hier wird, wenn test = test1
wahr ist, an Objekt
der Wert wert1
übertragen, wenn test = test2
wahr ist, an Objekt
der Wert wert2
übertragen usw. Sind alle Vergleiche falsch, bekommt Objekt
den Wert wert
. Auch hier ist die Angabe von wert
zwingend.
6.3.4.2 number
6.3.4.2.1 Zählschleifen
Mit dem Kommando:
forEach k -10 11 2 {
Block
};
wird der Block mehrfach durchlaufen. Zuerst mit dem Wert -10 für die Variable k
. Dann wird k
um den Wert 2
erhöht. Der Block wird erneut durchlaufen usw. Der letzte Durchlauf findet mit dem Wert 10
statt, da der nächste Wert 12
größer als die obere Grenze ist. Die Schrittweite (hier 2
) muss größer als 0
sein. Die Laufvariable (hier k
) muss entweder als Variable vom Typ number
angelegt worden sein, oder, wenn sie nicht existiert, wird sie an dieser Stelle angelegt. Steht k
für den Wert einer anderen Klasse bricht der Compiler mit einer Fehlermeldung ab.
Nach dem Durchlauf der Schleife hat die Laufvariable den letzten Wert mit dem der Block durchlaufen wurde.
Wird die letzte Zahl (die Schrittweite) nicht angegeben wird dafür 1
genommen. Wird der Startwert nicht angegeben wird dafür ebenfalls 1
angenommen. Beispiele:
forEach i 5 {
log i;
}
1
2
3
4
5
forEach i 5 9 {
log i " zum Quadrat = " (i i);
}
5 zum Quadrat = 25
6 zum Quadrat = 36
7 zum Quadrat = 49
8 zum Quadrat = 64
9 zum Quadrat = 81
6.3.4.3 string, word
6.3.4.3.1 Zusammensetzen von Strings
Das hier Gesagte gilt sowohl für Objekte von Typ word
als auch von Typ string
.
Zusammengefügt werden Zeichenketten in dem man die entsprechenden Objekte hintereinander schreibt. Beispiel:
x := "Hallo " "Welt" "!";
Die Verwendung des Symbols +
als Verknüpfungsoperator ist optional möglich. Obiges Kommando ist dem Folgenden äquivalent:
x := "Hallo " + "Welt" + "!";
Danach hat x
den Wert "Hallo Welt!"
. Logischerweise ergibt sich beim Zusammenfügen von Objekten des Typs word
wieder ein Objekt des Typs word
. Befindet sich in der Folge der zusammen zu fügenden Zeichenketten ein Objekt string
ist das Ergebnis auch vom Typ string
.
6.3.4.3.2 regEx mit Strings
Beim Test, ob Zeichenketten eine bestimmte Struktur oder einen bestimmten Inhalt haben kann die regEx-Syntax benutzt werden. Als charakteristisches Zeichen wird hier die Tilde ~
benutzt. Beispiel:
string := "Hallo Welt!";
log (string ~ /welt/);
false
log (string ~ /welt/i);
true
log (string ~ /Welt/);
true
Für eine ausfühliche Dokumentation von regEx sei auch an dieser Stelle auf die umfangreiche Literatur verwiesen.
Auch für die direkte Bearbeitung von Zeichenketten wurde das Konzept von regEx implementiert - mit dem Präfix s
. Beispiel:
string := "Hallo Welt!";
string ~ s/Welt/User/;
log string;
Hallo User!
Über die Arbeitsweise von regEx gibt es eine Vielzahl von Dokumentationen. Deshalb wird sie an dieser Stelle nicht vertieft.
6.3.4.4 moment, timespan
6.3.4.4.1 Operationen mit moment und timespan
Für die Verknüpfung von Zeitpunkten und Zeitspannen sind eine Reihe von Funktionen vordefiniert. Für die Addition und Subtraktion dieser Objekte sei zunächst auf die Tabelle in Zweistellige Verknüpfungsoperationen verwiesen. Ergänzend soll an dieser Stelle auf ein paar Dinge hingewiesen werden:
- Alle Berechnungen basieren auf dem Gregorianischen Kalender.
- Bei Objekten von Typ
timespan
werden intern die Angaben von Tagen, Stunden und Minuten in Sekunden umgerechnet und dieser Wert gespeichert. Die Angabe von Monaten wird davon separat gespeichert. - Wird ein Objekt
m
vom Typmoment
und ein Objektt
vomtimespan
addiert (oder subtrahiert) und enthält das Objektt
eine Monatsangabe, wird folgendermaßen vorgegangen: Gegenüber dem Datum dasm
entspricht wird um die int
enthaltene Monatszahl weitergezählt. Sollte dieses Datum (z.B. 30. Februar) nicht existieren wird auf den letzten Tag dieses Monats zurückgesprungen. Danach werden zu dem sich so ergebenden Zeitpunkt die int
hinterlegten Sekunden addiert (bzw. subtrahiert).
Dazu ein paar Beispiele:
m := 31.1.2019;
log m;
31.1.2019 00:00,00
m := 31.1.2019 12:34,56;
log m;
31.1.2019 12:34,56
m := 31.1.2019 1/0/0
log m;
28.2.2019 00:00,00
m := 31.1.2019 1/0/12
log m;
28.2.2019 12:00,00
Aus einem Objekt vom Typ second
, minute
, hour
, day
, weekday
, week
, month
, year
die entsprechenden Bestandteile aus dem Wert des Objektes "herauslösen".
m := 31.1.2019 12:34,56;
log m'second;
56
log m'minute;
34
log m'hour;
12
log m'day;
31
log m'weekday;
4
log m'week;
5
log m'month;
1
log m'year;
2019
weekday
liefert die Nummer des Wochentags zurück. (1 = Montag, 2 = Dienstag, ... , 7 = Sonntag)
Steht man vor der Aufgabe festzustellen, ob die aktuelle Uhrzeit in einem bestimmten Intervall, z.B. zwischen 8:00 Uhr und 12:30 Uhr, liegt, kann das folgende Kommando benutzt werden:
log [8:00 12:30];
true
Die beiden in den eckigen Klammern stehen Argumente sind von Typ timespan
. Beide Werte dürfen keine Monatsangabe besitzen und die Länge des Zeitintervalls darf nicht kleiner als 0
und darf nicht größer als eine Tageslänge (24:00
) sein. Das wird zur Laufzeit überprüft und das Script bei Nichterfüllung dieser Bedingungen mit einer Fehlermeldung beendet.
Das Ergebnis ist der boolsche Wert true
, wenn die aktuelle Uhrzeit im angegebenen Intervall liegt. Ansonsten ist false
der Rückgabewert.
Ist der zweite Wert in der eckigen Klammer kleiner als der Erste, wird das entgegengesetzte Ergebnis zurückgegeben. Dadurch werden "Mitternacht übergreifende" Auswertungen realisiert.
log [23:00 6:00];
Diese Abfrage liefert true
zwischen Abends 11 Uhr und Morgens 6 Uhr und sonst false
.
Steht man vor der Aufgabe festzustellen, ob die aktuelle Zeit vor oder nach einem definierten Zeitpunkt (z.B. heute 12:00 Uhr) liegt, lässt sich das so realisieren:
isVormittag := [0:00 12:00];
isNachmittag := [12:00 24.00];
Danach haben die Variablen isVormittag
und isNachmittag
den Wert true
oder false
je nachdem, ob die aktuelle Uhrzeit vor oder nach 12:00 Uhr ist.
6.3.4.4.2 Konvertierung von number nach timespan
Für den Fall, das Werte von Typ number
in Zeitspannen umgewandelt werden sollen, stehen einige Möglichkeiten bereit. Angenommen n
ist eine Zahl, so lässt sich mit den folgenden Kommandos diese Zahl in eine entsprechende Anzahl von Sekunden, Minuten, Stunden, Tagen, Wochen, Monate und Jahre umrechnen:
n = 3;
log n'seconds;
,3
log n'minutes;
3,0
log n'hours;
3:0,0
log n'days;
3/0:0,0
log n'weeks;
21/0:0,0
log n'months;
3/0/0:0,0
log n'years;
36/0/0:0,0
Die Ausgangswerte für eine Umrechnung in Monate oder Jahre müssen ganze Zahlen sein. Anderenfalls endet λscript mit einer Fehlermeldung.
6.3.4.5 collection
6.3.4.5.1 Existiert ein Element mit einem bestimmten Schlüssel
collection exist word|symbol; | ? | |
Beschreibung | Dieses Kommando gibt true oder false, je nach dem, ob das Objekt vom Typ collection ein Element mit dem Schlüssel vom Typ word (oder Symbol) enthält oder nicht. | |
Rückgabewert | boolean | |
Beispiel | coll := new collection; … if (coll exist "x") { … }; |
collection notExist word|symbol; | ? | |
Beschreibung | Dieses Kommando gibt false oder true, je nach dem, ob das Objekt vom Typ collection ein Element mit eimem Schlüssel vom Typ word (oder Symbol) enthält oder nicht. | |
Rückgabewert | boolean | |
Beispiel | coll := new collection; … if (coll notExist x) { … }; |
6.3.4.6 fhemDevices
Es gibt eine Reihe von vordefinierten Kommandos um auf die in einer Installation enthaltenen Objekte vom Typ fhemDevices
zuzugreifen:
set fhemDevice (value|symbol); | ? | |
Beschreibung | Dieses Kommando führt einen set-Befehl an einem Fhem-Device aus. Dazu werden alle dem Device folgenden Parameter zu einem String (durch Leerzeichen getrennt) zusammengefasst. Werte werden mit Hilfe von toString in Strings umgewandelt. | |
Rückgabewert | - | |
Beispiel | set Lamp on; level := 50; set Rollo pct level; |
[fhemDevice word|symbol]; | ? | |
Beschreibung | Dieses Kommando gibt den Wert eines Readings zurück. | |
Rückgabewert | string | |
Beispiel | s := [Thermostat humidity]; |
[fhemDevice word|symbol time]; | ? | |
Beschreibung | Dieses Kommando gibt den Zeitpunkt der letzten Änderung eines Readings zurück. | |
Rückgabewert | moment | |
Beispiel | t := [Thermostat humidity time]; |
[fhemDevice]; | ? | |
Beschreibung | Dieses Kommando gibt den Wert des Readings state zurück. | |
Rückgabewert | string | |
Beispiel | s := [Thermostat humidity]; |
[fhemDevice word|symbol time]; | ? | |
Beschreibung | Dieses Kommando gibt den Zeitpunkt der letzten Änderung des Readings state zurück. | |
Rückgabewert | moment | |
Beispiel | t := [Thermostat time]; |
[fhemDevice internal word|symbol]; | ? | |
Beschreibung | Das Kommando gibt aus dem Register Internals den Wert mit dem Namen der durch word oder ein Symbol bezeichnet ist zurück. | |
Rückgabewert | string | |
Beispiel | t := [Thermostat internal TYPE]; |
6.3.4.6.1 fhemLambdaDevice
setReading (@symbol|word value); | ? | |
Beschreibung | Dieses Kommando setzt in dem Device, welches das aktuelle lambda-script enthät, das mit einem Symbol oder mit word bezeichneten Reading auf den angegebenen Wert. Die value wird mit Hilfe des Kommandos value'toString in einen Wert vom Typ string umgewandelt. | |
Rückgabewert | - | |
Beispiel | setReading status "ein"; |
initReading (@symbol|word value); | ? | |
Beschreibung | Dieses Kommando arbeitet analog obigem setReading. Allerdings werden die Readings nur gesetzt wenn sie zum Zeitpunkt des Aufrufs noch nicht existieren. | |
Rückgabewert | - | |
Beispiel | initReading status "aus"; |
6.3.4.6.2 fhemHomematicDevice
fhemHomematicDevice channel_nn; | ? | |
Beschreibung | Dieses Kommando gibt das Fhem-Device zurück, das dem Channel mit der angegebenen Nummer entspricht. | |
Rückgabewert | fhemHomematicDevice | |
Beispiel | wz_weather := WZ_Thermostat channel_01; |
6.3.4.7 forEach - Schleifen mit wechselnden Inhalten
6.3.4.7.1 Durchlaufen eines Stacks
Ist myStack eine Variable vom Typ stack
wird mit dem Kommando
forEach myStack v {
Block
};
der Block für jedes Element (beginnend mit dem untersten Element) des Stacks abgearbeitet. Der Stack selbst wird dabei nicht verändert. Die Variable v
muss entweder als Variable vom Typ der Elemente des Stacks angelegt worden sein, oder, wenn sie nicht existiert, wird sie an dieser Stelle angelegt. Steht v
für den Wert einer anderen Klasse bricht der Compiler mit einer Fehlermeldung ab.
Nach dem Durchlauf der Schleife hat v
den Wert des obersten Elements des Stapels.
6.3.4.8 Zufallsfunktionen
6.3.4.8.1 random - Zufallszahlen
Zufallszahlen werden mit Kommandos die das Symbol random
enthalten erzeugt. Dem Symbol können ein oder zwei ganze Zahlen folgen. Werden zwei Zahlen angegeben gibt die zugehörige Funktion ganzzahlige Zufallszahlen, die nicht kleiner als die Erste und nicht größer als die Zweite sind, zurück. Die zweite Parameter muss dann selbstverständlich größer als der Erste sein. Ist das nicht der Fall, wird das Script mit einer Fehlermeldung beendet.
log (random 0 6);
5
log (random 0 6);
1
log (random 0 6);
6
log (random 0 6);
0
log (random 0 6);
3
Folgt dem Symbol random
nur eine Zahl, muss diese größer als 1
sein, und es werden Zufallszahlen zwischen 1
und diesem Parameter erzeugt.
6.3.4.8.2 shuffle - Mischen eines Stacks
Das Kommando myNewStack := myStack shuffle;
kopiert die Elemente des Stacks myStack
in zufälliger Reihenfolge in den Stack myNewStack
.
myStack := (new stack of word) << "a" "b" "c" "d";
log myStack;
(word|a,b,c,d)
log myStack'shuffle;
(word|c,d,a,b)
log myStack;
(word|a,b,c,d)
6.3.5 wait - Wartekommandos
Wartekommandos dienen dazu, die Abarbeitung eines Scrips zu stoppen und an einem definierten Punkt fortzusetzen. Dieser Punkt kann sein:
- Ein beobachteter Wert (z.B. ein Reading eines Devices) hat sich geändert. Dieser Wert kann aber auch der Wert einer Variablen sein, die in einem anderen Thread des selben Scriptes verändert wird.
- Ein definierter Zeitpunkt wurde erreicht.
- Eine bestimmte Zeitspanne ist verstrichen.
Wurden mehrere Bedingungen angegeben, wird der Wartezustand beendet, wenn einer der obigen Punkte eingetreten ist.
Das charakteristische Symbol für die Programmierung von Wartezuständen ist wait
. Es gibt mehrere, nachfolgend beschriebene, Varianten des wait
-Kommandos.
6.3.5.1 wait - Basisform
Die grundlegende Form des wait
-Kommandos ist:
wait v1 v2 … vn;
v1
bis vn
sind Werte und stellen die Parameter des wait
-Kommandos dar. Hat ein übergebener Parameter den Wert empty
, wird der Parameter nicht berücksichtigt. Das Kommando stoppt den laufenden Thread. Dieser Wartebezustand wird beendet, wenn einer der Parameter "feuert". Der Rückgabewert des wait
-Kommandos ist die Nummer der Parameters der gefeuert hat. Feuert der Parameter v1
ist der Rückgabewert 1. Feuert der Parameter v2
ist der Rückgabewert 2. ...
Der Rückgabewert ist 0, wenn das wait
-Kommando nicht ausgeführt wird. Das ist z.B. der Fall, wenn alle Parameter den Wert empty
haben oder die Zeitpunkte auf die gewartet werden soll, sämtlich in der Vergangenheit liegen.
Ein Parameter feuert, wenn einer dieser Fälle eintritt:
- Ist ein Parameter ein Objekt vom Typ
moment
, feuert er, wenn der angegebene Zeitpunkt erreicht wird. Liegt der Zeitpunkt beim Aufruf deswait
-Kommandos bereits in der Vergangenheit wird diese Zeitangabe ignoriert. Ist kein weiterer Parameter vorhanden, wird das Programm (korrekter der Thread) sofort fortgesetzt und der Rückgabewert deswait
-Kommandos ist 0. - Ist ein Parameter des
wait
-Kommandos ein Objekt vom Typtimespan
, feuert er, wenn die Zeitspanne verstrichen ist. Ist die Zeitspanne negativ und ist es der einzige Parameter, wird das Programm sofort fortgesetzt und der Rückgabewert deswait
-Kommandos ist 0. - In allen anderen Fällen feuert ein Parameter, wenn sich dessen Wert ändert. Der Wert kann sich selbstverständlich nur ändern, wenn er sich entweder auf die Hardwareumgebung bezieht, oder wenn es sich um eine Variable handelt die in einem parallel laufenden Thread verändert wird. Dazu gibt es im Abschnitt "threads" einige Beispiele.
Nachfolgend Beispiel mit dem Bezug auf ein fhemDevice. Hier wird der Rückgabewert des wait
-Kommandos in einer Variablen w
gespeichert:
w := wait 1:00 [WZ_Thermostat];
if (w = 1) {
command 11;
...
command 1n;
} (w = 2) {
command 21;
...
command 2n;
};
In diesem Beispiel wird darauf gewartet, dass sich das Reading state des Devices WZ_Thermostat ändert. Unabhängig davon, wird der Wartezustand spätestens nach einer Stunde beendet. In diesem Fall ist der Rückgabert des wait
-Kommandos 1 und es wird der erste Kommandoblock abgearbeitet. Ändert sich aber vor dem Ablauf der Stunde das Reading state, ist der Rückgabewert 2 und es wird der zweite Komamndoblock abgearbeitet.
6.3.5.2 wait - Blockform
Von der obigen Variante ist diese Form des Aufrufs abgeleitet:
wait a1 … an {blockA} b1 … bn {blockB} … v1 … vn {blockV};
Der letzte Block {blockv}
ist optional.
Der Wartezustand wird hier beendet, wenn einer der Parameter a1
bis vn
feuert. Feuert einer der Werte von a1
bis an
wird der Block {blockA}
abgearbeitet und danach mit den Kommandos die dem wait
-Befehl folgen fortgesetzt. Analog wird, wenn einer Werte b1
bis bn
feuern, der Block {blockB}
ausgeführt, usw. Ist {blockV}
nicht angegeben, und feuert einer der Parameter v1
bis vn
, wird unmittelbar mit dem Kommando nach dem wait
-Befehl fortgesetzt.
Das Beispiel aus dem vorhergehenden Abschnitt lässt sich damit wie folgt schreiben:
wait 1:00 {
command 11;
…
command 1n;
} [WZ_Thermostat] {
command 21;
…
command 2n;
}
;
6.3.6 threads - Parallel ablaufende Scripte
Das charakteristische Symbol für den Start von parallel laufenden Prozessen ist thread
.
Eine Möglichkeit des Aufrufs ist:
thread {
...
};
Das heißt, dem Schlüsselwort thread
folgt ein Kommandoblock. Dieser wird gestartet und abgearbeitet. Ein Thread wird bis zu einem wait
-Kommando ausgeführt. Danach wird gewartet bis ein anderer Thread bereit ist. In einem Thread kann auf alle in den übergeordneten Blöcken definierten Devices, Klassen und definierten Funktionen zugegriffen werden. Ansonsten ist der Kommandoblock des Threads gekapselt. Das bedeutet, der Zugriff auf die in den übergeordneten Blöcken verwendeten Variablen ist so nicht möglich. Sollen innerhalb des Threads Variable von außerhalb verwendet werden, müssen diese nach dem Schlüsselwort thread
explizit aufgelistet werden.
a := "Abend!";
thread a {
log "Guten " a;
};
log "Guten Morgen!";
Dieses Script gibt die beiden Nachrichten "Guten Morgen!" und "Guten Abend!" aus. Die Reihenfolge mit der das geschieht ist nicht definiert, da das System entscheidet, welcher der Threads wann abgearbeitet wird. Das ist im nachfolgenden Beipiel anders:
a := "Abend!";
w := 0;
thread a w {
wait w;
log "Guten " a;
};
log "Guten Morgen!";
w := 1;
Hier wird zuerst "Guten Morgen!" und danach "Guten Abend!" ausgegeben. Der Grund ist: Dem untergeordneten Thread wird eine zweite Variable w
übergeben. wait w;
stoppt den Thread bis sich w
ändert. Diese Änderung durch das Kommado w := 1;
erfolgt erst nach der Ausgabe von "Guten Morgen!".
Threads können ineinander verschachtelt werden. Das heist, in Threads können wieder Threads gestartet werden. Genau genomment ist es sogar so, dass jedes λscript einer FHEM-Installation ein Thread eines systemweiten Scripts ist.
Mit dem Kommando terminate;
wird ein laufender Thread beendet. Gleichzeig werden auch alle Threads beendet die in diesem gestartet wurden.
w := 0;
thread w {
wait w;
log "Guten Abend!";
terminate;
log "Gute Nacht!";
};
log "Guten Morgen!";
w := 1;
Hier wird "Gute Nacht!" nicht mehr ausgebeben, weil der Thread vorher beendet wurde.
Verwandt mit dem Kommando terminate;
ist das Kommando stop;
. Damit wird das gesamte λscript beendet.
6.3.7 Wiederholung von Programmteilen
6.3.7.1 repeat
Kommandos mit dem Symbol repeat
dienen dazu, Kommandoblöcke wiederholt zu durchlaufen. Um einen Kommandoblock unendlich oft zu wiederholen, dient das Kommando:
repeat {
command1;
...
commandk;
};
Soll der Block n-mal (n ist eine ganze Zahl ≥ 0) wiederholt werden, wird die Anzahl nach dem Schlüsselwort notiert:
repeat n {
command1;
...
commandk;
};
repeat 10,0 {
set WZ_Lampe toggle;
};
Vor die Zeitspanne, kann eine Zeitpunkt (Typ moment
) gesetzt werden. Liegt dieser Zeitpunkt in der Zukunft, wird mit der ersten Ausführen des Block gewartet bis der Zeitpunkt erreicht wird. Liegt der Zeitpunkt in der Vergangenheit, wird mit der erstmalige Ausführen des Block gewartet, bis durch wiederholte Addition der Zeitspanne ein zukünftiger Zeitpunkt erreicht wird.
repeat 22:00'today 10,0 {
set WZ_Lampe toggle;
};
Auch hier kann als dritter Parameter eine ganze Zahl ≥ 0 angegeben werden. Dann wird die Schleife so oft durchlaufen wie diese Zahl angibt. Beispiel:
repeat hour 1:00 {
repeat 0,1.5 now'hour {set WZ_Gong on}
};
Dieses Script lässt einen Gong zu jeder vollen Stunde schlagen. Die Anzahl der Schläge ist die Stundenzahl. Die äußere Schleife hat als Startzeipunkt hour
. Das ist der Beginn der laufenden Stunde. Von dort an (der Zeitpunkt liegt in der Vergangenheit wird der Block repeat 0,1.5 now'hour {set WZ_Gong on}
stündlich, zu jeder vollen Stunde, wiederholt. Dieser Block besteht aus einer Schleife die sofort startet und alle 1,5 Sekunden das Kommando zum Ertönen des Gongs gibt. Die aktuelle Stundenzahl liefert der Ausdruck now'hour
(siehe hier). Diese ganze Zahl bestimmt, wie oft die Schleife durchlaufen wird - wie oft der Gong ertönt.
6.3.7.1.1 loop
Das Kommando loop;
startet sofort einen neuen Schleifendurchlauf. Dazu ein Beispiel:
repeat {
wait 1:0;
if ([WZ_Lampe] ~ /off/) {loop};
log "WZ_Lampe ist an";
};
Auch hier wird stündlich protokolliert, ob WZ_Lampe
eingeschaltet ist. Die Schleife wird nie beendet. Wenn das Device ausgeschaltet wird, wird wieder an den Anfang der Schleife, zum Wartebefehl, gesprungen.
Eine Variation diese Kommandos ist die Übergabe eines Zeitspanne. In diesen Fall wird die wiederholte Abarbeitung des Blocks erst nach der angegebenen Zeitspanne durchgeführt. Beispiel: Ein- bzw. Ausschalten einer Lampe nach 10 Minuten:
6.3.7.1.2 quit
Das Kommando quit;
dient zum Beenden einer Schleife. Im nachfolgenden Beispiel wird stündlich protokolliert ob WZ_Lampe
eingeschaltet ist. Die Schleife wird beendet, wenn das Device ausgeschaltet wurde:
repeat {
if ([WZ_Lampe] ~ /off/) {quit};
log "WZ_Lampe ist an";
wait 1:0;
};
6.3.7.2 always
Der in der Praxis häufig vorkommende Fall, das die Endloswiederholung eines Kommandoblocks in einem seperaten Thread gestartet werden soll, wobei die Variablen v1
bis vn
an den Thread übergeben werden, würde so aussehen:
thread v1 ... vn {
repeat {
command1;
...
commandk;
};
};
Dafür gibt es die abkürzende Schreibweise unter Verwendung des Schlüsselwortes always
:
always v1 ... vn {
command1;
...
commandk;
};
7 Auslagerung und Import von Scripten
Um selbst definierte Klassen und Funktionen in mehreren Scripten verfügbar zum machen, kann man die entsprechenden Definitionen auslagern und mit dem Kommando import
in jedem Script, an jeder beliebigen Stelle wieder verwenden.
Die Auslagerung kann in ein Device vom Typ lambda oder in eine externe Datei mit der Endung ".lbd" erfolgen.
Angenommen die eigenen systemweit benutzen Klassen und Funktionen stehen in der Datei "myLambda.lbd", dann können diese Definitionen in einem anderen Script mit dem Kommando:
import pfadangabe/mylambda.lbd;
zur Verfügung gestellt werden.
Es ist ebenfalls möglich, die eigenen Klassen und Funktionen in einem Device vom Typ lambda (z.B. mit dem Namen "myClasses") zu hinterlegen und mit dem Befehl:
import myClasses;
zu importieren.