Operators

An operator is a special symbol that, combined with one or more operands, performs an operation using those operands and, generally, produces a value.

Lasso supports the standard arithmetical operators and logical operators as well as numerous other useful operations. Operators can be unary, taking only one operand, binary requiring two operands, or ternary, in the case of the conditional operator, requiring up to three operands.

Lasso permits the behavior of some operators to be controlled by the operand objects themselves. This is accomplished in an object by having it implement a method whose name matches the symbol for that operator. For example, a type that needed to support addition would implement a method named + accepting one parameter and returning the result of combining it with the type instance.

Assignment Operations

Assignment places the result of an expression into a destination. The destination must be a local or thread variable, or it must be an appropriately named method call. Lasso supports two types of assignment operators: assign-produce (:=) which produces the assigned value, and standard assignment (=) which does not.

// "dest" assigned value of expression
dest = expression

// "dest" assigned value of expression, "dest" produced
dest := expression
// => // Produces a reference to "dest"

An assign-produce operation, which produces the left-hand operand, is right-associative so that multiple assignments can be lined up. The following assigns “1” to “dst1”, “dst2” and “dst3” and also produces “1”:

dst1 := dst2 := dst3 := 1
// => 1

Locals and vars can both be assigned using assignment syntax.

// local "l" assigned expression
#l = expression

// local "l" assigned expression
local(l) = expression

// var "v" assigned expression
$v = expression

// var "v" assigned expression
var(v) = expression

Variables and data members are the only elements to which values can truly be assigned, but Lasso permits methods to be created that mimic the act of assignment. This is done by naming the method with a “=” character at the end. For example, a method that wanted to accept assignment for foo would be named foo=. Such a method must accept at least one parameter and must return the assigned value as if it were being called in the role of assign-produce (:=). Methods that permit such assignment are useful as “setters” and let an object control how the assignment is ultimately made. See the Types chapter for more detail on creating setter methods.

Arithmetical Operations

“Arithmetic” typically refers to mathematical operations using integer or decimal numbers, as explained in the Math chapter. However, an arithmetical operator can be applied to any object that supports the operation.

Basic Operators

These operators are all binary, requiring two operands. All of these operators can be implemented by a type containing the properly named method. Only the left-hand operand’s method is called. None of these operators should modify either operand, but must return a new object. The examples that follow show the use of each operator:

op1 + op2
// => // Returns the value of adding op2 to op1

op1 - op2
// => // Returns the value of subtracting opt2 from op1

op1 * op2
// => // Returns the value of multiplying op1 by op2

op1 / op2
// => // Returns the value of dividing op1 by op2

op1 % op2
// => // Returns the remainder of dividing op1 by op2 (modulo operation)

(: 10 + 3, 10 - 3, 10 * 3, 10 / 3, 10 % 3)
// => staticarray(13, 7, 30, 3, 1)

Assignment Operators

While the basic arithmetical operators use their operands to produce a new value, Lasso supports syntax for applying the operator to one of the operands. The following operators perform their operation and assign the result to the left-hand side operand. Only the left-hand operand can be assigned to and not every expression is capable of being assigned to, as described in the section on assignment operations. These assignment expressions do not produce a value.

// Equivalent to op1 = op1 + op2
op1 += op2

// Equivalent to op1 = op1 - op2
op1 -= op2

// Equivalent to op1 = op1 * op2
op1 *= op2

// Equivalent to op1 = op1 / op2
op1 /= op2

// Equivalent to op1 = op1 % op2
op1 %= op2

During parsing, these operators are expanded to their regular arithmetical and assignment operations, so a type does not need to do anything to support them aside from implementing the assignment operator method and the appropriate arithmetical operator method.

Pre-/Post-Increment and Decrement Operators

There is a common need to “advance” an object in a bidirectional manner. Usually this is done using integers as counters, though the concept can be applied elsewhere. Lasso supports the increment and decrement operators (++ and --) in both pre and post modes.

Pre-incrementing and pre-decrementing an object will add or subtract 1 to or from the object and then produce that object as a result. Post-incrementing and post-decrementing an object first copies that object, then adds or subtracts 1 to or from the original operand, then produces the copied object as a result.

// Pre-increment "op"
++op
// => // Produces the newly incremented "op"

