Rlab is, in a sense, an object oriented language. The term ``object oriented'' is used with some trepidation, since the term has itself been overloaded to the point of becoming unintelligible. Although Rlab does not support all of the concepts of the more classical object-oriented languages like Smalltalk it does offer some features of an object-oriented language. Operator overloading is one concept Rlab supports. For example, the behavior of the mathematical operators is dictated by the arguments, or objects.
There are several predefined classes, they are:
This class is the most widely used class in
Rlab. The numeric class, abbreviated for use as num
consists of two dimensional arrays or matrices. Subsets of this
class include real and complex matrices, in both dense and
sparse storage formats.
The string class consists of two dimensional arrays of variable length strings. Much of the syntax used for working with string arrays is directly derived from the numeric array class.
The list class provides a convenient, and user definable method for grouping related sets of data and functions. The list class is implemented as an N-dimensional associative array.
The function class consists of both builtin and
user-defined functions. If your computing platform supports
runtime dynamic linking, then you can add builtin function via
the dlopen
interface.
We will stick with conventional object oriented terminology, and call an instantiation of a class an object. All objects have members. The members themselves are other objects. The syntax for accessing an object's members is the same syntax used for list members (see Section lists ). To see what an objects members are named use the members function. Members returns a string matrix containing the names of an objects members, like so:
> a = rand(3,4);
> members(a)
nr nc n class type storage
The objects members can be referenced with either the formal string syntax like:
> a.["nr"]
3
Or, the shorthand notation can be used:
> a.nr
3
The formal notation is useful when you need variable evaluation:
> for (i in members(a)) { printf("%10s\t%s\n", i, a.[i]); }
nr 3
nc 4
n 12
class num
type real
storage dense
An object's predefined members can be considered "read-only". That
is, the user cannot change these member's values without changing
the object itself. For example: the nr
member of a matrix
denotes the number of rows in the matrix. This member cannot be
directly modified by the user. The only way to change the number of
rows member is to actually add, or delete rows from the object.
Additional members can be added to any object. Additional object members are arbitrary, and can be modified after creation. For example:
> a = rand(3,4);
> a.rid = [1;2;3];
> a.cid = 1:3;
> a
0.91 0.265 0.0918 0.915
0.112 0.7 0.902 0.441
0.299 0.95 0.96 0.0735
> a.rid
1
2
3
> a.cid
1 2 3
> a.rid = ["row1", "row2", "row3"]
0.91 0.265 0.0918 0.915
0.112 0.7 0.902 0.441
0.299 0.95 0.96 0.0735
> a.rid
row1 row2 row3
show
and whos
are useful functions for displaying
object information. show
displays an objects members, and
their values, iff the values are scalar. Otherwise show
displays the member's own attributes. For example:
> a = rand(3,4);
> a.row_id = (1:3)';
> a.col_id = 1:4;
> show(a);
nr : 3
nc : 4
n : 12
class : num
type : real
storage : dense
col_id : num, real, dense, 1x4
row_id : num, real, dense, 3x1
whos
will display the object information for each member
(with the exception of members that are functions) in addition to
more detailed information about the size in bytes of each object.
> whos(a)
Name Class Type Size NBytes
nr num real 1 1 8
nc num real 1 1 8
n num real 1 1 8
class string string 1 1 7
type string string 1 1 8
storage string string 1 1 9
col_id num real 1 4 32
row_id num real 3 1 24
Total MBytes = 0.000104
Both show
and whos
were originally designed to operate
on the global workspace. The fact that they work equally well on
individual objects provides a clue to the design of Rlab. The global
symbol table, or global workspace can be considered an object of the
list class. There is a special symbol for referring to the global
workspace, $$
. Accessing members of the global
workspace can be performed with the same notation as previously
described for an object's members. For example:
> $$.a
1 0.333 0.665 0.167
0.975 0.0369 0.0847 0.655
0.647 0.162 0.204 0.129
> $$.cos($$.a)
0.54 0.945 0.787 0.986
0.562 0.999 0.996 0.793
0.798 0.987 0.979 0.992
In this example, the object a
is referenced through the
global workspace object $$
rather than using
the more conventional shorthand a
. Then, the cosine function
is invoked, again through the global workspace symbol. There are
special provisions in place to ensure that users don't delete
$$
. The benefits of these capabilities may not
be apparent until there is a need to construct and work with
variables in an automated fashion.
The simplest numeric objects are scalars, or matrices with row and column dimensions of 1. Real values can be specified in integer or floating point format. For example:
> 3
3
> 3.14
3.14
> 3.14e2
314
> 3.14e-2
0.0314
> 3.14E-02
0.0314
Are all valid ways to express real numeric values. Complex values
are specified with the help of a complex constant. The complex
constant is any real value immediately followed by i
or j
. Complex numbers, with real and imaginary parts can be
constructed with the arithmetic operators, for example:
> z = 3.2 + 2j
3.2 + 2i
The numeric class supports two types of data, and two types of storage format.
Each object has, as a minimum the following members:
nr
The matrix number of rows.
nc
The matrix number of columns.
n
The matrix number of elements.
class
A string, with value "num"
, for
numeric.
type
A string, with value "real"
or
"complex"
.
storage
A string, with value "dense"
or
"sparse"
.
This section will cover the basic numeric operations. Starting with the operations necessary for creating and manipulating numeric matrices. The aritmetic operations are covered in Section Arithmetic Operations
The syntax for operating with numeric arrays/matrices is fairly
simple. Square braces, []
are used for both creating
matrices, assignment to matrix elements, and partitioning. The
operation of creating a matrix consists of either appending elements
to form rows, or stacking elements to form columns. Both operations
must be performed within brackets. The append operation is performed
with commas:
> a = [ 1 , 2 ];
> a = [ a , a ]
1 2 1 2
As you can see, either scalar elements, or matrices can be used with the append operator, as long as the row dimensions are the same. The stack operation is similar to the append operation, except semicolons are used as delimiters, and the column dimensions must match.
> b = [ 1 ; 2 ];
> b = [ b ; b]
1
2
1
2
Any combination of append and stack operations can be performed together as long as the dimensions of the operands match.
> a = [1, 2];
> b = [3; 4];
> c = [ [a; a], [b, b] ]
1 2 3 3
1 2 4 4
Assignment to matrix elements is also simple. Square brackets,
[]
are used to identify the matrix elements to be
re-assigned. Row and column identifiers are separated with a
semicolon. Multiple row or column specifiers can separated with
commas. To assign to a single element:
> a = [1, 2, 3; 4, 5, 6; 7, 8, 9];
> a[1;1] = 10
10 2 3
4 5 6
7 8 9
To assign to multiple elements, specifically multiple rows:
> a[1,3;2] = [11;12]
10 11 3
4 5 6
7 12 9
The dimensions of both the right hand side (RHS) and left hand side (LHS) must match. Assignment can be made to blocks of elements, in any specified order:
> a[3,1;3,1] = [30,30;30,30]
30 11 30
4 5 6
30 12 30
To eliminate the tedium of specifying all the rows or all the columns, simply leave out the appropriate row or column specifier:
> a[;2] = [100;200;300]
30 100 30
4 200 6
30 300 30
Entire rows and columns can be eliminated via the assignment of the null matrix to those row and columns to be removed. The allowed syntax for this operation is:
VAR [ ROW-ID ; ] = []
VAR [ ; COL-ID ] = []
A simple example:
> a = magic(5)
17 24 1 8 15
23 5 7 14 16
4 6 13 20 22
10 12 19 21 3
11 18 25 2 9
> a[3,2;] = []
17 24 1 8 15
10 12 19 21 3
11 18 25 2 9
> a[;2,4] = []
17 1 15
10 19 3
11 25 9
Matrix partitioning, the operation of extracting a sub-matrix from
an existing matrix, uses the same syntax, and concepts of matrix
element assignment. To partition a 2-by-2 sub-matrix from the
original a
:
> a = [1, 2, 3; 4, 5, 6; 7, 8, 9];
> a[2,3;2,3]
5 6
8 9
For clarification: each operand (either A
or B
) is a
matrix, with row dimension M, and column dimension N.
A + B
Does element-by-element addition of two
matrices. The row and column dimensions of both A
and
B
must be the same. An exception to the aforementioned
rule occurs when either A
or B
is a 1-by-1 matrix;
in this case a scalar-matrix addition operation is performed.
A - B
Does element-by-element subtraction of two
matrices. The row and column dimensions of both A
and
B
must be the same. An exception to the aforementioned
rule occurs when either A
or B
is a 1-by-1 matrix;
in this case a scalar-matrix addition operation is performed.
A * B
Performs matrix multiplication on the two
operands. The column dimension of A
must match the row
dimension of B
. An exception to the aforementioned rule
occurs when either A
or B
is a 1-by-1 matrix; in
this case a scalar-matrix multiplication is performed.
A .* B
Performs element-by-element matrix multiplication on the two operands. Both row and column dimensions must agree, unless:
A
or B
is a 1x1. In this case the operation
is performed element-by-element over the entire matrix. The
result is a MxN matrix.
A
or B
is a 1xN. and the other is MxN. In
this instance the operation is performed element-by-element
fashion for each row in the matrix. The result is a MxN matrix.
A
or B
is a Nx1. and the other is NxM. In
this instance the operation is performed element-by-element
fashion for each column in the matrix. The result is a NxM
matrix.
A / B
Performs matrix right-division on its
operands. The matrix right-division B/A
can be thought of
as B*inv (A)
. The column dimensions of A
and
B
must be the same. Internally right division is the same
as left-division with the arguments transposed.
B / A = ( A' \ B')'
The exception to the aforementioned dimension rule occurs when
A
is a 1-by-1 matrix; in this case a matrix-scalar divide
occurs.
A ./ B
Performs element-by-element right-division
on its operands. The dimensions of A
and B
must
agree, unless:
A
or B
is a 1x1. In this case the operation
is performed element-by-element over the entire matrix. The
result is a MxN matrix.
A
or B
is a 1xN. and the other is MxN. In
this instance the operation is performed element-by-element
fashion for each row in the matrix. The result is a MxN matrix.
A
or B
is a Nx1. and the other is NxM. In
this instance the operation is performed element-by-element
fashion for each column in the matrix. The result is a NxM
matrix.
A \ B
Performs matrix left-division. Given
operands A\B
matrix left division is the solution to
the set of equations Ax = B
. If B
has several
columns, then each column of x
is a solution to
A*x[;i] = B[;i]
. The row dimensions of A
and
B
must agree.
A .\ B
Performs element-by-element
left-division. Element-by-element left-division is provided for
symmetry, and is equivalent to B .\ A
. The row and
column dimensions of A
and B
must agree, unless:
A
or B
is a 1x1. In this case the operation
is performed element-by-element over the entire matrix. The
result is a MxN matrix.
A
or B
is a 1xN. and the other is MxN. In
this instance the operation is performed element-by-element
fashion for each row in the matrix. The result is a MxN matrix.
A
or B
is a Nx1. and the other is NxM. In
this instance the operation is performed element-by-element
fashion for each column in the matrix. The result is a NxM
matrix.
Although there is no separate vector class, the concept of row and column vectors is often used. Row and column vectors are matrices with a column or row dimension equal to one, respectively. Rlab offers convenient notation for creating, assignment to, and partitioning matrices as if they were vectors.
There is a special notation for creating ordered row vectors.
start-value : end-value : increment-value
The start, end and increment values can be any floating point or integer value. If the start-value is less than the end-value, then a null-vector will be returned, unless the increment-value is negative. This vector notation is most often used within for-loops.
> n = 4;
> 1:n
1 2 3 4
> n:1:-1
4 3 2 1
> 1:n/2:0.5
1 1.5 2
Unexpected results can occur when a non-integer increment is used. Since not all real numbers can be expressed precisely in floating point format, incrementing the start-value by the increment-value may not produce the expected result. An increment value of 0.1 provides a nice example:
> 1:2:.1
matrix columns 1 thru 6
1 1.1 1.2 1.3 1.4 1.5
matrix columns 7 thru 10
1.6 1.7 1.8 1.9
Most would expect the final value to be 2. But, since 0.1 cannot be expressed exactly in floating point format, the final value of 2 is not reached. The reason is more obvious if we reset the print format:
> format(18);
> 1:2:.1
matrix columns 1 thru 3
1 1.10000000000000009 1.19999999999999996
matrix columns 4 thru 6
1.30000000000000004 1.39999999999999991 1.5
matrix columns 7 thru 9
1.60000000000000009 1.69999999999999996 1.80000000000000004
matrix columns 10 thru 10
1.90000000000000013
When it is important to have the precise start and end values, the
user-function linspace
should be used.
Fairly frequently it is desirable to force a matrix into a column vector. This is fairly natural since matrices are stored in column-major order, and it makes operating on the data notationally simpler. The syntax for this operation is:
matrix [ : ]
For example:
> a = [1,2,3,4];
> a = a[:]
1
2
3
4
Sparse matrices are matrices in which the zero elements are not explicitly stored. Quite a few applications, such as finite element modeling, lead to matrices which are sparsely populated with non-zero elements. A sparse storage scheme offers reduced memory usage, and more efficient matrix operations (in most cases) for operations on matrices with mostly zero elements. There are many different sparse storage schemes, each offers particular advantages. Rlab uses the compressed row-wise (CRW) sparse storage format. The CRW format is very general, and offers good performance for a wide variety of problems.
Sparse matrices, and operations are not common for the majority of
users. Therefore, some extra work is required for users who wish to
use this storage scheme. The functions sparse
and
spconvert
are useful for converting from dense/full storage
to sparse storage, and vice-versa. Although the syntax for sparse
storage matrices is the same as that used for dense matrices, sparse
matrices are visibly different when printed to the display:
> a = speye(3,3)
(1, 1) 1
(2, 2) 1
(3, 3) 1
> full(a)
1 0 0
0 1 0
0 0 1
Since only the non-zero elements are stored, only the non-zero elements, along with their row and column indices are printed.
All matrix partitioning, assignment and arithmetic operations
perform the same function for sparse matrices as for dense matrices
(eventually). The only difference is the storage format of the
result. In some instances an operation on a sparse matrix will
produce a dense (or full) matrix because there is no benefit to
retaining sparse storage. For instance, using the cos
function on a sparse matrix will return a full matrix, since the
cosine of zero is one, there is no point in retaining the sparse
storage format for the result. On the other hand Rlab will never
change the storage format of a matrix, once it has been
created. Even if you deliberately add zeros to a sparse matrix, or
increase the number of non-zeros to the point where the matrix is
full, the storage format will remain sparse.
While sparse storage formats facilitate the solution of problems that dense storage cannot manage, there are some things that sparse storage cannot do efficiently. Sparse storage is inefficient for matrix manipulations. Assigning to the elements of a sparse matrix can be very inefficient, especially if you are replacing zeros with non-zero values. Likewise matrix stacking and concatenation are not very efficient.
Strings are arbitrary length concatenations of printable characters.
Each object has, as a minimum the following members:
nr
The matrix number of rows.
nc
The matrix number of columns.
n
The matrix number of elements.
class
A string, with value "string"
, for
numeric.
type
A string, with value "string"
.
storage
A string, with value "dense"
.
The syntax for creating a string is similar to the C-language syntax:
" arbitrary_printable_characters "
So to create a string, and assign it to a variable you might do:
> str = "this is a sample string"
this is a sample string
String matrix operations are performed exactly the same way as numeric matrix operations. String matrix creation, element assignment, and partitioning are all performed as described for numeric matrices. For example:
> strm = [ "this", "is a"; "sample", "string matrix"]
this is a
sample string matrix
> for (i in [1,3,2,4]) { strm[i] }
this
is a
sample
string matrix
There is no provision for individual character operations on
strings, unless the string consists of a single character. However,
the function strsplt
will break a string into an array
(row-matrix) of single character strings.
> strsplt (str)
t h i s i s a s a m p l e s t r i n g
strsplt
can also split strings into sub-strings of a
specified length, using the second (optional) argument.:
> strsplt (str, 4)
this is a sa mple str
> length(strsplt (str, 4))
4 4 4 4 4
Furthermore, strsplt
can split strings using a field
separator defined in the second (optional) argument:
> strsplt (str, "i")
th s s a sample str ng
Strings can be concatenated with the +
operator:
> strm[1;1] + " " + strm[1;2] + " " + strm[2;1] + " " + strm[2;2]
this is a sample string matrix
The relational operators work for strings, comparing them using the
characters ASCII decimal representation. Thus
"A"
, (ASCII 65) is less than
"a"
(ASCII 97). String comparisons are useful for
testing the properties of objects. For instance, the function
class
returns a string identifying the class an object
belongs to.
> class(l)
list
> class(l) == "list"
1
A list is a heterogeneous associative array. Simply, a list is an array whose elements can be from different classes. Thus a list can contain numeric, string, function, and other list objects. Lists are also a convenient vehicle for functions that must return multiple data objects. Additionally, lists offer programmer the ability to create arbitrary data structures to suit particular programming tasks.
Lists have no predefined elements, the quantity and class of a
list's elements is entirely up to the user. A list's elements are
displayed when an expression evaluates to a list. Entering the name
of a list variable, without a trailing semi-colon, will print out
the list's element names. The standard user-functions: show
,
who
, and whos
will also display information about a
list's elements. The following example will create a list, then
display information about the list's elements using the
aforementioned methods.
> rfile magic
> l = << m2 = magic(2); m3 = magic(3); m6 = magic(6) >>
m2 m3 m6
> who(l)
m2 m3 m6
> l
m2 m3 m6
> who(l)
m2 m3 m6
> show(l);
m2 :num real
m3 :num real
m6 :num real
> whos(l);
Name Class Type Size NBytes
m2 num real 2 2 32
m3 num real 3 3 72
m6 num real 6 6 288
Total MBytes = 0.000392
To create a list-object use the <<
and >>
operators. The list will be created, and the objects inside the
<< >>
will be installed in the new list. If the
objects are not renamed during the list-creation, they will be given
numerical index values. An expression that evaluates to a list will
print out the names of that list's elements. For example:
> a = rand(3,4); b = sqrt (a); c = 2*a + b;
> ll = << a ; b ; c >>
1 2 3
> ll2 = << A = a; b = b ; x = c >>
A b x
> ll2.A == ll.[1]
1 1 1 1
1 1 1 1
1 1 1 1
Lists are not indexed with numeric values. Lists are indexed with string values (in a fashion similar to AWK's associative arrays.}. There are two methods for referencing the elements of a list. The first, a shorthand notation looks like:
list_name . element_name
In this case, the list_name and element_name must follow the same rules as ordinary variable names. The second method for indexing a list is:
list_name . [ numeric_or_string_expression ]
The second method allows string and numeric variables to be evaluated before doing the conversion to string type.
The dimensionality of a list is also arbitrary. To increase the dimension of a list make a member of the parent list a list. For example:
> person = << type="Human"; name=<<first="John"; last="Doe">>; age=37 >>
age name type
> person.name
first last
> person.name.first
John
> person.name.last
Doe
The person
list contains the elements type
,
name
, and age
. However, the name
element is
another list that contains the elements first
and
last
.
Functions, both builtin and user written are stored in ordinary
variables, and in almost all instances are treated as such. An
expression that evaluates to a function prints the string:
<user-function>
if it is a user-written function, and
the string: <bltin-function>
if it is a builtin
function.
Each object has, as a minimum the following members:
class
A string, with value "function"
.
type
A string, with value "user"
or
"builtin"
.
The function class has an optional member which exists only when the
function is of type user
. The additional member is named
file
, and its value is the full pathname of the file that
contains the source code for the user function.
Functions, both user and builtin are treated in great detail in subsequent sections of this manual.