Programming

We'll show you how it's done!

What is the difference between define and declare? And what is a procedure?

With the command define you can create globally valid functions, which can then be used like any other function in NumeRe. But you can't overwrite predefined functions, but you can reference them. What is a function, you ask? A function is a mathematical expression that maps arguments uniquely to its values and is representable by a single expression in NumeRe. They are represented in NumeRe by their name and argument brackets, e.g. sin(x), arctan(x), norm(x,y,z,...).

You use the declare command to define constants in your code that you want to use repeatedly or that do not indicate their use by their value. Using named constants makes your code much more maintainable and readable. Unlike define, the constants that declare creates apply only to the current file. So you can define truly global constants only as functions with zero arguments.

Procedures, on the other hand, which are represented by a procedure $NAME(ARGS) ... endprocedure block can do much more than define functions can. Procedures can be implemented globally and restricted to the current file. You can use them when you want to handle more challenging topics, like developing an application or a plugin. Procedures can compute a wide variety of values or act on the original arguments. However, you cannot use them as plot or fit functions.

How can I make my code more readable?

Readable code is in principle a rather subjective topic. Some like CamelCase others rather like snake_case for their variables. Then there are those who type their variables (i.e. prefix them with characters to express the type, e.g. nInteger or fFloat) and those who strictly refuse to do so. In fact, prefixing variable types tempts to give them not very meaningful names. However, meaningful names for variables are the be-all and end-all of good code.

What can you do to make your code readable "for the masses"? We can give you the following tips:

  • Use meaningful variable names. We sometimes see that variable names are a, b or c. However, for someone reading the code for the first time, these names are not helpful. It is better to name variables according to their content, e.g. fAmplitude, fPhase or nFieldCount.

  • Limit the line length. Of course you can write many things in one line, but this does not improve the structure. You can use the character sequence \\ to break a line at this point and continue in the next line. If you have the choice, break before operators so that continued lines start with an operator and the continuation is easier to see. As a good guideline, use line lengths of about 100 characters.

  • Do not use obscure literal constants. What is a literal constant, you ask? It's a number or string that appears so directly in the code and is not part of a variable assignment, e.g. in strfnd("myliteral", sInputLine) the first argument is a literal constant. Why is this stupid, you ask? Well, especially with numbers you can rarely make sense of this number from its value alone, which makes understanding the code immensely difficult. If, for example, 21 appears in the code as a literal constant, you can't do much with it. But if it says halfMeaningOfLife it is rather clear what 21 is supposed to mean. But there is another reason why literal constants are a problem: if you find out that the constant was wrong, you have to search for it in the whole code. Variable values, on the other hand, can be changed comfortably in a single place. "Constant variables" clutter your workspace, you say? That is correct and solvable with the declare command, which creates a constant for the current file.

  • Use the auto-formatting feature. Of course, your code doesn't need spaces to be executable. However, it can help the reader immensely if a few spaces (and lines) highlight the structure of the code. You can do this manually, or you can use the feature in the tools menu or by using CTRL-SHIFT-F

How do I write a procedure that solves my problem?

We certainly cannot give a generically valid answer to this question. If it were possible, there would be no more problems for us to solve. However, we can show an example that explains the essential elements of a procedure and shows its structure. As a problem we choose the rather simple writing of logging messages to a file (a complete solution is also available at File tools).

As a first step, since this procedure should be globally callable, we need a new procedure file that must have the same name as the procedure. In this example we name the procedure $logger(), so we create a logger.nprc in the procedure path (this can also be done comfortably with the corresponding functions in the file menu). Normally we would select the appropriate namespace for this, but in this example we use the main namespace.

If we have created the procedure file with the appropriate menu option, the procedure body is already in the file. Otherwise we write

procedure $logger()

endprocedure

to the file. With this we have created the procedure and after saving it we can already call it in the console via $logger(), where the result must be 1 (i.e. true). This is the default return value of any procedure. But we don't need a return value in our case, so we add the line return void; between the two lines (if there is already return true; in the file, we just replace that).

Now we have to think about the arguments. We provide three: the destination filename, the actual message, and the logging level. With this we modify the first line as follows: procedure $logger(_sFileName, _sMessage, _sLevel). The underscores are not mandatory. They only serve to uniquely identify the arguments.

We want the logging level to appear in uppercase, so we transform it via _sLevel = to_uppercase(_sLevel);. Also, we want the current time to appear before the logging level and the message, so we write

_sMessage = timeformat("YYYY-MM-DD HH:mm:ss", time()) + " " + _sLevel + ": " + _sMessage;

The plus in the expression concatenates the different strings into one. Now all we have to do is write the message to the destination file. But before that we want the logging levels "ERROR" and "WARN" to be written to the console as a warning. We achieve this by inserting the following block

if (_sLevel == "ERROR" || _sLevel == "WARN")

warn _sMessage;

endif

before the return void; statement. Now right after that we add the write command with the message, the filename and the mode=app and our procedure is done, whereof of course a few comments about what we have done are certainly not wrong.

More to come ...