Objects and their properties
RPL uses objects to represent data. Previously, simple programs were introduced, which this section will now categorize as composite objects. The numbers used within those programs and on the stack are also objects.
This section will describe the different types of objects in newRPL, their behavior and properties:
Atomic objects:
- Real numbers
- Angles
- Complex numbers
- Identifiers
- Symbolic expressions
- Strings
Composite objects / containers:
- Vectors and Matrices
- Lists
- Programs
- Directories
Real Numbers
Numbers are simple objects that represent a real numeric quantity. Numbers in newRPL are stored with a floating point precision selectable by the user, up to 2000 digits. The default system precision is 36 digits, which provides a good balance of speed, memory usage and smaller rounding errors.
Real numbers can be expressed in several ways. The following are all valid real numbers:
- 1
- 1.
- 1.0
- 1.02
- 1.02.
- 1.02e10
- 1.02.e10
- 1.02e-10
- 1.02.e-10
Notice that half the entries in the table have a trailing dot. A real number with a trailing dot is a number that is known to be an approximated number, or that was calculated through approximated means. On the contrary, a number specified without a trailing dot is a number that is known (or assumed) to be exact.
1 3 /
Will produce 0.33333...333. (with a trailing dot) because the number cannot be represented exactly, and is therefore approximated.
1 2 /
Will produce 0.5 (no trailing dot) because the number was the result of an exact computation of exact numbers.
1. 2 /
Will produce 0.5. (with trailing dot) because one of the numbers was approximated, so the result is approximated even though the operation per se was exact with the given digits.
For numeric computations it doesn't seem relevant to know if a number is exact or approximated and has very limited use. However, the behavior of these two classes of numbers differs when used in symbolic expressions. The symbolic engine will attempt to keep exact numbers exact by deferring operations, but considers acceptable to perform approximations on numbers that are already approximated. More details about this can be found on the symbolic expressions section, for now suffices to show that '1/2' will remain a fraction if both numbers are exact, but will produce 0.5. as a result if any of the numbers are already approximated ('1./2', or '1/2.' or '1./2.').
Numbers in RPL can be exact or approximate. This is a unique feature of newRPL that allows fine-grain control of the results within symbolic expressions.
Real numbers in scientific notation can have an exponent of +/-30000 (from +/-1e-30000 to +/-1e+30000), regardless of the selected system precision. When working with the maximum precision of the system, the highest integer number that can be represented exactly would be 1e2000. Larger numbers can only be approximated to the first 2000 digits (or the selected system precision).
Real numbers are stored in the precision they were created/computed, and are not affected if the precision is changed afterwards. For example:
« 1 3 / 3 * »
« 1 3 / 30 SETPREC 3 * »
The first example computes 1/3 = 0.3333... at the default precision of 32 digits, Then when multiplied back by 3, the result is 0.999999... with 32 digits. The second example, computes 1/3 at 32 digits precision, then changes the system precision using the command SETPREC to 30 digits. This does not affect the number 0.333333... that was calculated previously and has 32 digits. When multiplied by 3, the result is 1, instead of .9999... because the number with 32 digits multiplied by 3 produced 0.999999.... with 32 good digits, and the result was rounded to the new selected precision (30 digits) afterwards. If the entire calculation had happened at 30 digits, the result would be 0.9999... with 30 digits of precision.
The programmer can selectively use higher precision to perform only certain critical computations, for example solve an ill-conditioned system of equations, and then go back to the default precision for the rest of the problem.
The amount of storage required by a number is variable and depends on the number of significant digits of the number, not on the system precision. The system will aggresively trim unnecessary digits from numbers (trailing zeroes) to reduce storage use.
As a guideline, real numbers require the following amounts of storage:
- Integers between -130000 and +130000 use 4 bytes.
- Integers outside that range that and within 2^63 and -2^63 will use 12 bytes.
- Real numbers others than above use 8+4*(N/8), where N=smaller multiple of 8 that is greater than the number of significant digits stored in the number. Digits are packed in groups of 8, so a number with 2 digits will need one pack of 8, a number with 12 digits will need 2 packs, etc.
At the default precision of 32 digits, a real number can take maximum 24 bytes.
Integer numbers can be expressed in the following bases, when preceded by the symbol # and followed by a letter indicating the base:
- Base 2: #1101b
- Base 8: #15o
- Base 10: #13d or simply 13
- Base 16: #dh
The default base is 10, unless the number is explicitly written in a different base. Numbers will keep the base they were written into, unless explicitly converted to a different base. Numbers in different bases can coexist and be operated upon normally. When an operation involves numbers in different bases, the result will be expressed in the base of the first argument.
For example:
#Ah 1 + --> #Bh 1 #Ah + --> 11 #1000b 2 / --> #100b
Only integer numbers in the range -2^63 to (2^63)-1 can be expressed in multiple bases. Real numbers with fractional part or integers outside the mentioned range will automatically be switched to decimal representation:
#6h 2 / --> #3h
#7h 2 / --> 3.5
#7FFFFFFFFFFFFFFFh #1h + --> 9223372036854775808
#2h 62 ^ --> #4000000000000000h
#2h 63 ^ --> 9223372036854775808
Negative numbers in other bases are represented with the negative sign in front of the # symbol:
#3h 5 - --> -#2h
while #-2h produces a syntax error.
Integers in the range -2^63 to (2^63)-1 can also be operated upon as binary integers, by using the operators BADD, BSUB, BMUL, BDIV, BAND, BOR, BXOR. These operators perform bitwise binary operations assuming the numbers are binary integers of a fixed length. This fixed length is determined by the system Word Size, which can be set with the command STWS (STore Word Size), and can be retrieved with the RCWS command (ReCall Word Size).
The system Word Size is the number of bits to use for the binary numbers, not counting the sign bit. Base numbers are always signed, therefore there is always an extra sign bit.
For example, to work with 32 bit signed numbers, set the Word Size to 31 bits, and all bitwise operators mentioned above will work within the range -2^31 to (2^31-1). Notice that negative numbers will not be displayed as two's complement, but as negative integers instead. The maximum system Word Size is 63 and the minimum is 1.
A few examples, assuming the Word size of 15 was set:
32767 1 BADD --> -32768 32767 -32768 BADD --> -1 32767 -32768 BSUB --> -1 32767 -32768 BOR --> -1 #1h 255 BXOR --> #FEh
Angles
Angles are represented in newRPL with a special syntax (using the angle symbol, In Alpha mode, Right-Shift-6):
∡90 ∡90° ∡90r ∡90g ∡90d
The angle symbol indicates that the number represents an angle, while the suffix indicates the system used to measure the angle. In the first example, the lack of suffix indicates an angle in the currently selected angle system. The user can change the current angle system from the GUI or using the DEG, GRAD, RAD and DMS commands.
Angles are a new object type in newRPL.
The degree symbol indicates degrees using decimal representation, the 'r' indicates radians, 'g' indicates grads or gons, and finally 'd' indicates degrees in sexagesimal system (degrees, minutes and seconds). The number has the syntax DDD.MMSS, where DDD=degrees, MM=minutes and SS=seconds. Not all digits might be displayed, depending on current display settings. Angles are always displayed with a suffix, but the user can enter an angle with or without a suffix, and uppercase suffixes are also accepted (d and D, g and G, r and R will be considered valid suffixes for data entry).
Addition/subtraction between angles returns an angle, expressed in the system of the first argument (level 2). Other operations will first convert the angle to the current system angle settings, then proceed with the operation as a number.
∡90° ∡50g - --> ∡45° ∡50g ∡90° - --> ∡-50g ∡90° 2 * --> 180 (when the system is in DEG mode) ∡90° 2 * --> 3.1415926... (when the system is in RAD mode)
Operations other than addition and subtraction return real numbers, not angles. Operations that take real numbers will accept angles, by first converting them to the current angle system.
Angle objects have a short life. Most operations will consider angles as real numbers, then operate and return real numbers.
Conversion to/from angle to real numbers can be done using the following commands:
→∡° --> Tag a real number as an angle in degrees. →∡r --> Tag a real number as an angle in radians. →∡g --> Tag a real number as an angle in grads. →∡d --> Tag a real number as an angle in degree/minute/second. A→∡° --> Convert angles to degrees. A→∡r --> Convert angles to radians. A→∡g --> Convert angles to grads. A→∡d --> Convert angles to degree/minute/second.
The first 4 commands simply make an angle object from a real number. The second group of commands will convert angle objects to the desired angle system. When given a real number, it will be assumed to represent an angle based in the current system settings. When given any other object containing angular quantities (such as polar vectors or complex numbers), real numbers will be left untouched, and angles will be converted to the desired system.
Trigonometric functions will accept angle objects in any system as arguments (real arguments will be considered angles in the system selected in the current settings). Inverse trigonometric functions return angle objects in the current system settings.
Angle objects need 4 bytes in addition to the storage for the real number they contain.
Complex Numbers
Complex numbers are an ordered pair of two Real Numbers, surrounded by parenthesis and separated by a comma or a space:
(1 0) (0,1)
are valid complex numbers representing the real number 1 and the imaginary number i respectively.
The same syntax rules of real numbers apply to each component of complex numbers.
Complex numbers can be expressed in polar form, where the second component is an angle object. The syntax is identical:
(1 0) is the same as (1,∡0°) (0,1) is the same as (1,∡90°)
Complex numbers in polar form will retain the polar form until converted to cartesian form. Operations will automatically convert between polar and cartesian forms as needed. Results of operations will come in the format of the first argument (level 2) whenever possible.
(1 0) (1,∡90°) + --> (1 1) (1,∡90°) (0 1) + --> (2 ∡90°)
Polar complex numbers in newRPL retain their polar form. Polar and cartesian numbers can coexist in the stack.
The commands →POLAR and →RECT will convert a complex number to/from its polar form. Explicit conversion is not necessary for operations, any command accepting complex numbers will automatically convert them as needed.
A few details about complex numbers:
- The user is allowed to enter integer real or imaginary components in bases other than 10. However, operations on complex numbers will produce results expressed in base 10 regardless of the base used in each operand's components.
- In polar form, angles can be input in any system, regardless of the current system settings.
- The storage space required for a complex number is the sum of the space required for its components, plus 4 bytes.
Identifiers
Identifiers are objects used to provide a human readable designation to other objects. They are used to store the names of variables, directories, functions, constants and symbols within expressions.
Identifiers in newRPL may or may not be enclosed in single quotes ('), depending on the context where they are being used. Inside an RPL program, names that are enclosed in quotes refer to the variable name itself, while unquoted names refer to the contents of the variable.
For example the following program stores the number 4 inside a variable named 'A', then doubles the value and leaves the result in the stack:
« 4 'A' STO A 2 * »
In this case, the arguments to the command STO are the number 4 and the variable name 'A', hence the name needs to be enclosed in single quotes. After the STO command, a variable named 'A' exists and contains the value 4. The next A does not have single quotes, thus referring to the value 4 (the contents of A), which is later multiplied by 2. If the second A had single quotes it would've left its name on the stack 'A' to be later multiplied by 2, resulting in the symbolic expression 'A*2' instead of the number 8 on the stack.
Identifiers can have any length and the names are arbitrary but with some restrictions:
a) Identifiers may not contain the NULL character or any of the following symbols:
+ - * / \ { } [ ] ( ) # ! ^ ; : < > = , " ' _ ` @
b) Identifiers may not begin with a number, and may not contain spaces
c) Identifiers that begin with a dot are reserved for the system and should not be used by the user.
d) Identifiers that have identical names as system commands are valid, but its use is discouraged.
When an identifier matches exactly the name of a system command, it cannot be used without quotes in a program because the compiler would interpret it as a command, not as a name. For example:
<< 3 'RAND' STO RAND 1 + >>
creates a variable named RAND, with value of 3. However, when using this variable unquoted, the compiler uses the command RAND instead, which produces a random number between 0 and 1, instead of the expected number 3. This causes the program to produce random results. The correct way to use this identifier would be by always enclosing it in quotes:
<< 3 'RAND' STO 'RAND' RCL 1 + >>
which produces the expected result of 4, because now the program clearly shows the identifier 'RAND' being explicitly recalled to the stack using RCL.
Symbolic Expressions
Symbolic expressions in newRPL are always enclosed in single quotes. Examples of valid expresions are:
'X^2/2' 'SIN(X*180/PI)' 'COS(X)^2+SIN(X^2)'
In the examples above, X is an identifier used within a symbolic. A variable named X may or may not exist, and its presence does not affect the symbolic expression. Some RPL commands are allowed to be used within expressions, and in that case they take the form of functions. Functions are used by specifying the name, and a list of arguments surrounded by parenthesis, with multiple arguments separated by comma. The examples above use SIN() and COS() commands as functions.
Expressions are stored as-written until they are explicitly evaluated or modified by means of using commands EVAL, EVAL1 or ->NUM, or any other command that does symbolic manipulation. The 3 evaluation commands mentioned before try to replace any identifiers with their values, each with a particular behavior:
- EVAL1 will replace all identifiers with their values, and apply basic numeric simplification. Variables that don't exist are kept as symbolic identifiers.
- EVAL will recursively replace identifiers until the expression doesn't change anymore, then apply basic numeric simplification. Variables that don't exist are kept as symbolic identifiers.
- ->NUM will recursively replace identifiers until all have been replaced, and will try to present a numeric result. It errors if an identifier doesn't exist as a variable.
This becomes much simpler to understand with an example:
« '1/3' 'X' STO 'X^2' 'Y' STO 'Y+1' EVAL1 »
This program stores the symbolic expression '1/3' in variable X, then the expression 'X^2' in variable Y. Finally, it does EVAL1 on the expression 'Y+1'.
The EVAL1 command will do a single pass of variable replacements. In this case, the variable Y will be replaced with its contents, leaving 'X^2+1' on the stack.
The EVAL1 command is new in newRPL, performing a single step of algebraic processing. It allows algebraic operations when there are circular references, and a recursive evaluation with EVAL would generate an error.
If EVAL is used in place of EVAL1, the evaluation is recursive, so the first step would turn 'Y+1' into 'X^2+1', then a second pass would replace the X variable, leaving '(1/3)^2+1' in the stack.
If ->NUM is used instead, the evaluation tries to produce a numeric result, so it would continue the evaluation of the numeric portions of the expression to arrive at the answer: 1.11111111...
If the variable Y had not been defined in the example above, both EVAL and EVAL1 would leave the expression 'Y+1' unmodified, while ->NUM would issue an error because a numeric answer could not be found.
Operators between symbolic expressions produce other symbolic expressions:
'X+1' 'Y-1' + --> 'X+1+Y-1' 'X+1' 2 / --> '(X+1)/2' 'X+1' (1,1) + --> 'X+1+1+i'
Expressions can be operated with numbers and other object types, and in such case the other object will be converted to symbolic form, as in the example above. However, not all object types are allowed within a symbolic.
Any identifier can be used in a symbolic expression. Identifiers matching the names of commands are discouraged. Using these names within symbolic expressions is only acceptable if the system command is not allowed in symbolics. An identifier named STO would be acceptable, since STO is not permitted to be used within a symbolic expression. Other commands are allowed to be used in expressions and can cause confusion. For example:
3 'INV' STO 'INV' EVAL --> 3 'INV+1' EVAL --> ERROR 'INV(2)+1' EVAL --> '3/2'
The first line creates an identifier INV, identical to the inverse operator 1/x. The second line uses the identifier name by itself in quotes which is acceptable, and correctly returns the value upon evaluation. The third line uses the identifier in a symbolic expression, which will cause the compiler to issue an error, as it expects the symbolic function INV(). Finally, the last line has the correct syntax for INV within symbolics, as a function, and performs the expected operation upon evaluation.
Based on the examples above, the use of identifiers matching command names is strongly discouraged, but acceptable if the user is aware of its limitations.
Strings
Strings are containers for arbitrary text. Strings are enclosed in double quotes, and may contain any character, including the NUL character (U+0000). In newRPL, strings are UTF-8 encoded.
The operator + is used to concatenate strings:
"HELLO" " " "WORLD" + + --> "HELLO WORLD"
Objects can be converted into strings (decompiled) with the →STR command. Strings containing the source code of programs or objects can be compiled with the STR→ command.
Strings in newRPL are always Unicode NFC normalized for maximum compatibility with other devices. Strings imported from other devices should be NFC normalized for proper operation in newRPL. When the source device doesn't guarantee text in a normalized form, this can be done in the calculator with the →NFC command.
Vectors and matrices
Vectors/matrices are container objects that can hold other objects. Only certain object types are allowed within a matrix or vector: integer or real numbers, complex numbers and symbolic expressions or single identifiers. Objects of different types can coexist within the same matrix. Matrices are limited to one and two dimensions (rows x columns) in newRPL.
Vectors are one-dimensional elements, but otherwise they behave exactly like matrices. Matrices and vectors are created using the square brackets [ ]. Each row needs to be enclosed in brackets, and matrices with more than one row need a second set of brackets enclosing all rows. For example:
[ 1 2 3 ] --> Valid (3 elements) vector [[ 1 2 3 ]] --> Valid (1x3) matrix. [[1 2 3] [4 5 6]] --> Valid (2x3) matrix [[1] [2] [3]] --> Valid (3x1) matrix
The first and second examples contain the same elements, but the first one is a vector and the second one a matrix.
When working with matrices, the following intuitive rules apply:
- Operations on matrices follow the standard mathematical rules.
- One-dimensional vectors can be considered as row or column vectors (comparable with 1xN and Nx1 matrices respectively) when operating with a matrix, whichever orientation makes the operation possible.
- If the result of an operation between a matrix and a vector is (1xN) or (Nx1), then the result will also be represented as a one-dimensional vector. Otherwise they will produce a matrix (or a scalar).
- Vector-vector multiplications are performed as dot products, producing a scalar result.
The following are examples of operations on matrices and vectors:
[ 1 2 3 ] [ 1 2 3 ] + --> [ 2 4 6 ]
Here, both vectors are one-dimensional and addition proceeds normally.
[[1] [2] [3]] [ 1 2 3 ] + --> [ [2] [4] [6] ]
The second example adds a (3x1) matrix and a 3-element vector. In this case, the vector is considered a column vector to make the operation possible, producing a (3x1) matrix.
[[1] [2] [3]] [[1] [2] [3]] + --> [[ 2 ] [ 4 ] [ 6 ]]
The example above adds (3x1) by (3x1) matrices (or two column vectors). In this case addition proceeds normally since they are of compatible dimensions.
[ 1 2 3 ] [ 1 2 3 ] * --> 14
This operation multiplies two 3-element vectors. They are treated as a row vector and a column vector, in which a matrix multiplication can be performed (1xN)x(Nx1). In this case multiplication of vectors becomes similar to a vectorial dot product.
[[ 1 2 3 ]] [ [1] [2] [3] ] * --> [[ 14 ]]
This operation multiplies two 3-element vectors represented as 1x3 and 3x1 matrices. In this case the multiplication of 2 matrices produces another matrix, in this case a (1x1) matrix.
This is a vector (considered as a 1x3 matrix to make the operation possible) by a (3x1) matrix multiplication. The result is a scalar value.
[[1] [2] [3]] [ 1 2 3 ] * --> [[ 1 2 3 ] [ 2 4 6 ] [ 3 6 9 ]]
This example does a (3x1) by (1x3) multiplication (the vector is considered a 1x3 matrix). In this case, multiplication proceeds normally, producing a (3x3) matrix.
[[1] [2] [3]] [[1] [2] [3]] * --> INVALID DIMENSION
In this case, a (3x1) by (3x1) multiplication is invalid because both operands are matrices of incompatible size.
[[ 1 2 ] [ 3 4 ]] [ 1 2 ] * --> [ 5 11 ]
Here, a (2x2) matrix is multiplied by a 2-element vector. The vector is auto-transposed to produce a (2x2) by (2x1) multiplication, resulting in a (2x1) matrix. However, since the original argument was a vector, the result is expressed also as a one-dimensional vector.
[[ 1 ] [ 2 ]] [[ 1 2 ] [ 3 4 ]] * --> INVALID DIMENSION
Finally, a (2x1) column matrix is multiplied by a (2x2) matrix. Since both arguments were expressed as matrices, the product fails.
Lists
Lists are arbitrary arrangements of other objects. Lists can contain any type of object, including lists. Lists of objects are enclosed in curly braces { }.
Arithmetic operations on lists are defined element-by-element. Individual elements can be added to a list by using the command ADD.
This behavior is reversed in newRPL. Classic RPL uses the command ADD to perform an element-by-element addition, while the + operation is used to append elements.
{ 1 2 } 3 ADD --> { 1 2 3 }
3 { 1 2 } ADD --> { 3 1 2 }
Here the element 3 is added to the end and beginning of the list respectively.
{ 1 2 } 3 + --> { 4 5 }
0 { 1 2 } * --> { 0 0 }
Operating a single element with a list will do the operation between the single element and each element of the list.
{ 2 3 } { 4 5 } + --> { 6 8 }
{ 2 3 } { 4 5 } * --> { 8 15 }
Doing an operation on lists of identical sizes performs the operation between corresponding elements. If sizes are not identical, it errors.
{ 1 2 } [1 2 3] * --> { [1 2 3] [2 4 6] }
Results from operations of different types of objects are identical to what would be obtained if each operation was carried on separately on the stack. The first example divides each element by a symbolic '2', causing it to return two symbolic expressions. In the second case, a vector is being multiplied with each element in the list (a scalar value), producing a list of vectors.
These examples are applied recursively when lists of lists are found.
{ {1 2} {3 4} } 4 * --> { { 4 8 } { 12 16 } }
In this case, the number 4 multiplies each list, and such operation between a number and a list will also be performed element-by-element, producing the resulting list of lists.
The EVAL command applied to a list performs an EVAL of each individual element.
Programs
Programs are delimited with « » brackets. Immediate programs are delimited with the double colon and semi colon pairs (:: ;). Programs and immediate programs can contain any sequence of objects and commands, that are to be executed by the RPL core. The only difference between these two types of programs is when the execution takes place.
When the execution core reaches a programs using « », the program object is put on the stack. The user has to explicitly run the program by using XEQ or EVAL commands. On the other side, when the RPL core reaches a program using :: ; delimiters, it executes the program immediately instead of putting it on the stack.
For example, the command line is executed immediately when ENTER is pressed, thanks to implicit :: ; delimiters (which are not visible on the screen). Since the command line has implicit immediate delimiters, there is no need for the user to include them and therefore :: ; are seldom used in practice. The main use of immediate programs is when nested within another program, to limit the scope of local variables, as all local variables created within the delimiters will be cleaned up when the semicolon is reached.
The immediate programs :: ; are a feature of newRPL, not included in classic RPL, but included in its parent language System RPL. It is made available to the user due to the more liberal use of local variables in newRPL.
Directories
Directories are pseudo objects representing permanent storage containers for other objects. All other object types can be contained within a directory. Each object within a directory has an associated identifier, so that the object can be referenced by name.
Directories are where global variables are stored.
A directory may contain other directories inside (from now on called subdirectories), forming a tree structure much like a file system.
The root directory is called HOME, and initially does not contain any other subdirectories. Subdirectories can be created and deleted by the user with the CRDIR (create dir) and PGDIR (purge dir) commands.
Only one directory is active at any given time, and is called the system current directory. Global variables are searched for first in the current directory, and if not found, they are searched for in its parent directories, all the way up to HOME.
The variables soft menu displays the contents of the current directory. The user may change the current directory by either using the name of a subdirectory unquoted or run EVAL on the quoted name. The UPDIR command will move to the parent of the current directory, and the HOME command will set HOME as the current directory.
Objects are stored in the current directory using the STO command, and retrieved by either the RCL command or using the unquoted name of an object.
Directories are pseudo-objects, as opposed to an actual object. The object in the stack is not the directory itself, but a small object created specifically to represet a directory. Directories are not physically stored as objects in memory, they are in a separate memory area. While there is an object representing a directory that can be put on the stack, evaluated or stored in other directories, it cannot be viewed, decompiled to text or edited in the command line.
Directory objects in newRPL are pseudo-objects, and unlike in classic RPL, they cannot be decompiled to text and edited in the command line.