Scripts and procedures
We'll show you how it's done!
Automation through scripts
What we have learned so far is directly useful and enables evaluations of all kinds, but interactive operation is neither particularly pleasant nor error-resistant in the long run. For example, a small typing error can quickly lead to having to re-enter the last 20 expressions. For this there is the input chronicle, but nevertheless this is unnecessarily laborious.
Therefore we show you the next logical step and turn to the editor. This tool is quite powerful and supports you in many ways. First and foremost, however, it is used to put NumeRe code into an easily repeatable form: NumeRe scripts. In such a script you can simply type the expressions and commands you want to string together line by line, save the script and let NumeRe execute it. You can also comment your code so that it can be understood in the future.
Yes, yes. We know. Code should be written in a self-explanatory way. But seriously: what is easier to read - a tapeworm of code without any further information or code that is occasionally interrupted by one or two lines of explanatory continuous text? The fact is that code is read much more often than it is written, and it should be optimally prepared for that.
Enough of the preliminaries. Let's imagine you want to plot the three possible variants of the Kepler orbits. Of course you want to change to polar coordinates if possible and also play with the parameters of the Kepler orbit to find the transitions between the variants.
So create a new script in the editor (e.g. in the file menu as New/New Script) and name it e.g. "kepler". A file will be opened in the editor named "kepler.nscr". This is your new script, in which we will insert the necessary lines of code.
In this file you will find block comments within #* ... *#, while line comments start with the character sequence ##. The block comments describe the header and footer of the file. Comments have no influence on the script. So you can delete them if you want.
Then copy the code below into the script and save it. You can also run it now if you want. You should then be able to see an ellipse in a polar coordinate system like in the right figure.
But you probably also want to know what exactly the new elements in this script do. The command lclfunc defines a new function like define with the only difference that the function is only defined within the current code unit (script or procedure).
The character sequence \\ at the end of the second-to-last line indicates that this line is not yet finished and will be continued in the next line. This helps to keep long expressions manageable. You can of course wrap an expression several times and thus spread it over many lines.
The parameters in the last line control the appearance of the plot. First they reset all previous settings (with reset), then a grid (using grid) and polar coordinates (coords=polar) are activated. The number of samples is increased to create a smoother image and also it is changed to a square window aspect ratio (aspect=1) so that the polar coordinates actually look circular.
Congratulations again! You have just written your first script. Now you can just play with the Kepler orbit parameters. For example, you could replace eps_ell() with eps_hyp(), which would make the ellipse a hyperbola.
Programming with control flow statements
Of course you don't want to write only linear code, but depending on conditions you want to run different code or the same code several times. This task is performed by control flow statements.
NumeRe offers 5 different control flow statements, all with different purposes. You can see these statements on the right side in form of a rudimentary pseudo code. Also, we have written you in the comments in which cases which branches are executed.
All statements have in common that they can be nested as often as you like (of course there are memory limits) and that after a control flow statement the code continues normally.
Let's now take a closer look at the different statements:
if statement: Depending on whether the condition directly after if is true, the content of the contained block is executed. If the condition is false, any subsequent elseif conditions are checked and jumped to their branches if necessary. If these are also all false, the else branch is used, if available.
for statement: As long as the count index i is within the start and end index, the content of the contained block is executed repeatedly and after each iteration the count index is incremented by 1 (or decremented if i2 < i1). On entering the for statement, the count index is always set to the start index.
while statement: As long as the condition immediately after while is true, the content of the contained block is executed repeatedly. The condition is checked at the beginning of each iteration.
switch statement: Depending on the value of the expression immediately after switch, the first case whose value matches the calculated value is selected and code execution continues from that case until either a break or the corresponding endswitch is found (the latter is known as fallthrough). If no value matches, execution jumps to default, if any. The switch statement therefore behaves more like its C++ counterpart and not like the MATLAB implementation.
try statement: This statement always executes the code in the first block. If an error occurs, the execution jumps to the first catch block whose type matches the error type or is a generic catch block without an error type. Here then an appropriate treatment of the error can take place. If there is no matching catch block, execution continues to jump outward until either a matching catch block is found or the command window is reached. In the latter case, an error message is then output as usual.
Abstraction and Advanced Programming: Procedures
NumeRe scripts are quite helpful when it comes to running the same code several times and editing it comfortably at the same time. Now scripts have the disadvantage that from one script no other script can be started. So in order not to have to copy scripts into each other all the time, we need more advanced programming: NumeRe procedures.
NumeRe procedures are files of type *.nprc, which are stored in the procedure library (the path behind <procpath>). To create a procedure, you have to create a procedure file with the same name, e.g. my_proc.nprc for a procedure named $my_proc(). This can also be easily done from the file menu. Just specify the desired name of your procedure and NumeRe will create the appropriate procedure file with a template for you.
Above you can see the generic structure of a procedure. The header of a procedure contains the name, the argument list and additional flags. The name obviously defines the identifier under which the procedure is called. The argument list describes the possible arguments and their default values in the form $PROCEDURE(ARG1, ARG2 = DEFVAL2, ARG3, ...). As you can see, it is not necessary to specify a default value for each argument. You could then call this procedure in these forms, for example:
$PROCEDURE(VAL1, VAL2, VAL3, ...)
$PROCEDURE(VAL1, , VAL3, ...)
In the second case then for ARG2 automatically DEFVAL2 is taken as value. The empty argument is only necessary if values are to be passed for the subsequent arguments. A procedure of the form $PROCEDURE(ARG1, ARG2 = DEFVAL2, ARG3 = DEFVAL3) can be called in these variants, whereby the default values are then used for the missing values:
$PROCEDURE(VAL1, VAL2, VAL3)
$PROCEDURE(VAL1, , VAL3)
You can use the flags as the last element of the procedure header if you want to specify the procedure further. As flags exist e.g. inline, test, mask, event, private and a few more. Details can be found later in the internal documentation of the procedures (help procedure). For the first steps the use of flags is not necessary.
In the actual procedure body you can see the return command. This command defines the return value and will leave the procedure at this point. You can use the command several times per procedure with different values and types, but note that NumeRe will always leave the procedure at its places.
This should suffice for abstract explanations for now. We will implement a simple example at this point. We want to develop a procedure that returns the name of the weekday when we pass it the corresponding number. To do this, you first have to create the corresponding procedure file. In this example we use the name $getNameOfWeekday. Enter this and NumeRe will create the corresponding file for you. Then add the code below for the procedure and save.
You can already execute this procedure now. For example, enter the following into the command window:
-> ans = "Friday"
As expected, the fifth day of the week is a Friday. If you want, you can also enter an invalid day like 0. You will then see the corresponding error message.
If you look at the procedure further, you will notice the comment block above the procedure that is highlighted. This is a documentation comment that will be used later for the tooltips and possibly a PDF export. To avoid typing it by hand, you can use the corresponding menu item in the Tools menu.
A real example
Since the previous example is rather academic, let's take a look at another example that you are more likely to encounter in your daily work. In the "File Tools" package (which you can install via the Package Repository Browser in the Packages menu) you will find the procedure files/tools/findApp.nprc, or $files~tools~findApp(), which we will take a look at here. What the new syntax $files~tools~findApp() is all about, you will learn in the next section.
This procedure looks more complex at first sight. But that doesn't have to make you panic. We will go through it step by step and discuss the new or important elements.
The two new commands str and cst define local variables for this procedure. There is a command for each data type: var for numeric variables, str for strings, cst for clusters and tab for tables. So far, all our variables have always been global, i.e. accessible from anywhere. This is not a problem, because local variables only become important for procedures. Local means here that these variables are only available within this procedure and are also only declared with the corresponding command. All other variables that you declare in procedures are automatically global.
The function getenvvar() reads the value of system environment variables; in this case the variable %PATH%. In this variable the search paths for your system are stored, i.e. if you type the command calc in cmd.exe, Windows will search for calc.exe in the paths in %PATH% and take the first hit to execute the program.
You may also see green highlighted methods as suffixes to the strings, e.g. sEnvVar.fnd() or sEnvVar.splt(). These are for ease of reading and are just aliases for the string functions strfnd() and split(). There are also such methods for tables, which then even provide new functionalities.
The locate() function searches (depending on the third parameter) for the second parameter in the list of strings in the first parameter and returns the corresponding index. Here the content of %PATH% was split into single strings (sEnvVar.splt()) and then searched in this list for the search path.
In the else branch you see the dialog command. With this you can open a graphical dialog for the user. This can be a simple MessageBox or a selection dialog. Here a file dialog (type=filedialog) was opened, where the user should search for the file.
Finally, we check if the searched file actually exists at the found or given path (findfile()) and if not, an empty string is returned. We could have used throw here as well, but then the user of the procedure would always have to put a try-catch statement around this procedure. Since try-catch is rather inefficient and it may also be expected that the file does not exist, throw would be rather contradictory from the point of view of comprehensibility.
We promised you that we would explain the new syntax $files~tools~findApp(). These are so called namespaces (in some other programming languages also called modules) in front of the procedure name, which help you to organize your procedures thematically clean. Every folder is automatically a namespace and because of this property all procedures in a namespace are unique. As you can see namespaces are separated from each other by the tilde ~ and the filename.
To call a procedure that is located in a certain namespace from the command window, you must explicitly specify the namespace:
<- $files~tools~findApp("Explorer", "Windows/System32", "explorer.exe")
-> ans = "c:/windows/system32/explorer.exe"
Why this additional structuring? It would be sufficient, as in MATLAB, to store procedures with the same name in other folders and then make them globally available. There are three reasons for this:
It would not be clear which of the procedures with the same name should be used. The first, the last or the one that fits best?
Procedures with the same name have different meanings in different contexts, which may not be obvious without context. For example, $xml~find(sStr) might search for an XML tag in an XML file, whereas $www~find(sStr) might call the search engine you trust, for example. Without the namespace xml or www you would have to use longer and more complex names ($findInXml() or $findInWww()). But who thinks early on that there might be a name collision with a procedure in another folder?
Namespaces also allow us to introduce the concept of private procedures (you need the private flag for this), which can only be used from within a namespace.
Note: In addition to explicit namespaces, there are two other namespaces: this and thisfile. The namespace this is always identical to the namespace of the current procedure. This makes it easier to work with relative namespaces. The namespace thisfile denotes the procedures of the current file; thus, it allows access to locally defined procedures.