2.2.2  ITEM: 1. Sequences: operator :

The : operator is used to construct vectors with (usually) more than one value.  In particular, it is used to construct sequences, and so it is called the sequence operator.  Given operands x and y (standing for any two numbers), the sequence operator starts at x and counts, by 1 (or -1, as appropriate) toward y without passing it.  It yields a vector containing all of the numbers it encounters along the way.

Note that the sequence operator can count down as well as up, that it can handle float as well as integer operands, and that negative numbers are allowed.

2.2.4  ITEM: 2. Subsets: operator []

The [] operator selects a subset of the vector upon which it operates; it is thus often called the subset operator.  It can work in one of two different ways, depending upon whether it is given an integer vector of indices, or is given a logical vector of selectors.

First of all, a subset can be selected with an integer vector of indices.  These indices are zero-based, like C but unlike R; the first value in a vector is thus at index 0, not index 1.  Note that a given index can be used multiple times.

Second, a subset can be selected with a logical vector of selectors.  In this case, the logical vector must be the same length as the vector being selected; each logical value indicates whether the corresponding vector value should be selected (T) or not (F).

2.3.1  ITEM: 3. Arithmetic operators: +, -, *, /, %, ^

These are the standard operators of arithmetic; + performs addition, - performs subtraction, * performs multiplication, / performs division, % performs a modulo operation (more on that below), and ^ performs exponentiation.  Not a great deal needs to be said about these operators, which behave according to the standard rules of mathematics.  They also follow the standard rules of “precedence”; exponentiation is the highest precedence, addition and subtraction are the lowest precedence, and the other three are in the middle, so 4^2+5*6^7 is grouped as (4^3)+(5*(6^7)), as expected if you remember your grade-school math.

There are only a few minor twists to be discussed.  One is the meaning of the % operator, which many people have not previously encountered.  This computes the “modulo” from a division, which is the remainder left behind after division.  For example, 13%6 is 1, because after 13 is divided evenly by 6 (taking care of 12 of the 13), 1 is left as a remainder.  Probably the most common use of % is in determining whether a number is even or odd by looking at the result of a %2 operation; 5%2 is 1, indicating that 5 is odd, whereas 6%2 is 0, indicating that 6 is even.

