Go to the first, previous, next, last section, table of contents.

Basic Ideas

Yorick is an interpreted programming language. With Yorick you can read input (usually lists of numbers) from virtually any text or binary file, process it, and write the results back to another file. You can also plot results on your screen.

Yorick expression and flow control syntax is similar to the C programming language, but the Yorick language lacks declaration statements. Also, Yorick array index syntax more closely resembles Fortran than C.

Simple Statements

An interpreter immediately executes each line you type. Most Yorick input lines define a variable, invoke a procedure, or print the value of an expression.

Defining a variable

The following five Yorick statements define the five variables c, m, E, theta, and file:

c= 3.00e10;  m= 9.11e-28
E= m*c^2
theta= span(0, 6*pi, 200)
file= create("damped.txt")

Variable names are case sensitive, so E is not the same variable as e. A variable name must begin with a letter of the alphabet (either upper or lower case) or with an underscore (_); subsequent characters may additionally include digits.

A semicolon terminates a Yorick statement, so the first line contains two statements. To make Yorick statements easier to type, you don't need to put a semicolon at the end of most lines. However, if you are composing a Yorick program in a text file (as opposed to typing directly to Yorick itself, a semicolon at the end of every line reduces the chances of a misinterpretation, and makes your program easier to read.

Conversely, a new line need not represent the end of a statement. If the line is incomplete, the statement automatically continues on the following line. Hence, the second and third lines above could have been typed as:

E=
  m *
  c^2
theta= span(0, 6*pi,
            200)

In the second line, * and ^ represent multiplication and raising to a power. The other common arithmetic operators are +, -, / (division), and % (remainder or modulo). The rules for forming arithmetic expressions with these operators and parentheses are the same in Yorick as in Fortran or C (but note that ^ does not mean raise to a power in C, and Fortran uses the symbol ** for that operation).

The span function returns 200 equally spaced values beginning with 0 and ending with 6*pi. The variable pi is predefined as 3.14159...

The create function returns an object representing the new file. The variable file specifies where output functions should write their data. Besides numbers like c, m, and E, or arrays of numbers like theta, or files like file, Yorick variables may represent several other sorts of objects, taken up in later chapters.

The = operator is itself a binary operator, which has the side effect of redefining its left operand. It associates from right to left, that is, the rightmost = operation is performed first (all other binary operators except ^ associate from left to right). Hence, several variables may be set to a single value with a single statement:

psi= phi= theta= span(0, 6*pi, 200)

When you define a variable, Yorick forgets any previous value and data type:

phi= create("junk.txt")

Invoking a procedure

A Yorick function which has side effects may sensibly be invoked as a procedure, discarding the value returned by the function, if any:

plg, sin(theta)*exp(-theta/6), theta
write, file, theta, sin(theta)*exp(-theta/6)
close, file

The plg function plots a graph on your screen -- in this case, three cycles of a damped sine wave. The graph is made by connecting 200 closely spaced points by straight lines.

