code / tomo

Lines41.3K C23.7K Markdown9.7K YAML5.0K Tomo2.3K
7 others 763
Python231 Shell230 make212 INI47 Text21 SVG16 Lua6
(292 lines)

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 numbers
  • mod: modulus for integers and floating point numbers
  • mod1: clock-style modulus, which is equivalent to 1 + ((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 for Bools 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 # Operators
3 Tomo supports a number of operators, both infix and prefix:
5 - `+`,`-`,`*`,`/`: addition, subtraction, multiplication, and division for
6 integers and floating point numbers
7 - `^`: exponentiation for integers and floating point numbers
8 - `mod`: modulus for integers and floating point numbers
9 - `mod1`: clock-style modulus, which is equivalent to `1 + ((x-1) mod y)`. This
10 is particularly useful for doing wraparound behavior on 1-indexed lists.
11 - `++`: concatenation (for text and lists)
12 - `<<`, `>>`: bitwise left shift and right shift for integers
13 - `<<<`, `>>>`: unsigned bitwise left shift and right shift for integers
14 - `_min_`/`_max_`: minimum and maximum (see below)
15 - `<`, `<=`, `>`, `>=`, `==`, `!=`: comparisons
16 - `<>`: the signed comparison operator (see below)
17 - `and`, `or`, `xor`: logical operations for `Bool`s and bitwise operations for
18 integers
20 ## Signed Comparison Operator
22 When performing comparisons, Tomo will internally use APIs that take two
23 objects and return a signed 32-bit integer representing the relationship
24 between the two objects. The `<>` operator exposes this signed comparison
25 value to the user.
27 ```tomo
28 assert 0 <> 99 == -1[32]
29 assert 5 <> 5 == 0[32]
30 assert 99 <> 0 == 1[32]
31 ```
33 It's particularly handy for using the list `sort()` method, which takes a
34 function that returns a signed integer:
36 ```tomo
37 foos.sort(func(a,b:&Foo): a.length <> b.length)
38 ```
40 ## Reducers
42 A novel feature introduced by Tomo is "reducer" operators, which function
43 similar to the `reduce` function in functional programming, but implemented
44 as a core language feature that runs as an inline loop. Reducers eliminate
45 the need for several polymorphic functions used in other languages like
46 `sum`, `any`, `all`, `reduce`, and `fold`. Here are some simple examples of
47 reducers in action:
49 ```tomo
50 nums := [10, 20, 30]
51 assert (+: nums)! == 60
52 assert (or: n > 15 for n in nums)! == yes
54 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 repeatedly
61 applying the infix operator on subsequent items in the iteration to get a
62 result. Specifically for `and` and `or`, the operations are short-circuiting,
63 so the iteration will terminate early when possible.
65 ## Handling Empty Iterables
67 In the case of an empty iteration cycle, there are two options available. The
68 first option is to not account for it, in which case you'll get a runtime error
69 if you use a reducer on something that has no values:
71 ```tomo
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 conditional
79 statement or you can provide a fallback option with `else` like this:
81 ```tomo
82 assert (+: nums) or 0 == 0
83 ```
85 The `else` clause must be a value of the same type that would be returned.
87 ## Extra Features
89 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 full
91 comprehension:
93 ```tomo
94 struct Foo(x,y:Int)
95 func is_even(f:Foo -> Bool)
96 return (f.x + f.y) mod 2 == 0
98 foos := [Foo(1, 2), Foo(-10, 20)]
100 assert (+.x: foos) == -9
101 // Shorthand for:
102 assert (+: f.x for f in foos) == -9
104 assert (or).is_even() foos == yes
105 // Shorthand for:
106 assert ((or) f.is_even() for f in foos) == yes
108 assert (+.x.abs(): foos) == 11
109 ```
111 ## `_min_` and `_max_`
113 Tomo introduces a new pair of operators that may be unfamiliar: `_min_` and
114 `_max_`. The `_min_` and `_max_` operators are infix operators that return the
115 larger or smaller of two elements:
117 ```tomo
118 assert 3 _max_ 5 == 5
119 assert "XYZ" _min_ "ABC" == "ABC"
120 ```
122 Initially, this might seem like a fairly useless operator, but there are two
123 tricks that make these operators extremely versatile.
125 ### Keyed Comparisons
127 The first trick is that the comparison operation supports keyed comparisons
128 that operate on a field, index, method call, or function call. This lets you
129 choose the larger or smaller of two elements _according to any standard_.
130 Here's some examples:
132 ```tomo
133 // Get the largest absolute value number:
134 assert 3 _max_.abs() -15 == -15
136 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 index
149 operations, method calls, etc. If you wanted to, for example, get the item
150 whose `x` field has the highest absolute value, you could use `_max_.x.abs()`.
152 ### Working with Reducers
154 The second trick is that the `_min_` and `_max_` operators work with reducers.
155 This means that you get get the minimum or maximum element from an iterable
156 object using them:
158 ```tomo
159 nums := [10, -20, 30, -40]
160 assert (_max_: nums) == 30
161 assert (_max_.abs(): nums) == -40
162 ```
164 ## Operator Overloading
166 Operator overloading is supported, but _strongly discouraged_. Operator
167 overloading should only be used for types that represent mathematical concepts
168 that users can be reliably expected to understand how they behave with math
169 operators, and for which the implementations are extremely efficient. Operator
170 overloading should not be used to hide expensive computations or to create
171 domain-specific syntax to make certain operations more concise. Examples of
172 good candidates for operator overloading would include:
174 - Mathematical vectors
175 - Matrices
176 - Quaternions
177 - Complex numbers
179 Bad candidates would include:
181 - Arbitrarily sized datastructures
182 - Objects that represent regular expressions
183 - Objects that represent filesystem paths
185 ### Available Operator Overloads
187 When performing a math operation between any two types that are not both
188 numerical or boolean, the compiler will look up the appropriate method and
189 insert a function call to that method. Math overload operations are all assumed
190 to return a value that is the same type as the first argument and the second
191 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 error
195 will be raised.
197 #### Addition
199 ```
200 func plus(T, T -> T)
201 ```
203 In an addition expression `a + b` between two objects of the same type, the
204 method `a.plus(b)` will be invoked, which returns a new value of the same type.
206 #### Subtraction
208 ```
209 func minus(T, T -> T)
210 ```
212 In a subtraction expression `a - b` between two objects of the same type, the
213 method `a.minus(b)` will be invoked, which returns a new value of the same type.
215 #### Multiplication
217 ```
218 func times(T, T -> T)
219 func scaled_by(T, N -> T)
220 ```
222 The multiplication expression `a * b` invokes either the `a.times(b)` method,
223 if `a` and `b` are the same non-numeric type, or `a.scaled_by(b)` if `a` is
224 non-numeric and `b` is numeric, or `b.scaled_by(a)` if `b` is non-numeric and
225 `a` is numeric. In all cases, a new value of the non-numeric type is returned.
227 #### Division
229 ```
230 func divided_by(T, N -> T)
231 ```
233 In a division expression `a / b` the method `a.divided_by(b)` will be invoked
234 if `a` has type `T` and `b` has a numeric type `N`.
236 #### Exponentiation
238 ```
239 func power(T, N -> T)
240 ```
242 In an exponentiation expression, `a ^ b`, if `a` has type `T` and `b` has a
243 numeric type `N`, then the method `a.power(b)` will be invoked.
245 #### Modulus
247 ```
248 func mod(T, N -> T)
249 func mod1(T, N -> T)
250 ```
252 In a modulus expression, `a mod b` or `a mod1 b`, if `a` has type `T` and `b`
253 has a numeric type `N`, then the method `mod()` or `mod1()` will be invoked.
255 #### Negative
257 ```
258 func negative(T -> T)
259 ```
261 In a unary negative expression `-x`, the method `negative()` will be invoked
262 and will return a value of the same type.
264 #### Bit Operations
266 ```
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 ```
276 In a bit shifting expression, `a >> b` or `a << b`, if `a` has type `T` and `b`
277 is an `Int`, then the method `left_shift()` or `right_shift()` will be invoked.
278 A value of type `T` will be returned. The same is true for `>>>`
279 (`unsigned_right_shift()`) and `<<<` (`unsigned_left_shift`).
281 In a bitwise binary operation `a and b`, `a or b`, or `a xor b`, then the
282 method `bit_and()`, `bit_or()`, or `bit_xor()` will be invoked, assuming that
283 `a` and `b` have the same type, `T`. A value of type `T` will be returned.
285 #### Bitwise Negation
287 ```
288 func negated(T -> T)
289 ```
291 In a unary bitwise negation expression `not x`, the method `negated()` will be
292 invoked and will return a value of the same type.