Scripte und Prozeduren

Wir zeigen Dir, wie's geht!

Automatisierung durch Scripte

Was wir bisher gelernt haben, ist direkt nützlich und ermöglicht Auswertungen aller Art, jedoch ist eine interaktive Bedienung auf Dauer weder besonders angenehm noch fehlerresistent. So kann es bei einem kleinen Tippfehler schnell dazu kommen, dass man die letzten 20 Ausdrücke nochmals eingeben darf. Dafür gibt es zwar die Eingabechronik, doch trotzdem ist das unnötig mühselig.

Daher zeigen wir Dir hier den nächsten logischen Schritt und wenden uns dem Editor zu. Dieses Werkzeug ist ziemlich mächtig und unterstützt Dich in vielerlei Hinsicht. In erster Linie dient er aber dazu, NumeRe-Code in eine einfach wiederholbare Form zu bringen: NumeRe-Scripte. In einem solchen Script kannst Du die Ausdrücke und Kommandos, die Du aneinanderreihen willst, einfach zeilenweise nacheinander eintippen, das Script speichern und von NumeRe ausführen lassen. Außerdem kannst Du Deinen Code zusätzlich noch kommentieren, um ihn auch in der Zukunft noch verstehen zu können.


Ja, ja. Wissen wir. Code sollte selbsterklärend geschrieben sein. Aber jetzt mal ernsthaft: was ist leichter zu lesen - ein Bandwurm an Code ohne weitere Informationen oder Code, der gelegentlich von ein oder zwei Zeilen erläuterndem Fließtext unterbrochen wird? Es ist nun mal so, dass Code halt viel häufiger gelesen als geschrieben wird und dafür sollte er auch optimal vorbereitet sein.

Genug des Vorgeplänkels. Stellen wir uns vor, Du wolltest die drei möglichen Varianten der Keplerbahnen plotten. Dabei willst Du natürlich nach Möglichkeit auf polare Koordinaten wechseln und auch noch mit den Parametern der Keplerbahn spielen, um die Übergänge zwischen den Varianten zu finden.

Erzeuge also ein neues Script im Editor (z.B. im Datei-Menü als Neu/Neues Script) und benenne es z.B. "kepler". Eine Datei wird im Editor geöffnet, die "kepler.nscr" heißt. Das ist Dein neues Script, in dem wir gleich die nötigen Code-Zeilen einfügen werden.

In dieser Datei wirst Du Blockkommentare innerhalb von #* ... *# finden, Zeilenkommentare beginnst Du dagegen mit der Zeichensequenz ##. Die Blockkommentare beschreiben Header und Footer der Datei. Kommentare haben keinen Einfluss auf das Script. Du kannst sie also löschen, wenn Du willst.

Kopiere dann den unten folgenden Code in das Script und speichere es. Du kannst es auch jetzt schon ausführen, wenn Du willst. Du solltest dann eine Ellipse in einem Polarkoordinatensystem wie in der rechten Abbildung erkennen können.

Kopiere diesen Code in Dein Script mit dem Namen "kepler.nscr"

Du willst aber sicher auch wissen, was die neuen Elemente in diesem Script genau bewirken. Das Kommando lclfunc definiert eine neue Funktion wie define mit dem einzigen Unterschied, dass die Funktion nur innerhalb der aktuellen Code-Einheit (Script oder Prozedur) definiert ist.

Die Zeichensequenz \\ am Ende der vorletzten Zeile indiziert, dass diese Zeile noch nicht fertig ist und in der nächsten Zeile fortgesetzt wird. Das hilft, lange Ausdrücke überblickbar zu halten. Du kannst einen Ausdruck natürlich auch mehrmals umbrechen und so auf viele Zeilen verteilen.

Die Parameter in der letzten Zeile steuern das Erscheinungsbild des Plots. Zuerst setzen sie alle vorherigen Einstellungen zurück (reset), dann wird ein Gitter (grid) und polare Koordinaten (coords=polar) aktiviert. Die Zahl der Samples wird erhöht, um ein glatteres Bild zu erzeugen und außerdem wird noch auf ein quadratisches Fensterseitenverhältnis gewechselt (aspect=1), damit die polaren Koordinaten auch tatsächlich Kreisrund aussehen.

Wieder einmal Glückwunsch! Du hast soeben Dein erstes Script geschrieben. Nun kannt du einfach mit den Keplerbahnenparametern spielen. Du könntest beispielsweise eps_ell() durch eps_hyp() austauschen, was dann aus der Ellipse eine Hyperbel macht.

Programmieren mit Kontrollfluss-Statements