The write function writes a two column, 200 line table of values of the same damped sine wave to the file `damped.txt'. Then close closes the file, making it unavailable for any further write operations.

A line which ends with a comma will be continued, to allow procedures with long argument lists. For example, the write statement could have been written:

write, file, theta,
             sin(theta)*exp(-theta/6)

A procedure may be invoked with zero arguments; several graphics functions are often used in this way:

hcp
fma

The hcp function writes the current graphics window contents to a "hardcopy file" for later retrieval or printing. The fma function stands for "frame advance" -- subsequent plotting commands will draw on a fresh page. Normally, plotting commands such as plg draw on top of whatever has been drawn since the previous fma.

Printing an expression

An unadorned expression is also a legal Yorick statement; Yorick prints its value. In the preceding examples, only the characters you would type have been shown; to exhibit the print function and its output, I need to show you what your screen would look like -- not only what you type, but what Yorick prints. To begin with, Yorick prompts you for input with a > followed by a space (see section Prompts). In the examples in this section, therefore, the lines which begin with > are what you typed; the other line(s) are Yorick's responses.

> E
8.199e-07
> print, E
8.199e-07
> m;c;m*c^2
9.11e-28
3.e+10
8.199e-07
> span(0,2,5)
[0,0.5,1,1.5,2]
> max(sin(theta)*exp(-theta/6))
0.780288

In the span example, notice that an array is printed as a comma delimited list enclosed in square brackets. This is also a legal syntax for an array in a Yorick statement. For numeric data, the print function always displays its output in a format which would be legal in a Yorick statement; you must use the write function if you want "prettier" output. Beware of typing an expression or variable name which is a large array; it is easy to generate lots of output (you can interrupt Yorick by typing control-c if you do this accidentally, see section Starting, stopping, and interrupting Yorick).

Most non-numeric objects print some useful descriptive information; for example, before it was closed, the file variable above would have printed:

> file
write-only text stream at:
  LINE: 201  FILE: /home/icf/munro/damped.txt

As you may have noticed, printing a variable and invoking a procedure with no arguments are syntactically indistinguishable. Yorick decides which operation is appropriate at run time. Thus, if file had been a function, the previous input line would have invoked the file function; as it was not a function, Yorick printed the file variable. Only an explicit use of the print function will print a function:

> print, fma
builtin fma()

Flow Control Statements

Almost everything you type at Yorick during an interactive session will be one of the simple Yorick statements described in the previous section -- defining variables, invoking procedures, and printing expressions. However, in order to actually use Yorick you need to write your own functions and procedures. You can do this by typing Yorick statements at the keyboard, but you should usually put function definitions in a text file. Suggestions for how to organize such "include" files will be the topic of the next section. This section introduces the Yorick statements which define functions, conditionals, and loops.

Defining a function

Consider a damped sine wave. It describes the time evolution of an oscillator, such as a weight on a spring, which bobs up and down for a while after you whack it. The basic shape of the wave is determined by the Q of the oscillator; a high Q means there is little friction, and the weight will continue bobbing for many cycles, while a low Q means a lot of friction and few cycles. The amplitude of the oscillation is therefore a function of two parameters -- the phase (time in units of the natural period of the oscillator), and the Q:

func damped_wave(phase, Q)
{
  nu= 0.5/Q;
  omega= sqrt(1.-nu*nu);
  return sin(omega*phase)*exp(-nu*phase);
}

Within a function body, I terminate every Yorick statement with a semicolon (see section Defining a variable).

The variables phase and Q are called the parameters of the function. They and the variables nu and omega defined in the first two lines of the function body are local to the function. That is, calling damped_wave will not change the values of any variables named phase, Q, nu, or omega in the calling environment.

In fact, the only effect of calling damped_wave is to return its result, which is accomplished by the return statement in the third line of the body. That is, calling damped_wave has no side effects. You can use damped_wave in expressions like this:

> damped_wave(1.5, 3)
0.775523
> damped_wave([.5,1,1.5,2,2.5], 3)
[0.435436,0.705823,0.775523,0.659625,0.41276]
> q5= damped_wave(theta,5)
> fma; plg, damped_wave(theta,3), theta
> plg, damped_wave(theta,1), theta

The last two lines graphically compare Q=3 oscillations to Q=1 oscillations.

Notice that the arguments to damped_wave may be arrays. In this case the result will be the array of results for each element of input; hence q5 will be an array of 200 numbers. Nor is Yorick confused by the fact that the phase argument (theta) is an array, while the Q argument (5) is a scalar. The precise rules for "conformability" between two arrays will be described later (see section Broadcasting and conformability); usually you get what you expected.

In this case, as Yorick evaluates damped_wave, nu and omega will both be scalars, since Q is a scalar. On the other hand, omega*phase and nu*phase become arrays, since phase is an array. Whenever an operand is an array, an arithmetic operation produces an array as its result.

Defining Procedures

Any Yorick function may be invoked as a procedure; the return value is simply discarded. Calling damped_wave as a procedure would be pointless, since it has no side effects. The converse case is a function to be called only for its side effects. Such a function need not have an explicit return statement:

func q_out(Q, file_name)
{
  file= create(file_name);
  write, file, "Q= "+pr1(Q);
  write, file, "   theta       amplitude";
  write, file, theta, damped_wave(theta,Q);
}

The pr1 function (for "print one value") returns a string representation of its numeric argument, and the + operator between two strings concatenates them.

You would invoke q_out with the line:

q_out, 3, "q.out"

Besides lacking an explicit return statement, the q_out function has two other peculiarities worth mentioning:

First, the variable theta is never defined. But neither are the functions create and write. Any symbol not defined within a function is external to the function. As already noted, parameters and variables defined within the function are local to the function. Here, theta is the 200 element array of phase angles defined before q_out was called. You can change theta (both its dimensions and its values) before the next call to q_out; that is, an external reference may change between calls to a function that uses it.

Second, file is never explicitly closed. When a function returns, its local variables (such as file) disappear; when a file object disappears, the associated file automatically closes.

Conditional Execution

The design of the q_out function can be improved. As written, each output file will contain only a single wave. You might want the option of writing several waves into a single file. Consider this alternative:

func q_out(Q, file)
{
  if (!is_stream(file)) file= create(file);
  write, file, "Q= "+pr1(Q);
  write, file, "   theta       amplitude";
  write, file, theta, damped_wave(theta,Q);
  return file;
}

The if statement executes its body (the redefinition of file) if and only if its condition (!is_stream(file)) is true. Any scalar number may serve as a condition -- a non-zero value is "true", and the value zero is "false".

The is_stream function returns 1 (true) when its argument is a file object (a "data stream"), and 0 (false) otherwise. In particular, if file is a text string (like "q.out"), is_stream returns 0. The unary operator ! is logical negation, that is, "not".

Hence, if the file argument is not already a file object, the new q_out presumes it is the name of a file, which it creates, redefining file as the associated file object. Thus, after the first line of the function body, file will be a file object, even if a file name was passed into the function. Furthermore, since the parameter file is local to q_out, none of this hocus pocus will have any effect outside q_out.

The second trick in the new q_out is the reappearance of a return statement. The original calling sequence:

q_out, 3, "q.out"

has the same result as before -- the if condition is true, so the file is created, then the wave data is written. This time the file object is returned, only to be discarded because q_out was invoked as a procedure. When the file object disappears, the file closes. But if q_out were invoked as a function, the file object can be saved, which keeps the file open:

f= q_out(3,"q.out")
q_out,2,f
q_out,1,f
close,f

Now the file `q.out' contains the Q=3 wave, followed by the Q=2 and Q=1 waves. In the second and third calls to q_out, the file parameter is already a file object, so the if condition is false, and create is not called. Notice that the file does not close when the return value from the second (or third) call is discarded; the variable f refers to the same file object as the discarded return value. Without an explicit call to close, a file only closes when the final reference to it disappears.