// Pre-decrement "op"
--op
// => // Produces the newly decremented "op"

// Post-increment "op"
op++
// => // Produces a copy of "op" before incrementing

// Post-decrement "op"
op--
// => // Produces a copy of "op" before decrementing

These increment/decrement operators are translated into regular arithmetical method calls with “1” as the method parameter. This means that if a type is intended to be used with the increment (++) and decrement (--) operators, all that’s necessary is to implement + and - which will be called with “1” as the parameter.

Positive and Negative Operators

Lasso supports the unary operators which are typically intended to change the sign of an integer or decimal number. These operators can be applied to any object that supports them. When applied, these operators will produce a new object, leaving the single operand unchanged.

+op1
// => // Produces a new object whose value is positive op1

-op1
// => // Produces a new object whose value is negative op1

Types can implement this operator by defining a method named + or - that accepts no parameters. When unary + or - is applied to integer or decimal literals, no method call is generated. Instead, the positive or negative number is created from the beginning.

Boolean Operations

Boolean describes the values “true” and “false”. Lasso supports several operators that either treat their operands as boolean values and/or produce boolean values. These operators are broken down into several categories. (See the definition of boolean for how other other values are cast to boolean types.)

Logical Operators

There are three logical operators. The first is the unary operator “not”. This operator treats its single operand as a boolean value and produces the opposite of that value. The “not” operator turns a “true” into a “false” and a “false” into a “true”. Although the operand can be of any type, this operator always produces a “true” or “false” value. The “not” operator can take one of two forms: an exclamation mark (!) or the not keyword.

!true
// => false

not false
// => true

The other two logical operators are logical “and” and logical “or”, and they also can take two forms: double ampersands (&&) or the and keyword for logical “and”, and double pipes (||) or the or keyword for logical “or”.

These binary operators treat their first operand as a boolean value and perform their operation based on that value. Logical “and” inspects its first operand, and if it is “true”, produces its second operand. If the first operand is “false”, logical “and” will produce the value “false”. Logical “or” inspects its first operand, and if it is “true”, produces that first operand. If the first operand is “false”, logical “or” will produce the second operand.

op1 && op2
// => // Returns "false" if either op1 or op2 evaluates to "false" else opt2

op1 || op2
// => // Returns op1 if it evaluates to "true" else op2

These operators perform shortcut evaluation, meaning that if the result of the operation is determined before the second operand is evaluated, the second operand will not be evaluated. Also note that the behavior of the logical operators cannot be defined by the operand objects.

Equality Operators

The equality operators are used to determine if one object is logically equivalent to another. These operators are split into positive and negative equality tests as well as strict and non-strict equality tests. A positive equality test checks if one object is equal to another object while a negative equality test checks if an object is not equal to another. Strict equality testing further tests the types of the operand objects. If the right-hand operand is not an instance of the type of the left-hand operand, the equality test fails. These operators all produce either a “true” or “false” value.

op1 == op2
// => // Produces "true" if op1 is equal to op2 else false

op1 != op2
// => // Produces "true" if op1 is not equal to op2 else false

op1 === op2
// => // Produces "true" if op1 is both equal to and the same type as op2 else false

op1 !== op2
// => // Produces "true" if op1 is not equal to or not the same type as op2 else false

(: 3 == 3.0, 3 != 3.0, 3 === 3.0, 3 !== 3.0)
// => staticarray(true, false, false, true)

For allowing an object to be tested for equality against another, its type must implement a method named onCompare, which is automatically called at runtime to perform equality checks. It must require one parameter for the right-hand operand, which will be compared to the left-hand operand. When called, it indicates whether the left-hand operand is less than, equal to, or greater than the right-hand operand by returning either an integer less than zero, zero, or greater than zero, respectively. The act of checking the object types in the case of strict equality testing is automatically performed by the runtime, so a type need not account for that scenario in its own implementation of onCompare.

Relative Equality Operators

The relative equality operators indicate whether an object is less than, greater than, or possibly equal to another object. These operators all produce either a “true” or “false” value.

op1 < op2
// => // Produces "true" if op1 less than op2 else "false"

op1 > op2
// => // Produces "true" if op1 greater than op2 else "false"