Natürlich willst Du nicht nur linear ablaufenden Code schreiben, sondern abhängig von Bedingungen auch unterschiedlichen Code oder den gleichen Code mehrmals ablaufen lassen. Diese Aufgabe erfüllen Kontrollfluss-Statements.

NumeRe bietet 5 verschiedene Kontrollfluss-Statements, die alle unterschiedliche Einsatzzwecke haben. Du siehst diese Statements auf der rechten Seite in Form eines rudimentären Pseudo-Codes. Außerdem haben wir Dir in den Kommentaren geschrieben, in welchen Fällen welche Zweige ausgeführt werden.

Alle Statements haben gemein, dass sie beliebig häufig ineinander verschachtelt werden können (natürlich sind dem Grenzen seitens des Speichers gesetzt) und dass nach einem Kontrollfluss-Statement der Code normal fortgesetzt wird.

Betrachten wir nun die verschiedenen Statements genauer:

  • if-Statement: Abhängig davon, ob die Bedingung direkt nach if wahr ist, wird der Inhalt des enthaltenen Blocks ausgeführt. Sollte die Bedingung falsch sein werden eventuell vorhandene, nachfolgende elseif -Bedingungen geprüft und ggf. in deren Zweige gesprungen. Sind auch diese alle falsch, wird der else-Zweig verwendet, sofern vorhanden.

  • for-Statement: So lange der Zählindex i sich innerhalb des Start- und Endindexes befindet, wird der Inhalt des enthaltenen Blocks wiederholt ausgeführt und nach jeder Iteration der Zählindex um 1 erhöht (oder erniedrigt, falls i2 < i1). Bei Eintritt in das for-Statement, wird der Zählindex immer auf den Startindex gesetzt.

  • while-Statement: So lange die Bedingung direkt nach while wahr ist, wird der Inhalt des enthaltenen Blocks wiederholt ausgeführt. Die Bedingung wird zu Beginn jeder Iteration geprüft.

  • switch-Statement: Abhängig vom Wert des Ausdrucks direkt nach switch wird der erste case ausgewählt, dessen Wert mit dem berechneten Wert übereinstimmt und die Codeausführung ab diesem case fortgesetzt, bis entweder ein break oder das zugehörige endswitch gefunden wird (letzteres ist als fallthrough bekannt). Stimmt kein Wert überein, springt die Ausführung zu dem eventuell vorhandenen default. Das switch-Statement verhält sich daher eher wie das C++-Pendant und nicht wie die MATLAB-Implementierung.

  • try-Statement: Dieses Statement führt immer den Code im ersten Block aus. Sollte es dabei zu einem Fehler kommen, dann springt die Ausführung zum ersten catch-Block, dessen Typ mit dem Fehlertyp übereinstimmt oder ein generischer catch-Block ohne Fehlertyp ist. Hier kann dann eine entsprechende Behandlung des Fehlers erfolgen. Sollte kein passender catch-Block vorhanden sein, springt die Ausführung so lange weiter nach außen, bis entweder ein passender catch-Block gefunden oder das Kommandofenster erreicht wurde. In letzterem Fall wird dann wie immer eine Fehlermeldung ausgegeben.

Abstraktion und fortgeschrittene Programmierung: Prozeduren

NumeRe-Scripte sind ziemlich hilfreich, wenn es darum geht, denselben Code mehrmals auszuführen und ihn gleichzeitig komfortabel zu bearbeiten. Nun haben Scripte aber den Nachteil, dass von einem Script kein weiteres Script gestartet werden kann. Um also Scripte nicht ständig ineinander kopieren zu müssen, benötigen wir eine fortgeschrittenere Programmierung: die NumeRe-Prozeduren.

NumeRe-Prozeduren sind Dateien vom Typ *.nprc, die in der Prozedur-Bibliothek (der Pfad hinter <procpath>) abgelegt sind. Um eine Prozedur zu erstellen, muss eine Prozedurdatei mit dem gleichen Namen erzeugt werden, also z.B. my_proc.nprc für eine Prozedur mit Namen $my_proc(). Auch das kannst Du einfach vom Datei-Menü erledigen lassen. Gebe einfach den gewünschten Namen Deiner Prozedur an und NumeRe wird Dir die passende Prozedurdatei mit einem Template erstellen.

Oben siehst Du den generischen Aufbau einer Prozedur. Der Kopf einer Prozedur enthält den Namen, die Argumentliste und zusätzliche Flags. Der Name definiert offensichtlich den Identifizierer, unter dem die Prozedur aufgerufen wird. Die Argumentliste beschreibt die möglichen Argumente und ihre Standardwerte in der Form $PROCEDURE(ARG1, ARG2 = DEFVAL2, ARG3, ...). Wie Du siehst, ist es nicht nötig für jedes Argument einen Standardwert vorzugeben. Du könntest diese Prozedur dann z.B. in diesen Formen aufrufen:

$PROCEDURE(VAL1, VAL2, VAL3, ...)

$PROCEDURE(VAL1, , VAL3, ...)

Im zweiten Fall wird dann für ARG2 automatisch DEFVAL2 als Wert übernommen. Das leere Argument ist nur nötig, wenn für die darauffolgenden Argumente Werte übergeben werden sollen. Eine Prozedur der Form $PROCEDURE(ARG1, ARG2 = DEFVAL2, ARG3 = DEFVAL3) kann in diesen Varianten aufgerufen werden, wobei für die fehlenden Werte dann die Standardwerte verwendet werden:

$PROCEDURE(VAL1, VAL2, VAL3)

$PROCEDURE(VAL1, VAL2)

$PROCEDURE(VAL1, , VAL3)

$PROCEDURE(VAL1)

Die Flags als letztes Element des Prozedurkopfes kannst Du verwenden, wenn Du die Prozedur weiter spezifizieren willst. Als Flags existieren z.B. inline, test, mask, event, private und noch ein paar weitere. Details dazu kannst Du Dir später in der internen Dokumentation zu den Prozeduren (help procedure) ansehen. Für die ersten Schritte ist die Verwendung von Flags nicht nötig.

Im eigentlichen Prozedurrumpf siehst Du noch das Kommando return. Dieses Kommando definiert den Rückgabewert und wird die Prozedur auch an dieser Stelle verlassen. Du kannst das Kommando mehrmals pro Prozedur mit unterschiedlichen Werten und Typen verwenden, doch beachte, dass NumeRe immer die Prozedur an deren Stellen verlassen wird.

Das soll an abstrakten Erklärungen vorerst einmal ausreichen. Wir implementieren an dieser Stelle einmal ein einfaches Beispiel. Wir wollen eine Prozedur entwickeln, die uns den Namen des Wochentags zurück gibt, wenn wir ihr die entsprechende Nummer übergeben. Dazu musst Du erst einmal die entsprechende Prozedurdatei erzeugen. Wir verwenden in diesem Beispiel den Namen $getNameOfWeekday. Gebe diesen ein und NumeRe wird Dir die entsprechende Datei erzeugen. Füge dann den unteren Code für die Prozedur ein und speichere.

Du kannst diese Prozedur jetzt bereits ausführen. Gebe in das Kommandofenster z.B. das folgende ein:

<- $getNameOfWeekday(5)

-> ans = "Friday"

Wie erwartet ist der fünfte Tag der Woche ein Freitag. Wenn Du willst, kannst Du auch einen ungültigen Tag wie z.B. 0 eingeben. Du wirst dann die entsprechende Fehlermeldung sehen.

Wenn Du die Prozedur noch weiter betrachtest, fällt Dir der Kommentarblock oberhalb der Prozedur auf, der besonders hervorgehoben ist. Das ist ein Dokumentationskommentar, der später für die Tooltips und ggf. einen PDF-Export verwendet wird. Um den nicht von Hand abtippen zu müssen, kannst Du den entsprechenden Menüpunkt im Werkzeuge-Menü verwenden.

Ein reales Beispiel

Da das vorherige Beispiel eher akademischer Natur ist, schauen wir uns ein weiteres Beispiel an, dass Dir in der täglichen Arbeit eher einmal begegnen wird. Im "File Tools"-Package (das Du Dir über den Package Repository Browser im Packages-Menü installieren kannst) findest Du die Prozedur files/tools/findApp.nprc, bzw. $files~tools~findApp(), die wir uns hier einmal anschauen möchten. Was es mit der neuen Syntax $files~tools~findApp() auf sich hat, erfährst Du im nächsten Abschnitt.

Diese Prozedur sieht auf den ersten Blick komplexer aus. Das muss Dich jetzt aber nicht in Panik ausbrechen lassen. Wir gehen da Schritt für Schritt durch und diskutieren die neuen bzw. wichtigen Elemente.

Die beiden neuen Kommandos str und cst definieren lokale Variablen für diese Prozedur. Für jeden Datentyp gibt es ein Kommando: var für numerische Variablen, str für Zeichenketten, cst für Cluster und tab für Tabellen. Bisher waren alle unsere Variablen immer global, d.h. von überall aus her zugreifbar. Das ist auch kein Problem, weil lokale Variablen erst für Prozeduren wichtig werden. Lokal bedeutet hier, dass diese Variablen nur innerhalb dieser Prozedur verfügbar und auch erst mit dem entsprechenden Kommando deklariert sind. Alle anderen Variablen, die Du in Prozeduren deklarierst, sind automatisch global.

