Operators
Tomo supports a number of operators, both infix and prefix:
+,-,*,/: addition, subtraction, multiplication, and division for integers and floating point numbers^: exponentiation for integers and floating point numbersmod: modulus for integers and floating point numbersmod1: clock-style modulus, which is equivalent to1 + ((x-1) mod y). This is particularly useful for doing wraparound behavior on 1-indexed lists.++: concatenation (for text and lists)<<,>>: bitwise left shift and right shift for integers<<<,>>>: unsigned bitwise left shift and right shift for integers_min_/_max_: minimum and maximum (see below)<,<=,>,>=,==,!=: comparisons<>: the signed comparison operator (see below)and,or,xor: logical operations forBools and bitwise operations for integers
Signed Comparison Operator
When performing comparisons, Tomo will internally use APIs that take two
objects and return a signed 32-bit integer representing the relationship
between the two objects. The <> operator exposes this signed comparison
value to the user.
assert 0 <> 99 == -1[32]
assert 5 <> 5 == 0[32]
assert 99 <> 0 == 1[32]
It's particularly handy for using the list sort() method, which takes a
function that returns a signed integer:
foos.sort(func(a,b:&Foo): a.length <> b.length)
Reducers
A novel feature introduced by Tomo is "reducer" operators, which function
similar to the reduce function in functional programming, but implemented
as a core language feature that runs as an inline loop. Reducers eliminate
the need for several polymorphic functions used in other languages like
sum, any, all, reduce, and fold. Here are some simple examples of
reducers in action:
nums := [10, 20, 30]
assert (+: nums)! == 60
assert (or: n > 15 for n in nums)! == yes
texts := ["one", "two", "three"]
assert (++: texts)! == "onetwothree"
The simplest form of a reducer is an infix operator surrounded by parentheses,
followed by either something that can be iterated over or a comprehension.
Results are produced by taking the first item in the iteration and repeatedly
applying the infix operator on subsequent items in the iteration to get a
result. Specifically for and and or, the operations are short-circuiting,
so the iteration will terminate early when possible.
Handling Empty Iterables
In the case of an empty iteration cycle, there are two options available. The first option is to not account for it, in which case you'll get a runtime error if you use a reducer on something that has no values:
nums : [Int] = []
result := (+: nums)!
Error: this collection was empty!
If you want to handle this case, you can either wrap it in a conditional
statement or you can provide a fallback option with else like this:
assert (+: nums) or 0 == 0
The else clause must be a value of the same type that would be returned.
Extra Features
As syntactic sugar, reducers can also access fields, indices, or method calls. This simplifies things if you want to do a reduction without writing a full comprehension:
struct Foo(x,y:Int)
func is_even(f:Foo -> Bool)
return (f.x + f.y) mod 2 == 0
foos := [Foo(1, 2), Foo(-10, 20)]
assert (+.x: foos) == -9
// Shorthand for:
assert (+: f.x for f in foos) == -9
assert (or).is_even() foos == yes
// Shorthand for:
assert ((or) f.is_even() for f in foos) == yes
assert (+.x.abs(): foos) == 11
_min_ and _max_
Tomo introduces a new pair of operators that may be unfamiliar: _min_ and
_max_. The _min_ and _max_ operators are infix operators that return the
larger or smaller of two elements:
assert 3 _max_ 5 == 5
assert "XYZ" _min_ "ABC" == "ABC"
Initially, this might seem like a fairly useless operator, but there are two tricks that make these operators extremely versatile.
Keyed Comparisons
The first trick is that the comparison operation supports keyed comparisons that operate on a field, index, method call, or function call. This lets you choose the larger or smaller of two elements according to any standard. Here's some examples:
// Get the largest absolute value number:
assert 3 _max_.abs() -15 == -15
struct Person(name:Text, age:Int)
// Get the oldest of two people:
assert Person("Alice", 33) _max_.age Person("Bob", 20) == Person(name="Alice", age=33)
// Get the longest of two lists:
assert [10, 20, 30, 40] _max_.length [99, 1] == [10, 20, 30, 40]
// Get the list with the highest value in the last position:
assert [10, 20, 999] _max_[-1] [99, 1] == [10, 20, 999]
The keyed comparison can chain together multiple field accesses, list index
operations, method calls, etc. If you wanted to, for example, get the item
whose x field has the highest absolute value, you could use _max_.x.abs().
Working with Reducers
The second trick is that the _min_ and _max_ operators work with reducers.
This means that you get get the minimum or maximum element from an iterable
object using them:
nums := [10, -20, 30, -40]
assert (_max_: nums) == 30
assert (_max_.abs(): nums) == -40
Operator Overloading
Operator overloading is supported, but strongly discouraged. Operator overloading should only be used for types that represent mathematical concepts that users can be reliably expected to understand how they behave with math operators, and for which the implementations are extremely efficient. Operator overloading should not be used to hide expensive computations or to create domain-specific syntax to make certain operations more concise. Examples of good candidates for operator overloading would include:
- Mathematical vectors
- Matrices
- Quaternions
- Complex numbers
Bad candidates would include:
- Arbitrarily sized datastructures
- Objects that represent regular expressions
- Objects that represent filesystem paths
Available Operator Overloads
When performing a math operation between any two types that are not both numerical or boolean, the compiler will look up the appropriate method and insert a function call to that method. Math overload operations are all assumed to return a value that is the same type as the first argument and the second argument must be either the same type as the first argument or a number, depending on the specifications of the specific operator.
If no suitable method is found with the appropriate types, a compiler error will be raised.
Addition
func plus(T, T -> T)
In an addition expression a + b between two objects of the same type, the
method a.plus(b) will be invoked, which returns a new value of the same type.
Subtraction
func minus(T, T -> T)
In a subtraction expression a - b between two objects of the same type, the
method a.minus(b) will be invoked, which returns a new value of the same type.
Multiplication
func times(T, T -> T)
func scaled_by(T, N -> T)
The multiplication expression a * b invokes either the a.times(b) method,
if a and b are the same non-numeric type, or a.scaled_by(b) if a is
non-numeric and b is numeric, or b.scaled_by(a) if b is non-numeric and
a is numeric. In all cases, a new value of the non-numeric type is returned.
Division
func divided_by(T, N -> T)
In a division expression a / b the method a.divided_by(b) will be invoked
if a has type T and b has a numeric type N.
Exponentiation
func power(T, N -> T)
In an exponentiation expression, a ^ b, if a has type T and b has a
numeric type N, then the method a.power(b) will be invoked.
Modulus
func mod(T, N -> T)
func mod1(T, N -> T)
In a modulus expression, a mod b or a mod1 b, if a has type T and b
has a numeric type N, then the method mod() or mod1() will be invoked.
Negative
func negative(T -> T)
In a unary negative expression -x, the method negative() will be invoked
and will return a value of the same type.
Bit Operations
func left_shifted(T, Int -> T)
func right_shifted(T, Int -> T)
func unsigned_left_shifted(T, Int -> T)
func unsigned_right_shifted(T, Int -> T)
func bit_and(T, T -> T)
func bit_or(T, T -> T)
func bit_xor(T, T -> T)
In a bit shifting expression, a >> b or a << b, if a has type T and b
is an Int, then the method left_shift() or right_shift() will be invoked.
A value of type T will be returned. The same is true for >>>
(unsigned_right_shift()) and <<< (unsigned_left_shift).
In a bitwise binary operation a and b, a or b, or a xor b, then the
method bit_and(), bit_or(), or bit_xor() will be invoked, assuming that
a and b have the same type, T. A value of type T will be returned.
Bitwise Negation
func negated(T -> T)
In a unary bitwise negation expression not x, the method negated() will be
invoked and will return a value of the same type.
1 # Operators3 Tomo supports a number of operators, both infix and prefix:6 integers and floating point numbers10 is particularly useful for doing wraparound behavior on 1-indexed lists.18 integers20 ## Signed Comparison Operator22 When performing comparisons, Tomo will internally use APIs that take two23 objects and return a signed 32-bit integer representing the relationship25 value to the user.28 assert 0 <> 99 == -1[32]29 assert 5 <> 5 == 0[32]30 assert 99 <> 0 == 1[32]31 ```34 function that returns a signed integer:37 foos.sort(func(a,b:&Foo): a.length <> b.length)38 ```40 ## Reducers42 A novel feature introduced by Tomo is "reducer" operators, which function44 as a core language feature that runs as an inline loop. Reducers eliminate45 the need for several polymorphic functions used in other languages like47 reducers in action:50 nums := [10, 20, 30]51 assert (+: nums)! == 6052 assert (or: n > 15 for n in nums)! == yes54 texts := ["one", "two", "three"]55 assert (++: texts)! == "onetwothree"56 ```58 The simplest form of a reducer is an infix operator surrounded by parentheses,59 followed by either something that can be iterated over or a comprehension.60 Results are produced by taking the first item in the iteration and repeatedly61 applying the infix operator on subsequent items in the iteration to get a63 so the iteration will terminate early when possible.65 ## Handling Empty Iterables67 In the case of an empty iteration cycle, there are two options available. The68 first option is to not account for it, in which case you'll get a runtime error69 if you use a reducer on something that has no values:72 nums : [Int] = []73 result := (+: nums)!75 Error: this collection was empty!76 ```78 If you want to handle this case, you can either wrap it in a conditional82 assert (+: nums) or 0 == 083 ```87 ## Extra Features89 As syntactic sugar, reducers can also access fields, indices, or method calls.90 This simplifies things if you want to do a reduction without writing a full91 comprehension:94 struct Foo(x,y:Int)95 func is_even(f:Foo -> Bool)96 return (f.x + f.y) mod 2 == 098 foos := [Foo(1, 2), Foo(-10, 20)]100 assert (+.x: foos) == -9101 // Shorthand for:102 assert (+: f.x for f in foos) == -9104 assert (or).is_even() foos == yes105 // Shorthand for:106 assert ((or) f.is_even() for f in foos) == yes108 assert (+.x.abs(): foos) == 11109 ```115 larger or smaller of two elements:118 assert 3 _max_ 5 == 5119 assert "XYZ" _min_ "ABC" == "ABC"120 ```122 Initially, this might seem like a fairly useless operator, but there are two123 tricks that make these operators extremely versatile.125 ### Keyed Comparisons127 The first trick is that the comparison operation supports keyed comparisons128 that operate on a field, index, method call, or function call. This lets you130 Here's some examples:133 // Get the largest absolute value number:134 assert 3 _max_.abs() -15 == -15136 struct Person(name:Text, age:Int)138 // Get the oldest of two people:139 assert Person("Alice", 33) _max_.age Person("Bob", 20) == Person(name="Alice", age=33)141 // Get the longest of two lists:142 assert [10, 20, 30, 40] _max_.length [99, 1] == [10, 20, 30, 40]144 // Get the list with the highest value in the last position:145 assert [10, 20, 999] _max_[-1] [99, 1] == [10, 20, 999]146 ```148 The keyed comparison can chain together multiple field accesses, list index149 operations, method calls, etc. If you wanted to, for example, get the item152 ### Working with Reducers155 This means that you get get the minimum or maximum element from an iterable156 object using them:159 nums := [10, -20, 30, -40]160 assert (_max_: nums) == 30161 assert (_max_.abs(): nums) == -40162 ```164 ## Operator Overloading167 overloading should only be used for types that represent mathematical concepts168 that users can be reliably expected to understand how they behave with math169 operators, and for which the implementations are extremely efficient. Operator170 overloading should not be used to hide expensive computations or to create171 domain-specific syntax to make certain operations more concise. Examples of172 good candidates for operator overloading would include:174 - Mathematical vectors175 - Matrices176 - Quaternions177 - Complex numbers179 Bad candidates would include:181 - Arbitrarily sized datastructures182 - Objects that represent regular expressions183 - Objects that represent filesystem paths185 ### Available Operator Overloads187 When performing a math operation between any two types that are not both188 numerical or boolean, the compiler will look up the appropriate method and189 insert a function call to that method. Math overload operations are all assumed190 to return a value that is the same type as the first argument and the second191 argument must be either the same type as the first argument or a number,192 depending on the specifications of the specific operator.194 If no suitable method is found with the appropriate types, a compiler error195 will be raised.197 #### Addition199 ```200 func plus(T, T -> T)201 ```206 #### Subtraction208 ```209 func minus(T, T -> T)210 ```215 #### Multiplication217 ```218 func times(T, T -> T)219 func scaled_by(T, N -> T)220 ```227 #### Division229 ```230 func divided_by(T, N -> T)231 ```236 #### Exponentiation238 ```239 func power(T, N -> T)240 ```245 #### Modulus247 ```248 func mod(T, N -> T)249 func mod1(T, N -> T)250 ```255 #### Negative257 ```258 func negative(T -> T)259 ```262 and will return a value of the same type.264 #### Bit Operations266 ```267 func left_shifted(T, Int -> T)268 func right_shifted(T, Int -> T)269 func unsigned_left_shifted(T, Int -> T)270 func unsigned_right_shifted(T, Int -> T)271 func bit_and(T, T -> T)272 func bit_or(T, T -> T)273 func bit_xor(T, T -> T)274 ```285 #### Bitwise Negation287 ```288 func negated(T -> T)289 ```292 invoked and will return a value of the same type.