op1 <= op2
// => // Produces "true" if op1 less than or equal to op2 else "false"

op1 >= op2
// => // Produces "true" if op1 greater than or equal to op2 else "false"

Types control how equality checks behave by implementing the onCompare method as described above in the section on equality operators. Because onCompare is required to return an integer value (either zero, less than zero, or greater than zero), it can handle all possible types of equality tests.

Containment Operators

There are two containment operators used to test if an object “contains” another object. One checks for positive containment (>>) and the other for negative containment (!>>). Both are binary operators and both produce either a “true” or “false” value.

op1 >> op2
// => // Produces "true" if op2 is contained within op1 else false

op1 !>> op2
// => // Produces "true" if op2 is not contained within op1 else false

To support containment testing, a type must implement a method named contains which requires one parameter for the right-hand operand and returns a boolean “true” or “false”. Only the left-hand operand will have its contains method called.

Containment testing only logically applies to certain types of objects. For example, it makes no sense to ask what an integer object contains, because it is scalar, consisting of only one value. Containment testing is primarily done on collection types such as array or map. Objects of those types can contain any number of other arbitrary objects, so it makes sense to query them for their contents.

Conditional Operator

The conditional operator allows for concisely implementing if/then/else logic in which an expression is tested and depending on its boolean value, either the “then” or the “else” expressions will be executed and their values produced as the result of the operator. The “then” and “else” can consist of only one expression. The “else” portion of a conditional operator may be omitted. In such a case, if the condition is “false”, a “void” object will be produced.

The conditional operator is a ternary operator consisting of the two “?” and “|” characters. The “?” follows the test condition and the “|” delimits the “then” and “else” expressions. A conditional operator with no “else” condition will have no delimiting “|” character.

test ? expression1 | expression2
// => // Produces expression1 if test is "true" else expression2

test ? expression
// => // Produces expression if test is "true" else void

Grouping

Sub-expressions can be grouped together by surrounding them with parentheses. This can alter the normal precedence of some operations. All subexpressions in parentheses are evaluated before the expressions surrounding them. The first example below shows how multiplication normally occurs before addition. The second example applies parentheses to have the addition take precedence.

2 * 5 + 7
// => 17

2 * (5 + 7)
// => 24

Invocation

Parentheses can be applied to some expressions in order to invoke the value. Invoking can have different results for different objects. By default, most objects return a copy of themselves when they are invoked. Methods, when invoked, execute the method, returning its value.

Invoking an object by applying parentheses is always equivalent to directly calling the method named invoke. The following examples invoke a local variable and a thread variable with no parameters:

#lv()
// => // Produces the value of invoking the object stored in the local "lv"

$tv->invoke
// => // Produces the value of invoking the object stored in the var "tv"

Parameters may be given to an invoke. The following invokes #lv with three parameters:

#lv(1, 'two', 3)
// => // Produces the value of invoking the object stored in the local "lv" with those parameters

See the Types chapter for more information on the invoke callback.

It is also possible to dynamically generate parameters and programmatically pass them into an invocation. By first adding the parameters to an array named “my_params” and including a colon after the opening parenthesis of the invocation statement, the following example results in an equivalent invocation as the previous.

local(my_params) = array(1, 'two', 3)
#lv(: #my_params)
// => // Produces the value of invoking the object stored in the local "lv" with those parameters

This form is useful for passing a set of values from an object of any type supporting trait_forEach to a method that accepts a rest parameter.

