This purpose of this section is to give the reader a feel for the S-Lang language, its syntax, and its capabilities. The information and examples presented in this section should be sufficient to provide the reader with the necessary background to understand the rest of the document.
S-Lang is different from many other interpreted languages in the sense that all variables and functions must be declared before they can be used.
Variables are declared using the variable
keyword, e.g.,
variable x, y, z;
declares three variables, x
, y
, and z
. Note the
semicolon at the end of the statement. All S-Lang statements must
end in a semicolon.
Unlike compiled languages such as C, it is not necessary to specify the data type of a S-Lang variable. The data type of a S-Lang variable is determined upon assignment. For example, after execution of the statements
x = 3;
y = sin (5.6);
z = "I think, therefore I am.";
x
will be an integer, y
will be a
double, and z
will be a string. In fact, it is even possible
to re-assign x
to a string:
x = "x was an integer, but now is a string";
Finally, one can combine variable declarations and assignments in
the same statement:
variable x = 3, y = sin(5.6), z = "I think, therefore I am.";
Most functions are declared using the define
keyword. A
simple example is
define compute_average (x, y)
{
variable s = x + y;
return s / 2.0;
}
which defines a function that simply computes the average of two
numbers and returns the result. This example shows that a function
consists of three parts: the function name, a parameter list, and
the function body.
The parameter list consists of a comma separated list of variable names. It is not necessary to declare variables within a parameter list; they are implicitly declared. However, all other local variables used in the function must be declared. If the function takes no parameters, then the parameter list must still be present, but empty:
define go_left_5 ()
{
go_left (5);
}
The last example is a function that takes no arguments and returns
no value. Some languages such as PASCAL distinguish such objects
from functions that return values by calling these objects
procedures. However, S-Lang, like C, does not make such a
distinction.
The language permits recursive functions, i.e., functions that call themselves. The way to do this in S-Lang is to first declare the function using the form:
define function-name ();
It is not necessary to declare a list of parameters when declaring a
function in this way.
Perhaps the most famous example of a recursive function is the factorial function. Here is how to implement it using S-Lang:
define factorial (); % declare it for recursion
define factorial (n)
{
if (n < 2) return 1;
return n * factorial (n - 1);
}
This example also shows how to mix comments with code. S-Lang uses
the `%
' character to start a comment and all characters from
the comment character to the end of the line are ignored.
S-Lang 2.1 introduced support for function qualifiers as a mechanism for passing additional information to a function. For example, consider a plotting application with a function
define plot (x, y)
{
variable linestyle = qualifier ("linestyle", "solid");
variable color = qualifier ("color", "black");
sys_set_color (color);
sys_set_linestyle (linestyle);
sys_plot (x,y);
}
Here the functions sys_set_linestyle
, sys_set_color
,
and sys_plot
are hypothetical low-level functions that
perform the actual work. This function may be called simply as
x = [0:10:0.1];
plot (x, sin(x));
to produce a solid black line connecting the points. Through the
use of qualifiers, the color or linestyle may be specified, e.g,,
plot (x, sin(x); linestyle="dashed");
would produce a ``dashed'' black curve, whereas
plot (x, sin(x); linestyle="dotted", color="blue");
would produce a blue ``dotted'' one.
Perhaps the most appealing feature of any interpreted language is that it frees the user from the responsibility of memory management. This is particularly evident when contrasting how S-Lang handles string variables with a lower level language such as C. Consider a function that concatenates three strings. An example in S-Lang is:
define concat_3_strings (a, b, c)
{
return strcat (a, b, c);
}
This function uses the built-in
strcat
function for concatenating two or more strings. In C, the
simplest such function would look like:
char *concat_3_strings (char *a, char *b, char *c)
{
unsigned int len;
char *result;
len = strlen (a) + strlen (b) + strlen (c);
if (NULL == (result = (char *) malloc (len + 1)))
exit (1);
strcpy (result, a);
strcat (result, b);
strcat (result, c);
return result;
}
Even this C example is misleading since none of the issues of memory
management of the strings has been dealt with. The S-Lang language
hides all these issues from the user.
Binary operators have been defined to work with the string data
type. In particular the +
operator may be used to perform
string concatenation. That is, one can use the
+
operator as an alternative to strcat
:
define concat_3_strings (a, b, c)
{
return a + b + c;
}
See the section on
Strings for more information about string variables.
The unary prefix operator, &
, may be used to create a
reference to an object, which is similar to a pointer
in other languages. References are commonly used as a mechanism to
pass a function as an argument to another function as the following
example illustrates:
define compute_functional_sum (funct)
{
variable i, s;
s = 0;
for (i = 0; i < 10; i++)
{
s += (@funct)(i);
}
return s;
}
variable sin_sum = compute_functional_sum (&sin);
variable cos_sum = compute_functional_sum (&cos);
Here, the function compute_functional_sum
applies the
function specified by the parameter funct
to the first
10 integers and returns the sum. The two statements
following the function definition show how the sin
and
cos
functions may be used.
Note the @
operator in the definition of
compute_functional_sum
. It is known as the dereference
operator and is the inverse of the reference operator.
Another use of the reference operator is in the context of the
fgets
function. For example,
define read_nth_line (file, n)
{
variable fp, line;
fp = fopen (file, "r");
while (n > 0)
{
if (-1 == fgets (&line, fp))
return NULL;
n--;
}
return line;
}
uses the fgets
function to read the nth line of a file.
In particular, a reference to the local variable line
is
passed to fgets
, and upon return line
will be set to
the character string read by fgets
.
Finally, references may be used as an alternative to multiple
return values by passing information back via the parameter list.
The example involving fgets
presented above provided an
illustration of this. Another example is
define set_xyz (x, y, z)
{
@x = 1;
@y = 2;
@z = 3;
}
variable X, Y, Z;
set_xyz (&X, &Y, &Z);
which, after execution, results in X
set to 1, Y
set to 2, and Z
set to 3. A C programmer will
note the similarity of set_xyz
to the following C
implementation:
void set_xyz (int *x, int *y, int *z)
{
*x = 1;
*y = 2;
*z = 3;
}
The S-Lang language supports multi-dimensional arrays of all datatypes. For example, one can define arrays of references to functions as well as arrays of arrays. Here are a few examples of creating arrays:
variable A = Int_Type [10];
variable B = Int_Type [10, 3];
variable C = [1, 3, 5, 7, 9];
The first example creates an array of 10 integers and assigns
it to the variable A
. The second example creates a 2-d array
of 30 integers arranged in 10 rows and 3 columns
and assigns the result to B
. In the last example, an array
of 5 integers is assigned to the variable C
. However,
in this case the elements of the array are initialized to the
values specified. This is known as an inline-array.
S-Lang also supports something called a range-array. An example of such an array is
variable C = [1:9:2];
This will produce an array of 5 integers running from 1 through 9
in increments of 2. Similarly [0:1:#1000]
represents a 1000
element floating point array of numbers running from 0 to 1
(inclusive).
Arrays are passed by reference to functions and never by value. This permits one to write functions that can initialize arrays. For example,
define init_array (a)
{
variable i, imax;
imax = length (a);
for (i = 0; i < imax; i++)
{
a[i] = 7;
}
}
variable A = Int_Type [10];
init_array (A);
creates an array of 10 integers and initializes all its
elements to 7.
There are more concise ways of accomplishing the result of the previous example. These include:
A = [7, 7, 7, 7, 7, 7, 7, 7, 7, 7];
A = Int_Type [10]; A[[0:9]] = 7;
A = Int_Type [10]; A[*] = 7;
The second and third methods use an array of indices to index the array
A
. In the second, the range of indices has been explicitly
specified, whereas the third example uses a wildcard form. See
chapter
Arrays for more information about array indexing.
Although the examples have pertained to integer arrays, the fact is that S-Lang arrays can be of any type, e.g.,
A = Double_Type [10];
B = Complex_Type [10];
C = String_Type [10];
D = Ref_Type [10];
create 10 element arrays of double, complex, string, and
reference types, respectively. The last example may be used to
create an array of functions, e.g.,
D[0] = &sin;
D[1] = &cos;
S-Lang arrays also can be of Any_Type
. An array of such a
type is capable of holding any object, e.g.,
A = Any_Type [3];
A[0] = 1; A[1] = "string"; A[2] = (1 + 2i);
Dereferencing an Any_Type
object returns the actual object. That
is, @A[1]
produces "string"
.
The language also defines unary, binary, and mathematical
operations on arrays. For example, if A
and B
are
integer arrays, then A + B
is an array whose elements are
the sum of the elements of A
and B
. A trivial example
that illustrates the power of this capability is
variable X, Y;
X = [0:2*PI:0.01];
Y = 20 * sin (X);
which is equivalent to the highly simplified C code:
double *X, *Y;
unsigned int i, n;
n = (2 * PI) / 0.01 + 1;
X = (double *) malloc (n * sizeof (double));
Y = (double *) malloc (n * sizeof (double));
for (i = 0; i < n; i++)
{
X[i] = i * 0.01;
Y[i] = 20 * sin (X[i]);
}
A S-Lang list is like an array except that it may contain a heterogeneous collection of data, e.g.,
my_list = { 3, 2.9, "foo", &sin };
is a list of four objects, each with a different type. Like an
array, the elements of a list may be accessed via an index, e.g.,
x=my_list[2]
will result in the assignment of "foo"
to x
. The most important difference between an array and a
list is that an array's size is fixed whereas a list may grow or
shrink. Algorithms that require such a data structure may execute
many times faster when a list is used instead of an array.
A structure is similar to an array in the sense that it is a
container object. However, the elements of an array must all be of
the same type (or of Any_Type
), whereas a structure is
heterogeneous. As an example, consider
variable person = struct
{
first_name, last_name, age
};
variable bill = @person;
bill.first_name = "Bill";
bill.last_name = "Clinton";
bill.age = 51;
In this example a structure consisting of the three fields has been
created and assigned to the variable person
. Then an
instance of this structure has been created using the
dereference operator and assigned to bill
. Finally, the
individual fields of bill
were initialized. This is an
example of an anonymous structure.
Note: S-Lang versions 2.1 and higher permit assignment statements within the structure definition, e.g.,
variable bill = struct
{
first_name = "Bill",
last_name = "Clinton",
age = 51
};
A named structure is really a new data type and may be created
using the typedef
keyword:
typedef struct
{
first_name, last_name, age
}
Person_Type;
variable bill = @Person_Type;
bill.first_name = "Bill";
bill.last_name = "Clinton";
bill.age = 51;
One advantage of creating a new type is that array elements of such
types are automatically initialized to instances of the type. For
example,
People = Person_Type [100];
People[0].first_name = "Bill";
People[1].first_name = "Hillary";
may be used to create an array of 100 such objects and initialize
the first_name
fields of the first two elements. In
contrast, the form using an anonymous would require a separate step
to instantiate the array elements:
People = Struct_Type [100];
People[0] = @person;
People[0].first_name = "Bill";
People[1] = @person;
People[1].first_name = "Hillary";
Another big advantage of a user-defined type is that the binary and unary operators may be overloaded onto such types. This is explained in more detail below.
The creation and initialization of a structure may be facilitated by a function such as
define create_person (first, last, age)
{
variable person = @Person_Type;
person.first_name = first;
person.last_name = last;
person.age = age;
return person;
}
variable Bill = create_person ("Bill", "Clinton", 51);
Other common uses of structures is the creation of linked lists, binary trees, etc. For more information about these and other features of structures, see the section on Linked Lists.
The language supports namespaces that may be used to control the scope and visibility of variables and functions. In addition to the global or public namespace, each S-Lang source file or compilation unit has a private or anonymous namespace associated with it. The private namespace may be used to define symbols that are local to the compilation unit and inaccessible from the outside. The language also allows the creation of named (non-anonymous or static) namespaces that permit access via the namespace operator. See the chapter on Namespaces for more information.