Die Funktion getenvvar() liest den Wert von Systemumgebungsvariablen; in diesem Fall die Variable %PATH%. In dieser Variable sind die Suchpfade für Dein System abgelegt, d.h. wenn Du in cmd.exe das Kommando calc eintippst, wird Windows in den Pfaden in %PATH% nach calc.exe suchen und den ersten Treffer nehmen, um das Programm auszuführen.

Du kannst auch grün hervorgehobene Methoden als Suffix an den Zeichenketten sehen, z.B. sEnvVar.fnd() oder sEnvVar.splt(). Diese dienen zur einfacheren Lesbarkeit und sind nur Aliase für die Zeichenkettenfunktionen strfnd() und split(). Es gibt auch solche Methoden für Tabellen, die dann sogar neue Funktionalitäten bereitstellen.

Die Funktion locate() sucht (abhängig vom dritten Parameter) nach dem zweiten Parameter in der Liste von Zeichenketten im ersten Parameter und gibt den entsprechenden Index zurück. Hier wurde der Inhalt von %PATH% in einzelne Zeichenketten aufgesplittet (sEnvVar.splt()) und dann in dieser Liste nach dem Suchpfad gesucht.

Im else-Zweig siehst Du das Kommando dialog. Damit kannst Du einen graphischen Dialog für den User öffnen. Das kann eine einfache MessageBox sein oder ein Auswahldialog. Hier wurde ein Dateidialog (type=filedialog) geöffnet, in dem der Nutzer nach der Datei suchen soll.

Abschließend wird noch geprüft, ob die gesuchte Datei an dem gefundenen oder vorgegebenen Pfad tatsächlich vorhanden ist (findfile()) und falls nicht, wird eine leere Zeichenkette zurückgegeben. Wir hätten auch hier throw verwenden können, aber dann müsste der Nutzer der Prozedur immer ein try-catch-Statement um diese Prozedur setzen. Da try-catch recht ineffizient ist und ggf. auch erwartet wird, dass die Datei nicht existiert, wäre throw aus Sicht der Verständlichkeit eher widersprüchlich.

Namespaces

Wir hatten Dir versprochen, dass wir die neue Syntax $files~tools~findApp() erklären würden. Hierbei handelt es sich um sogenannte Namespaces (in manchen anderen Programmiersprachen auch Module genannt) vor dem Prozedurnamen, die Dir helfen, Deine Prozeduren thematisch sauber zu organisieren. Dabei ist jeder Ordner automatisch ein Namespace und aufgrund dieser Eigenschaft sind auch alle Prozeduren in einem Namespace eindeutig. Wie du siehst werden Namespaces mittels der Tilde ~ voneinander und dem Dateinamen abgegrenzt.

Um eine Prozedur, die in einem bestimmten Namespace liegt, vom Kommandofenster aufzurufen, musst Du den Namespace explizit mit angeben:

<- $files~tools~findApp("Explorer", "Windows/System32", "explorer.exe")

-> ans = "c:/windows/system32/explorer.exe"

Warum diese zusätzliche Strukturierung? Es würde doch wie bei MATLAB genügen, Prozeduren gleichen Namens in anderen Ordnern abzulegen und dann trotzdem global verfügbar zu machen. Das hat drei Gründe:

  • Es wäre nicht eindeutig, welche der Prozeduren gleichen Namens verwendet werden soll. Die erste, die letzte oder die, die am besten passt?

  • Prozeduren gleichen Namens haben in unterschiedlichem Kontext unterschiedliche Bedeutungen, die sich ohne Kontext vielleicht nicht erschließt. So könnte $xml~find(sStr) in einer XML-Datei nach einem XML-Tag suchen, wohingegen $www~find(sStr) z.B. die Suchmaschine Deines Vertrauens aufruft. Ohne den Namensraum xml oder www müsste man längere und komplexere Namen vergeben ($findInXml() oder $findInWww()). Aber wer denkt schon frühzeitig daran, dass es mal zu einer Namenskollision mit einer Prozedur in einem anderen Ordner kommen könnte?

  • Durch Namespaces können wir auch das Konzept privater Prozeduren einführen (dazu brauchst Du den private-Flag), die nur von innerhalb eines Namensraumes verwendbar sind.

Hinweis: Neben den expliziten Namensräumen gibt es noch zwei weitere Namensräume: this und thisfile. Der Namensraum this ist immer identisch zum Namensraum der aktuellen Prozedur. Das erleichtert die Arbeit mit relativen Namensräumen. Der Namensraum thisfile bezeichnet die Prozeduren der aktuellen Datei; erlaubt damit den Zugriff auf lokal definierte Prozeduren.

Fortsetzung folgt ...