define printArgs(...) => with i in #rest do stdoutnl(#i)
printArgs(: #my_params)

// =>
// 1
// two
// 3

The concept behind invocation is somewhat abstract, but it permits objects and methods to operate as function objects. This is an object that can be called upon to do an operation with zero or more parameters and produce a value. For example, a sorting routine could employ such an object to handle the actual comparisons between two objects, invoking the object each time it is required, while the routine handles only the shifting of the objects during the sort.

This technique would permit the sorting routine to be customized for a wide variety of object types as well as ascending and descending directions by just switching out the objects designated to handle each permutation while keeping the internal operations identical.

Target Operation

To target means to access a particular member method or data member from an object. The target operator (->) is a binary operator accepting the target object as the left-hand operand and the method name as the right-hand operand. Targeting a member method always executes that method, passing along any given parameters.

#lv->meth()
// => // Produces the value of calling meth() on the object stored in #lv with no parameters

#lv->meth
// => // Same as the first example, showing parentheses are optional

#lv->meth(40)
// => // Produces the value of calling meth() on the object stored in #lv with 1 parameter

#lv->meth(40, 'sample')
// => // Produces the value of calling meth() on the object stored in #lv with 2 parameters

Accessing a data member is accomplished through a similar syntax, but by surrounding the name in single quotes. A data member can only be accessed from within the type in which the data member is defined. When accessing a data member, it is an error to have any value except for self as the left-hand operand, and the right-hand operand must be single-quoted.

self->'dMem'
// => // Produces the value stored in the "dMem" data member

As it is very common to access data and methods using the current “self”, Lasso provides a shortcut syntax for accessing members within “self” or inherited members. Using a period (.) before the member name will target the current “self”. Using two periods (..) before the member name will target inherited members, skipping the current “self” and searching for the member starting from the parent of the type that defined the currently executing member method. Two periods can only be used for methods, as only “self” can access data members.

.'dMem'
// => // Produces the value stored in the "dMem" data member (same as self->'dMem')

.meth(1, 2)
// => // Produces the value of calling self->meth(1, 2)

..meth(3, 4)
// => // Produces the value of calling inherited->meth(3, 4)

Retarget Operation

The retarget operation allows the same target object to be used for multiple method calls. The retarget operator (&) is placed between the individual method calls. Retarget is only ever used in the context of a member method call using the target operator (->). The target object of the last target operator is used as the object for the retargeted member call. For each method call, the & is placed following the method’s name, parameters, and capture block (if present).

The retarget operator can string two or more methods together. The return value of the final method will be produced by this type of retarget.

object->meth & meth2
// => // Execute meth on the object then execute meth2 and produce its value

object->meth(1, 2) & meth2()
// => // Execute meth on the object then execute meth2 and produce its value

Retarget can also be used to change the produced value of a member method call to be that of the target object. This is done by having a trailing & at the end of a method call.

targetObject->meth(1, 2) &;
// => // Execute meth, but produce targetObject

Formatting Retarget

When stringing several method calls together, formatting over multiple lines can help with readability. It is important, however, to keep the & on the same line as the next method call, and to follow any trailing retarget operators with a semicolon to ensure the expression is ended. This holds only for cases that have a next method and for method call expressions that are not ultimately parenthesized.

The following example illustrates this formatting principle:

targetObject->meth(5, 7)
& meth2()
& meth3(90) &;
// => // Execute meth, meth2, meth3, and then produce targetObject

Method Escaping

To escape a method is to allow a method to be searched for by name and returned to the caller. The caller can later use that method, executing it by applying parentheses as described in the section on invocation. This makes it easy for methods to be treated as regular values and to be used as callbacks. It is an error if the method that is being escaped is not defined.

Both member methods and unbound methods can be escaped. There are two escape method operators, one for member methods and one for unbound methods. Escaping a member method uses the binary escape operator (->\), while escaping an unbound method uses the unary escape operator (\).

#lv->\meth
// => // Produces a reference to the member method "meth" of the object in local "lv"

\meth
// => // Produces a reference to the unbound method "meth"

When a member method is escaped, the resulting value is bound to that target object. This ensures that when the resulting value/method is invoked, that the current “self” will be the object from which the method was escaped. Additionally, if there is more than one method defined under the given name, all of the methods are retrieved. This permits multiple dispatch to be used with an escaped method.

The right-hand method name operand can come from the result of any expression. When using such a dynamic method name, the expression must be surrounded in parentheses to disambiguate.

#lv->\(meth + 'name')
// => // Produces a reference to the member method whose name matches the
//       result of appending "name" to the value returned by "meth"

Although the escape operators are used to find methods by name, the object produced by the operators is a memberstream. This object manages the finding of the desired method, the potential bundling of the target object (in the case of ->\), and the execution of the method when the memberstream is invoked.

Additional Syntax

There are several other operator-like syntax elements that will be described in detail in later sections of this document. Many of them apply in limited situations or special contexts and so are beyond the scope of this chapter.

See also