General if and else constructs

The most general form of the if statement is:

if (condition) statement_if_true;
else statement_if_false;

When you need to choose among several alternatives, make the else clause itself an if statement:

if (condition_1) statement_if_1;
else if (condition_2) statement_if_2;
else if (condition_3) statement_if_3;
...
else statement_if_none;

The final else is always optional -- if it is not present, nothing at all happens if none of the conditions is satisfied.

Often you need to execute more than one statement conditionally. Quite generally in Yorick, you may group several statements into a single compound statement by means of curly braces:

{ statement_1; statement_2; statement_3; statement_4; }

Ordinarily, both curly braces and each of the statements should be written on a separate line, with the statements indented to make their membership in the compound more obvious. Further, in an if-else construction, when one branch requires more than one statement, you should write every branch as a compound statement with curly braces. Therefore, for a four branch if in which the second if requires two statements, and the else three, write this:

if (condition_1) {
  statement_if_1;
} else if (condition_2) {
  statement_if_2_A;
  statement_if_2_B;
} else if (condition_3) {
  statement_if_3;
} else {
  statement_if_none_A;
  statement_if_none_B;
  statement_if_none_C;
}

The else statement has a very unusual syntax: It is the only statement in Yorick which depends on the form of the previous statement; an else must follow an if. Because Yorick statements outside functions (and compound statements) are executed immediately, you must be very careful using else in such a situation. If the if has already been executed (as it would be in the examples without curly braces), the following else leads to a syntax error!