Another twist is that both the division and modulo operators in Eidos operate on float values – even if integer values are passed – and return float results.  (For those who care, division is performed internally using the C++ division operator /, and modulo is performed using the C++ fmod() function).  This policy was chosen because the definitions of integer division and modulo vary widely among programming languages and are contested and unclear (see Bantchev 2006, http://www.math.bas.bg/bantchev/articles/divmod.pdf).  If you are sure that you want integer division or modulo, and understand the issues involved, Eidos provides the functions integerDiv() and integerMod() for this purpose.  Besides side-stepping the vague definitions of the integer operator, this policy also avoids rather common bugs involving the accidental use of integer division when float division was desired – a much more common occurrence than vice versa.

A third twist is that + and - can both act as “unary” operators, meaning that they are happy to take just a single operand.  This is standard math notation, as in the expressions -6+3 or 7*-5; but it can sometimes look a bit strange, as in the expression 5--6 (more easily read as 5 - -6).

A fourth twist is that the ^ operator is right-associative, whereas all other binary Eidos operators are left-associative.  For example, 2-3-4 is evaluated as (2-3)-4, not as 2-(3-4); this is left-associativity.  However, 2^3^4 is evaluated as 2^(3^4), not (2^3)^4; this is right-associativity.  Since this follows the standard associativity for these operators, in both mathematics and most other programming languages, the result should generally be intuitive, but if you have never explicitly thought about associativity before you might be taken by surprise.

A fifth twist is that the arithmetic operators and functions in Eidos are guaranteed to handle overflows safely.  The float type is safe because it uses IEEE-standard arithmetic, including the use of INF to indicate infinities and the use of NAN to represent not-a-number results; this is the same as in most languages.  In Eidos, however, the integer type is also safe, unlike in C, C++, and many other languages.  All operations on integer values in Eidos either (1) will always produce float results, as the / and % operators do; (2) will produce float results when needed to avoid overflow, as the product() and sum() functions do; or (3) will raise an error condition on an overflow, as the Eidos operators +, -, and * do, as well as the abs() and asInteger() functions.  This means that the integer type in Eidos can be used without fear that overflows might cause results to be incorrect.

The final twist is really a reminder: everything is a vector.  These operators are designed to do something smart, when possible, with vectors of any length, not just with single-valued vectors as shown above.  In general, the operands of these arithmetic operators must either be the same length (in which case the elements in the operand vectors are paired off and the operation is performed between each pair), or one or the other vector must be of length 1 (in which case the operation is performed using that single value, paired with each value in the other operand vector).

2.3.2  ITEM: 4. Logical operators: |, &, !

The |, &, and ! operators act upon logical values.  If they are given operands of other types, those operands will be “coerced” to logical values following the rule mentioned above: zero is F, non-zero is T (and for string operands, a string that is zero characters long – the empty string, "" – is considered F, while all other string values are considered T).

As to what they do: | is the “or” operation, & is the “and” operation, and ! is the “not” operation.  As in common parlance, “or” is T if either of its operands is T, whereas “and” is T only if both of its operands are T.  The “not” operator is unary (it takes only one operand), and it negates its operand; T becomes F, F becomes T.  As with the arithmetic operators, these operators work with vector operands, too – either matching up values pairwise between the two operands, or applying a single value across a multivalued operand.

Those familiar with programming might wish to know that the | and & operators do not “short-circuit” – they can’t, because they are vector operators. If the & operator first sees an operand that evaluates to F, for example, it knows that it will produce F value(s) as a result; but it does not know what size result vector to make. If a later operand is a multivalued vector, the & operator will produce a result vector of matching length; if all later operands are also length 1, however, & will produce a result vector of length 1.  To know this for sure (and to make sure that there are no illegal length mismatches between later operands), it must evaluate all of its operands; it cannot short-circuit.  Similarly for the | operator.

These semantics match those in R, for its | and & operators, but they might seem a little strange to those used to C and other scalar-based languages.  For those used to R, on the other hand, it should be noted here that Eidos does not support the && and || operators of R, for reasons of simplicity; it is safer to use the any() or all() functions to simplify multivalued logical vectors before using & or |.  If this is gibberish to you, it is not important; the point here is only to prevent confusion among users accustomed to R.

2.3.3  ITEM: 5. Comparative operators: ==, !=, <, <=, >, >=

These operators compare their left and right operand.  The operators test for equality (==), inequality (!=), less-than (<), less-than-or-equality (<=), greater-than (>), and greater-than-or-equality (>=) relationships.  As seen above with the arithmetic and logical operators, this can work in two different ways: if the operands are the same length, their elements are paired up and the comparison is done between each pair, whereas if the operands are not the same length then one operand must be of length one, and its value is compared against all of the values of the other operand.

Regardless of the types of the operands, these operators all produce a logical result vector.  If the operands are of different types, promotion will be used to coerce them to be the same type (i.e. logical will be coerced to integer, integer to float, and float to string).  Note that this is often not what you want!  You might not want the automatic type promotion that makes 5=="5" evaluate as T, or the vectorized comparison that makes 1:5==4 evaluate as something other than simply F.  You might really want to ask: are two values identical?  For such purposes, the identical() function is a better choice.

2.3.4  ITEM: 6. String concatenation: operator +

The + operator is often used as an arithmetic operator, but it can also act as a concatenation operator for string operands. Concatenation is pasting together; the + operator simply pastes its string operands together, end to end.

In fact, this works with non-string operands too, as long as a string operand is nearby; the interpretation of + as a concatenation operator is preferred by Eidos, and wins out over its arithmetic interpretation, as long as a string operand is present to suggest doing so. The other non-string operands will be coerced to string.  However, this does not work retroactively; if Eidos has already done arithmetic addition on some operands, it will not go back and perform concatenation instead.  To force concatenation in such situations, you can simply begin the expression with an empty string, "".

The concatenation operator also works with vectors, as usual.

Beginning with Eidos 2.2, string concatenation involving NULL concatenates the string value "NULL", just as if NULL were a singleton string vector containing that value.

2.4.1  ITEM: 7. Assignment: operator =

The results of expressions can be saved in variables.  As in many languages, this is done with the = operator, often called the assignment operator.

The assignment operator, =, is different from the equality comparison operator, ==.  In many languages, confusing the two can cause bugs that are hard to find; in C, for example, it is legal to write:

if (x=y) ...

In C, this would assign the value of y to x, and then the expression x=y would evaluate to the value that was assigned, and that value would be tested by the if statement.  This can be useful as a way of writing extremely compact code; but it is also a very common source of bugs, especially for inexperienced programmers.  In Eidos using assignment in this way is simply illegal; assignment is allowed only in the context of a statement like x=y; to prevent these issues.  (This point is mostly of interest to experienced programmers, so if it is unclear, don’t worry.)

Variable names are fairly unrestricted.  They may begin with a letter (uppercase or lowercase) or an underscore, and subsequently may contain all of those characters, and numerical digits as well.  So x_23, fooBar, and MyVariable23 are all legal variable names (although not good ones – good variable names explain what the variable represents, such as selection_coeff).  However, 4by4 would not be a legal variable name, since it begins with a digit.

2.3.5  ITEM: 8. The ternary conditional: operator ? else

Eidos, like many languages, has an if statement that can be used to specify conditional execution of statements, and an if-else construct can be used to provide an alternative code path.  Sometimes, however, one wishes to have conditional execution of an expression, rather than an entire statement.  The if-else construct is particularly inconvenient with assignments involving complex lvalues, such as:

if (condition)

x[index].property = a;

else

x[index].property = b;

It is desirable to provide a way for the user to specify that the choice of rvalue, a or b, should depend upon condition without having to duplicate the lvalue and the assignment.  The R language provides this functionality by making if-else statements result in an rvalue, like an expression.  The C language, on the other hand, provides a ternary conditional operator, ?:, that can be used in expressions to much the same effect.  Eidos straddles the gap with a ternary conditional operator, ? else, that uses the ? initiator of C, but the else token as a continuation as in R.  In the syntax of Eidos, the above conditional assignment can be rewritten as:

x[index].property = condition ? a else b;

This will evaluate condition and result in a if condition is T, or b if condition is F.  That result is then assigned into the lvalue.  Note that, as in C, the precedence of the ternary conditional operator is very low, but higher than operator =, so that parentheses are often not needed to group statements of this type.  The else clause of the ternary conditional is required; there is no equivalent of an if statement without an else, since an rvalue must be produced.

Just as with if-else statements, only the selected subexpression, as determined by the condition, is evaluated; the other subexpression will not be evaluated, so any side effects it might have will not occur.  For example, with the statement:

x = condition ? f1() else f2();

here f1() will be called if condition is T, f2() if condition is F; only the subexpression selected by the condition is evaluated, and so it is never the case that both f1() and f2() are called.

Ternary conditionals may be nested.  Because the operator is right-associative, an expression such as:

z = (a == b ? a else b ? c else d);

is grouped as:

z = (a == b ? a else (b ? c else d));

rather than

z = ((a == b ? a else b) ? c else d);

This is generally desirable, since it provides a flow similar to chaining of if-else if-else statements.  In any case, parentheses may be used to change the order to evaluation as usual.

2.3.6  ITEM: 9. Grouping: operator ()

All of the discussion above involved simple expressions that allowed the standard precedence rules of mathematics to determine the order of operations; 1+2*3 is evaluated as 1+(2*3) rather than (1+2)*3 because the * operator is higher precedence than the + operator.  For the record, here is the full precedence hierarchy for operators in Eidos, from highest to lowest precedence:

[], (), . subscript, function call, and member access

^ exponentiation (right-associative)

+, -, ! unary plus, unary minus, logical (Boolean) negation (right-associative)

: sequence construction

*, /, % multiplication, division, and modulo

+, - addition and subtraction

<, >, <=, >= less-than, greater-than, less-than-or-equality, greater-than-or-equality

==, != equality and inequality

& logical (Boolean) and

| logical (Boolean) or

= assignment

Operators at the same precedence level are generally evaluated in the order in which they are encountered.  Put more technically, Eidos operators are generally left-associative; 3*5%2 evaluates as (3*5)%2, which is 1, not as 3*(5%2), which is 3.  The only binary operator in Eidos that is an exception to this rule is the ^ operator, which (following standard mathematical convention) is right-associative; 2^3^4 is evaluated as 2^(3^4), not (2^3)^4.  The unary +, unary -, and ! operators are also technically right-associative; for unary operators this is of little practical import, however (it basically just implies that the unary operators must occur to the left of their operand; you write -x, not x-, to express the negation of x).

In any case, parentheses can be used to modify the order of operations, just as in math.  This works just as you would expect.

Note that this use of parentheses is distinct from the () operator as used in making function calls.

Finally, note that Eidos 2.4 and earlier (SLiM 3.4 and earlier) had an operator precedence bug: exponentiation was given a lower precedence than unary minus and its siblings, and so the expression -2^2 would evaluate to 4, as (-2)^2, rather than -4, as -(2^2).  This violated standard mathematical precedence rules, and was fixed in Eidos 2.5 (SLiM 3.5).

2.7.1  ITEM: 10. Function calls: operator ()

A function is simply a block of code which has been given a name.  Using that name, you can then cause the execution of that block of code whenever you wish.  That is the first major purpose of functions: the reuseability of a useful chunk of code.  A function can be supplied with the particular variables upon which it should act, called the function’s “parameters” or “arguments”; you can execute a function with the sequence 5:15 as an argument in one place, and with the string "foo" as an argument in another.  That is the second major purpose of functions: the generalization of a useful chunk of code to easily act on different inputs.

In Eidos, you may define your own functions, or you may execute a lambda (i.e., a snippet of code represented as a string value) directly in the Eidos interpreter.  However, a fairly large set of built-in functions are supplied for your use, and the hope is that they will suffice for most purposes.

Functions are called using the () operator.  Function arguments go between the parentheses of the () operator, separated by commas.  Most functions expect an exact number of arguments; many functions, in fact, are even fussier than that, requiring each parameter to be of a particular type, a particular size, or both.  But some, such as c(), are more flexible.

Many functions provide a return value.  In other words, a function call like c(5,6) can evaluate to a particular value, just as an expression like 5+6 evaluates to a particular value.  The result from a function call can be used in an expression or assigned to a variable, as you might expect.

2.8.3  ITEM: 11. Properties: operator .

Objects encapsulate behaviors as well as elements.  One type of behavior is called a property.  A property is a simple attribute of each element in an object.  Properties can be read using the member-access operator, written as . (a period).  The name of a particular property can be used with . to get that property’s value.  Operations on object are vectorized just as they are for all other types in Eidos; the result of the . operator is a vector containing the value of the property for all of the elements of the object operand.

You can also use the member-access operator to write new values to properties that are not read-only, using the = operator to do the assignment into the property selected by the . operator.

2.8.6  ITEM: 12. Method calls: operator () and operator .

Objects encapsulate behaviors as well as elements.  In addition to properties, another type of behavior is called a method.  Methods are very much like functions; they are chunks of code that you can call to perform tasks.  However, each type of object has its own particular methods – unlike functions, which are defined globally.  Methods are more heavyweight than properties; they might involve quite a lot of computation, they might create a completely new object as their result, and they might even modify the object upon which they are called.  Not all methods are heavyweight in this sort of way, however; anything that one might want an object to do, but that does not feel like a simple property of the object, can be a method.  Methods can also take arguments, just like functions, and they can return whole vectors as their result, unlike (read-write) properties, which must refer to singleton values so that multiplexed assignment can work.  Methods are therefore much more powerful than properties.

Methods are called using the member-access operator, ., with a syntax that looks a lot like accessing a property, but combined with the function call operator, ().  That might look like:

object.method()

Naturally, method calls are also vector operations.  For a multi-element object, a single method call will result in the method call being multiplexed out to all of the elements of the object, and the results from all of those method calls will be concatenated together in the same way that the c() function performs concatenation (including dropping of NULLs and type promotion, potentially).