The best way to avoid this puzzling problem is to always use the curly brace syntax of the latest example when you use else outside a function. (You don't often need it in such a context anyway.)

Combining conditions with && and ||

The logical operators && ("and") and || ("or") combine conditional expressions; the ! ("not") operator negates them. The ! has the highest precedence, then &&, then ||; you will need parentheses to force a different order.

(Beware of the bitwise "and" and "or" operators & and | -- these should never be used to combine conditions; they are for set-and-mask bit fiddling.)

The operators for comparing numeric values are == (equal), != (not equal), > (greater), < (less), >= (greater or equal), and <= (less or equal). These all have higher precedence than &&, but lower than ! or any arithmetic operators.

if ((a<b && x>a && x<b) || (a>b && x<a && x>b))
  write, "x is between a and b"

Here, the expression for the right operand to && will execute only if its left operand is actually true. Similarly, the right operand to || executes only if its left proves false. Therefore, it is important to order the operands of && and || to put the most computationally expensive expression on the right -- even though the logical "and" and "or" functions are commutative, the order of the operands to && and || can be critical.

In the example, if a>b, the x>a and x<b subexpressions will not actually execute since a<b proved false. Since the left operand to || was false, its right operand will be evaluated.

Despite the cleverness of the && and || operators in not executing the expression for their right operands unless absolutely necessary, the example has obvious inefficiencies: First, if a>=b, then both a<b and a>b are checked. Second, if a<b, but x is not between a and b, the right operand to || is evaluated anyway. Yorick has a ternary operator to avoid this type of inefficiency:

expr_A_or_B= (condition? expr_A_if_true : expr_B_if_false);

The ?: operator evaluates the middle expression if the condition is true, the right expression otherwise. Is that so? Yes : No. The efficient betweeness test reads:

if (a<b? (x>a && x<b) : (x<a && x>b))
  write, "x is between a and b";

Loops

Most loops in Yorick programs are implicit; remember that operations between array arguments produce array results. Whenever you write a Yorick program, you should be suspicious of all explicit loops. Always ask yourself whether a clever use of array syntax could have avoided the loop.

To illustrate an appropriate Yorick loop, let's revise q_out to write several values of Q in a single call. Incidentally, this sort of incremental revision of a function is very common in Yorick program development. As you use a function, you notice that the surrounding code is often the same, suggesting a savings if it were incorporated into the function. Again, a careful job leaves all of the previous behavior of q_out intact:

func q_out(Q, file)
{
  if (!is_stream(file)) file= create(file);
  n= numberof(Q);
  for (i=1 ; i<=n ; ++i) {
    write, file, "Q= "+pr1(Q(i));
    write, file, "   theta       amplitude";
    write, file, theta, damped_wave(theta,Q(i));
  }
  return file;
}

Two new features here are the numberof function, which returns the length of the array Q, and the array indexing syntax Q(i), which extracts the i-th element of the array Q. If Q is scalar (as it had to be before this latest revision), numberof returns 1, and Q(1) is the same as Q, so scalar Q works as before.

But the most important new feature is the for loop. It says to initialize i to 1, then, as long as i remains less than or equal to n, to execute the loop body, which is a compound of three write statements. Finally, after each pass, the increment expression ++i is executed before the i<=n condition is evaluated. ++i is equivalent to i=i+1; --i means i=i-1.

A loop body may also be a single statement, in which case the curly braces are unecessary. For example, the following lines will write the Q=3, Q=2, and Q=1 curves to the file `q.out', then plot them:

q_list= [3,2,1]
q_out, q_list, "q.out"
fma; for(i=1;i<=3;++i) plg, damped_wave(theta,q_list(i)), theta

A for loop with a plg body is the easiest way to overlay a series of curves. After the three simple statement types, for statements see the most frequent direct use from the keyboard.

The while and do while statements

The while loop is simpler than the for loop:

while (condition) body_statement

The body_statement -- very oten a compound statement enclosed in curly braces -- executes over and over until the condition becomes false. If the condition is false to begin with, body_statement never executes.

Occasionally, you want the body_statement to execute and set the condition before it is tested for the first time. In this case, use the do while statement:

do {
  body_statement_A;
  body_statement_B;
  ...etc...
} while (condition);

The for statement

The for statement is a "packaged" form of a while loop. The meaning of this generic for statement is the same as the following while:

for (start_statements ; condition ; step_statements) body_statements

start_statments
while (condition) {
  body_statements
  step_statements
}

There only two reasons to prefer for over while: Most importantly, for can show "up front" how a loop index is initialized and incremented, rather than relegating the increment operation (step_statements) to the end of the loop. Secondly, the continue statement (see section Using break, continue, and goto) branches just past the body of the loop, which would include the step_statements in a while loop, but not in a for loop.

In order to make Yorick loop syntax agree with C, Yorick's for statement has a syntactic irregularity: If the start_statements and step_statements consist of more than one statement, then commas (not semicolons) separate the statements. (The C comma operator is not available in any other context in Yorick.) The two semicolons separate the start, condition, and step clauses and must always be present, but any or all of the three clauses themselves may be blank. For example, in order to increment two variables i and j, a loop might look like this:

for (i=1000, j=1 ; i>j ; i+=1000, j*=2);

This example also illustrates that the body_statements may be omitted; the point of this loop is merely to compute the first i and j for which the condition is not satisfied. The trailing semicolon is necessary in this case, since otherwise the line would be continued (on the assumption that the loop body was to follow).

The += and *= are special forms of the = operator; i=i+1000 and j=j*2 mean the same thing. Any binary operator may be used in this short-hand form in order to increment a variable. Like ++i and --i, these are particularly useful in loop increment expressions.

Using break, continue, and goto

The break statement jumps out of the loop containing it. The continue statement jumps to the end of the loop body, "continuing" with the next pass through the loop (if any). In deeply nested loops -- which are extremely rare in Yorick programs --- you can jump to more general places using the goto statement. For example, here is how to break out or continue from one or both levels of a doubly nested loop:

 for (i=1 ; i<=n ; ++i) {
   for (j=1 ; j<=m ; ++j) {
     if (skip_to_next_j_now) continue;
     if (skip_to_next_i_now) goto skip_i;
     if (break_out_of_inner_loop_now) break;
     if (break_out_of_both_loops_now) goto done;
     more_statements_A;
   }
   more_statements_B;
  skip_i:
 }
done:
 more_statements_C;

The continue jumps just after more_statements_A, the break just before more_statements_B.

Break and continue statements may be used to escape from while or do while loops as well.

If while tests the condition before the loop body, and do while checks after the loop body, you may have wondered what to do when you need to check the condition in the middle of the loop body. The answer is the "do forever" construction, plus the break statement (note the inversion of the sense of the condition):

for (;;) {
  body_before_test;
  if (!condition) break;
  body_after_test;
}

Variable scope

By default, dummy parameters and variables which are defined in a function body before any other use are local variables. Variables (or functions) used before their definition are external variables (see section Defining Procedures, see section Defining a function). A variable (or function) defined outside of all functions is just that -- external to all functions and local to none.

Whenever a function is called, Yorick remembers the external values of all its local variables, then replaces them by their local values. Thus, all of its local variables are potentially "visible" as external variables to any function it calls. When it returns, the function replaces all its local variables by the values it remembered. Neither that function, nor any function it calls can affect these remembered values; Yorick provides no means of "unmasking" a local variable.

The default rule for determining whether a variable should have local or external scope fails in two cases: First, you may want to redefine an external variable without looking at its value. Second, a few procedures set the values of their parameters when they return; it may appear to Yorick's parser that such a variable has been used without being defined, even though you intend it to be local to the function. The extern and local statements solve these two problems, respectively.

extern statements

Suppose you want to write a function which sets the value of the theta array used by all the variants of the q_out function:

func q_out_domain(start, stop, n)
{
  extern theta;
  theta= span(start, stop, n);
}

Without the extern statement, theta will be a local variable, whose external value is restored when q_out_domain returns (thus, the function would be a no-op). With the extern statement, q_out_domain has the intended side effect of setting the value of theta. The new theta would be used by subsequent calls to q_out.

Any number of variables may appear in a single extern statement:

extern var1, var2, var3, var4;

local statements

The save and restore functions store and retrieve Yorick variables in self-descriptive binary files (called PDB files). To create a binary file `demo.pdb', save the variables theta, E, m, and c in it, then close the file:

f= createb("demo.pdb")
save, f, theta, E, m, c
close, f

To open this file and read back the theta variable:

restore, openb("demo.pdb"), theta, c

For symmetry with save, the restore function has the unusual property that it redefines its parameters (except the first). Thus, theta is redefined by this restore statement, just as it would have been by a theta= statement. The Yorick parser does not understand this, which means that you must be careful when you place such a function call inside a function:

func q_out_file(infile, Q, outfile)
{
  if (!is_stream(infile)) infile= openb(infile);
  local theta;
  restore, infile, theta;
  return q_out(Q, outfile);
}

This variant of q_out uses the theta variable read from infile, instead of an external value of theta. Without the local declaration, restore would clobber the external value of theta. This way, when q_out_file calls q_out, it has remembered the external value of theta, but q_out "sees" the value of theta restored from infile. When q_out_file finally returns, it replaces the original value of theta intact.

Any number of variables may appear in a single local statement:

local var1, var2, var3, var4;

The Interpreted Environment

The Yorick program accepts only complete input lines typed at your keyboard. Typing a command to Yorick presumes a "command line interface" or a "terminal emulator" which is not a part of Yorick. I designed Yorick on the assumption that you have a good terminal emulator program. In particular, Yorick is much easier to use if you can recall and edit previous input lines; as in music, repitition with variations is at the heart of programming. My personal recommendation is shell-mode in GNU Emacs.

Therefore, Yorick inherits most of its "look and feel" from your terminal emulator. Yorick's distinctive prompts and error messages are described later in this section.

Any significant Yorick program will be stored in a text file, called an include file, after the command which reads it. Use your favorite text editor to create and modify your include files. Again, GNU Emacs is my favorite -- use its c-mode to edit Yorick include files as well as C programs. Just as C source file names should end in `.c' or `.h', and Fortran source file names should end in `.f', so Yorick include file names should end in `.i'.

This section begins with additional stylistic suggestions concerning include files. In particular, Yorick's help command can find documentation comments in your include files if you format them properly. All of the built-in Yorick functions, such as sin, write, or plg, come equipped with such comments.

Starting, stopping, and interrupting Yorick

Start Yorick by typing its name to your terminal emulator. When you are done, use the quit function to exit gracefully:

% yorick
 Yorick ready.  For help type 'help'.
> quit
%

You can interrupt Yorick at any time by typing Control-C; this causes an immediate runtime error (see section Error Messages). (Your terminal emulator and operating system must be able to send Yorick a SIGINT signal in order for this to work. On UNIX systems, you can set this character using the intr option of the stty command; Control-C is the usual setting.)

Include files

When you run Yorick in a window or Emacs environment, keep a window containing the include file you are writing, in addition to the window where Yorick is running. When you want to test the latest changes to your include file -- let's call it `damped.i' -- save the file, then move to your Yorick window and type:

#include "damped.i"

The special character # reminds you that the include directive is not a Yorick statement, like all other inputs to Yorick. Instead, #include directs Yorick to switch its input stream from the keyboard to the quoted file. Yorick then treats each line of the file exactly as if it were a line you had typed at the keyboard (including, of course, additional #include lines). When there are no more lines in the file, the input stream switches back to the keyboard.

While you are debugging an include file, you will ordinarily include it over and over again, until you get it right. This does no harm, since any functions or variables defined in the file will be replaced by their new definitions the next time you include the file, just as they would if you typed new definitions.

Ideally, your include files should consist of a series of function definitions and variable initializations. If you follow this discipline, you can regard each include file as a library of functions. You type #include once in order to load this library, making the functions defined there available, just like Yorick's built in functions. Many such libraries come with Yorick --- Bessel functions, cubic spline interpolators, and other goodies.

A sample include file

Here is `damped.i', which defines the damped sine wave functions we've been designing in this chapter:

/* damped.i */

local damped;
/* DOCUMENT damped.i -- compute and output damped sine waves
   SEE ALSO: damped_wave, q_out
 */

func damped_wave(phase, Q)
/* DOCUMENT damped_wave(phase, Q)
     returns a damped sine wave evaluated at PHASE, for quality factor Q.
     (High Q means little damping.)  The PHASE is 2*pi after one period of
     the natural frequency of the oscillator.  PHASE and Q must be
     conformable arrays.

   SEE ALSO: q_out
 */
{
  nu= 0.5/Q;
  omega= sqrt(1.-nu*nu);
  return sin(omega*phase)*exp(-nu*phase);  /* always zero at phase==0 */
}

func q_out(Q, file)
/* DOCUMENT q_out, Q, file
         or q_out(Q, file)
     Write the damped sine wave of quality factor Q to the FILE.
     FILE may be either a filename, to create the file, or a file
     object returned by an earlier create or q_out operation.  If
     q_out is invoked as a function, it returns the file object.

     The external variable
          theta
     determines the phase points at which the damped sine wave is
     evaluated; q_out will write two header lines, followed by
     two columns, with one line for each element of the theta
     array.  The first column is theta; the second is
     damped_wave(theta, Q).

     If Q is an array, the two header lines and two columns will
     be repeated for each element of Q.

   SEE ALSO: damped_wave
 */
{
  if (!is_stream(file)) file= create(file); /* file name --> object */
  n= numberof(Q);
  for (i=1 ; i<=n ; ++i) { /* loop on elements of Q */
    write, file, "Q= "+pr1(Q(i));
    write, file, "   theta       amplitude";
    write, file, theta, damped_wave(theta,Q(i));
  }
  return file;
}

You would type

#include "damped.i"

to read the Yorick statements in the file. The first thing you notice is that there are comments -- the text enclosed by /* */. If you take the trouble to save a program in an include file, you will be wise to make notes about what it's for and annotations of obscure statements which might confuse you later. Writing good comments is arguably the most difficult skill in programming. Practice it diligently.

Comments

There are two ways to insert comments into a Yorick program. These are the C style /* ... */ and the C++ style //:

// C++ style comments begin with // (anywhere on a line)
// and end at the end of that line.

E= m*c^2;   /* C style comments begin with slash-star, and
               do not end until start-slash, even if that
               is several lines later.  */
/* C style comments need not annotate a single line.
 * You should pick a comment style which makes your
 * code attractive and easy to read.  */

F= m*a;          // Here is another C++ style comment...
divE= 4*pi*rho;  /* ... and a final C style comment. */

I strongly recommend C++ style comments when you "comment out" a sequence of Yorick statements. C style comments do not nest properly, so you can't comment out a series of lines which contain comments:

/*
E= m*c^2;   /* ERROR -- this ends the outer comment --> */
F= m*a
*/ <-- then this causes a syntax error

The C++ style not only works correctly; it also makes it more obvious that the lines in question are comments:

// E= m*c^2;   /* Any kind of comment could go here.  */
// F= m*a;

Yorick recognizes one special comment: If the first line of an include file begins with #!, Yorick ignores that line. This allows Yorick include scripts to be executable on UNIX systems supporting the "pound bang" convention:

#!/usr/local/bin/yorick -batch
/* If this file has execute permission, UNIX will use Yorick to
 * execute it.  The Yorick function get_argv can be used to accept
 * command line arguments (see help, get_argv).  You might want
 * to use -i instead of -batch on the first line.  Read the help
 * on process_argv and batch for more information.  */
write, "The square root of pi is", sqrt(pi);
quit;

DOCUMENT comments

Immediately after (within a few lines of) a func, extern, or local statement (see section Variable scope), you may put a comment which begins with the eleven characters /* DOCUMENT. Although Yorick itself doesn't pay any more attention to a DOCUMENT comment than to any other comment, there is a function called help which does. What Yorick does do when it sees a func, extern, or local (outside of any function body), is to record the include file name and line number where that function or variable(s) are defined.

Later, when you ask for help on some topic, the help function asks Yorick whether it knows the file and line number where that topic (a function or variable) was defined. If so, it opens the file, goes to the line number, and scans forward a few lines looking for a DOCUMENT comment. If it finds one, it prints the entire comment.

The file damped.i has three DOCUMENT comments: one for a fictitious variable called damped, so that someone who saw the file but didn't know the names of the functions inside could find out, and one each for the damped_wave and q_out functions.

Near the end of most DOCUMENT comments, you will find a SEE ALSO: field which feeds the reader the names of related functions or variables which also have DOCUMENT comments. If you use these wisely, you can lead someone (often an older self) to all of the documentation she needs to be able to use your package.

This low-tech form of online documentation is surprisingly effective: easy to create, maintain, and use. As an automated step toward a more formal document, the mkdoc function (in the Yorick library include file `mkdoc.i') collects all of the DOCUMENT comments in one or more include files, alphabetizes them, adds a table of contents, and writes them into a file suitable for printing.

Where Yorick looks for include files

You can specify a complete path name (including directories) for the file in an #include directive. More usually, you will use a relative path name. In that case, Yorick tries to find the file relative to these four directories, in this order:

  1. Your current working directory.
  2. `~/Yorick', that is, the `Yorick' subdirectory of your home directory.
  3. `Y_SITE/include', where `Y_SITE' is the directory where Yorick was installed at your site (help will tell you where this is).
  4. `Y_SITE/contrib'

You can use the set_path command in your `custom.i' file in order to change this path, but you should be very cautious if you do this.

The `~/Yorick' directory is where you put all of the Yorick include files you frequently use, which have not been placed in the include or contrib directories at your site. You can also override an include file in one of these places by placing a file of the same name in your `~/Yorick' directory.

The `custom.i' file

When Yorick starts, the last thing it does before prompting you is to include the file `custom.i'. The default `custom.i' is in the `Y_SITE/include/custom.i'. (Yorick's help command will tell you the actual name of the `Y_SITE' directory at your site.)

If you create the directory `~/Yorick', you can place your own version of custom.i there to override the default. Always begin by copying the default file to your directory. Then add your customizations to the bottom of the default file. Generally, these customizations consist of #include directives for function libraries you use heavily, plus commands like pldefault to set up your plotting style preferences.

The help function

Use the help function to get online help. Yorick will parrot the associated DOCUMENT comment to your terminal:

> #include "damped.i"
> help, damped
  DOCUMENT damped.i -- compute and output damped sine waves
  SEE ALSO: damped_wave, q_out
defined at:   LINE: 3  FILE: /home/icf/munro/damped.i
>

Every Yorick function (including help!) has a DOCUMENT comment which describes what it does. Most have SEE ALSO: references to related help topics, so you can navigate your way through a series of related topics.

Whenever you want more details about a Yorick function, the first thing to do is to see what help says about it. Note that, in addition to the DOCUMENT comment, help also reports the full file name and line number where the function is defined. That way, if the comment doesn't tell you what you need to know, you can go to the file and read the complete definition of the function.

The help for help itself, which is the default topic, is of particular interest, even to a Yorick expert: it tells you the directory where you can find the include files for all of Yorick's library functions. Just type help, like it says when Yorick starts. One of the things you will find in Yorick's directory tree is a `doc' directory, which contains not only the source for this manual, but also alphabetized listings of all DOCUMENT comments. Read the README file in the `doc' directory.

The info function

If you want to know the data type and dimensions of a variable, use the info function. Unlike help, which is intended to tell you what a thing means, info simply tells what a thing is.

> info, theta
 array(double,200)
> info, E
 array(double)

Here, double means "double precision floating point number", which is the default data type for any real number. The default integer data type is called long. (Both these names come from the C language, which gives them a precise meaning.)

Notice that the scalar value E is, somewhat confusingly, called an "array". In fact, a scalar is a special case of an array with zero dimensions. The info function is designed to print a Yorick expression which will create a variable of the same data type and shape as its argument. Thus, array(double,200) in an expression would evaluate to an array of 200 real numbers, while array(double) is a real scalar (the values are always zero).

Using info on a non-numeric quantity (a file object, a function, etc.) results in the same output as the print function. If an array of numbers might be large, try info before print.

Prompts

Yorick occasionally prompts you with something other than >. The usual > prompt tells you that Yorick is waiting for a new input line. The other prompts alert you to unusual situations:

cont>
The previous input line was not complete; Yorick is waiting for its continuation before it does anything. Type several close brackets or control-C if you don't want to complete the line.
dbug>
When an error occurs during the execution of a Yorick program, Yorick offers you the choice of entering "debug mode". In this mode, you are "inside" the function where the error occurred. You can type any Yorick statement you wish, but until you explicitly exit from debug mode by means of the dbexit function, Yorick will prompt you with dbug> instead of its usual prompt. (see section How to respond to a runtime error)
quot>
It is possible to continue a long string constant across multiple lines. If you do, this will be your prompt until you type the closing ". Don't ever do this intentionally; use the string concatenation operator + instead, and break long strings into pieces that will fit on a single line.
comm>
For some reason, you began a comment on the previous line, and Yorick is waiting for the */ to finish the comment. I can't imagine why you would put comments in what you were typing at the terminal.

Shell commands, removing and renaming files

Yorick has a system function which you can use to invoke operating system utilities. For example, typing ls *.txt to a UNIX shell will list all files in your current working directory ending with `.txt'. Similarly, pwd prints the name of your working directory:

> system, "pwd"
/home/icf/munro
> system, "ls *.txt"
   damped.txt      junk.txt

This is so useful that there is a special escape syntax which automatically generates the system call, so you don't need to type the name system or all of the punctuation. The rule is, that if the first character of a Yorick statement is $ (dollar), the remainder of that line becomes a quoted string which is passed to the system function. Hence, you really would have typed:

> $pwd
/home/icf/munro
> $ls *.txt
   damped.txt      junk.txt

Note that you cannot use ; to stack $ escaped lines --- the semicolon will be passed to system. Obviously, this syntax breaks all of the ordinary rules of Yorick's grammar.

On UNIX systems, the system function (which calls the ANSI C standard library function of the same name) usually executes your command in a Bourne shell. If you are used to a different shell, you might be surprised at some of the results. If you have some complicated shell commands for which you need, say, the C-shell csh, just start a copy of that shell before you issue your commands:

> $csh
% ls *.txt
  damped.txt    junk.txt
% exit
>

As this example shows, you can start up an interactive program with the system function. When you exit that program, you return to Yorick.

One system command which you cannot use is cd (or pushd or popd) to change directories. The working directory of the shell you start under Yorick will change, but the change has no effect on Yorick's own working directory. Instead, use Yorick's own cd function:

> cd, "new/working/directory"
>

Also, if you write a Yorick program which manipulates temporary files, you should not use the UNIX commands mv or rm to rename or remove the files; any time you use the system command you are restricting your program to a particular class of operating systems. Instead, use Yorick's rename and remove functions, which will work under any operating system.

Error Messages

When Yorick cannot decipher the meaning of a statement, you have made a syntax error. Syntax errors are mostly simple typos, but a mechanical language like Yorick can be exasperatingly picky:

> th= span(0,2*pi,200)
> for (i=1 ; i<=5 ; ++i) { r=cos(i*th); plg,r*sin(th),r*cos(th) }
SYNTAX: parse error near }
>

The only mistake here is that Yorick wants a semicolon (or newline) before the close curly brace completing a compound statement. If you think "fixing" this type of behavior would be simple, I suggest you study parsers. Every programming language has its quirks.

When Yorick detects a syntax error, the error message always begins with SYNTAX:. The entire block containing the error will be discarded; no statements will be executed which might lead to the execution of the statement containing the error. If the error is in a function body, the function will not be defined. However, if the sytax error occurs reading an include file, Yorick continues to parse the file looking for additional syntax errors. After about a dozen syntax errors in a single file, Yorick gives up and waits for keyboard input. Therefore, you may be able to repair several syntax errors before you re-include the file.

All other errors are runtime errors.

Runtime errors

Runtime errors in Yorick are often simple typos, like syntax errors:

> theta= span(0,2*pi,200)
> wave= sin(thta)*exp(-0.5*theta)
ERROR (*main*) expecting numeric argument
WARNING source code unavailable (try dbdis function)
now at pc= 1 (of 23), failed at pc= 5
 To enter debug mode, type <RETURN> now (then dbexit to get out)
> 

Many errors of this sort would be detected as syntax errors if you had to declare Yorick variables. Yorick's free-and-easy attitude toward declaration of variables is particularly annoying when the offending statement is in a conditional branch which is very rarely executed. When a bug like that ambushes you, be philosophical: Minutely declared languages will just ambush you in more subtle ways.

Other runtime errors are more interesting; often such a bug will teach you about the algorithm or even about the physical problem:

> #include "damped.i"
> theta= span(0, 6*pi, 300)
> amplitude= damped_wave(theta, 0.25)
ERROR (damped_wave) math library exception handler called
  LINE: 19  FILE: /home/icf/munro/damped.i
 To enter debug mode, type <RETURN> now (then dbexit to get out)
>

What is an oscillator with a Q of less than one half? Maybe you don't care about the so-called overdamped case -- you really wanted Q to be 2.5, not 0.25. On the other hand, maybe you need to modify the damped_wave function to handle the overdamped case.

How to respond to a runtime error

When Yorick stops with a runtime error, you have a choice: You can either type the next statment you want to execute, or you can type a carriage return (that is, a blank line) to enter debug mode. The two possibilities would look like this:

ERROR (damped_wave) math library exception handler called
  LINE: 19  FILE: /home/icf/munro/damped.i
 To enter debug mode, type <RETURN> now (then dbexit to get out)
> amplitude= damped_wave(theta, 2.5)
>
ERROR (damped_wave) math library exception handler called
  LINE: 19  FILE: /home/icf/munro/damped.i
 To enter debug mode, type <RETURN> now (then dbexit to get out)
>
dbug>

In the second case, you have entered debug mode, and the dbug> prompt appears. In debug mode, Yorick leaves the function which was executing and its entire calling chain intact. You can type any Yorick statement; usually you will print some values or plot some arrays to try to determine what went wrong. When you reference or modify a variable which is local to the function, you will "see" its local value:

dbug> nu; 1-nu*nu
2
-3
dbug>

As soon as possible, you should escape from debug mode using the dbexit function:

dbug> dbexit
>

You may also be able to repair the function's local variables and resume execution. To modify the value of a variable, simply redefine it with an ordinary Yorick statement. The dbcont function continues execution, beginning by re-executing the statement which failed to complete. Use the help function (see section The help function) to learn about the other debugging functions; the help for the dbexit function describes them all.


Go to the first, previous, next, last section, table of contents.