1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308
|
Advanced features
=================
This section describes advanced features of SurgeScript.
Lookup operator
---------------
Some programming languages, such as C++, have a feature called *operator overloading*. It's a *syntactic sugar* that allows the programmer to attribute custom implementations to different operators.
In SurgeScript, the `[]` operator (also called the *lookup operator*), used by [Arrays](/reference/array) and [Dictionaries](/reference/dictionary), is used to **get** and **set** values from/to the data structure. In fact, the `[]` operator can be used with any object. It is necessary to define, in your object, functions `get()` and `set()` with the following signature:
```
fun get(key)
{
// custom implementation
}
fun set(key, value)
{
// custom implementation
}
```
Given an object `obj`, the expression `x = obj[key]` is equivalent to `x = obj.get(key)`. Similarly, `obj[key] = value` is equivalent to `obj.set(key, value)`.
Function objects
----------------
In SurgeScript, objects can be made to behave like functions. We call these objects *function objects* (or *functors*). To make an object behave like a function, you have to overload the `()` operator (also known as the *function operator*). This is done by defining function `call()` in your object:
```
fun call()
{
// custom implementation
}
```
Function `call()` may take any number of parameters. Given an object `f`, the expression `y = f(x)` is equivalent to `y = f.call(x)`. Notice that, since `f` is an object, you may exchange its implementation during runtime.
Assertions
----------
The `assert(condition)` statement specifies a `condition` that you expect to be true at a certain point in your code. If that condition turns out to be false, the code will be interrupted with an *assertion failed* error. Example:
```
assert(name == "Surge"); // will crash if name isn't "Surge"
```
Chaining
--------
In SurgeScript, it's possible to configure objects in an elegant way using a technique called *chaining*. Consider the object below - it simply displays a message at regular intervals:
```
object "Parrot"
{
message = "I am a Parrot";
state "main"
{
if(timeout(1.0))
state = "print";
}
state "print"
{
Console.print(message);
state = "main";
}
// Note that this function returns
// this, i.e., the object itself.
fun setMessage(newMessage)
{
message = newMessage;
return this;
}
}
```
Suppose that, in your Application, you would like to spawn that object and modify its message. One way of doing it would be making its internal variable `public` and changing its contents in the [constructor function](/tutorials/functions) of your Application. A more concise and elegant way of doing it would be calling function `setMessage()` just after you spawn the object:
```
object "Application"
{
parrot = spawn("Parrot").setMessage("Hello!");
state "main"
{
}
}
```
Observe that the function we have defined does two things:
* It modifies the internals of the object in some way
* It always returns `this` (that is, the object itself)
We call that function a *chainable function*. You may call such a function from your Application, just after `spawn()`, and you'll still have a reference to the spawned object. Moreover, since chainable functions always return `this`, you may chain multiple function calls into a single statement, making your code concise and your statement descriptive. Example:
```
parrot = spawn("Parrot").setMessage("Hello!").setInterval(2.0);
```
Factory
-------
In SurgeScript, a factory is a functor that spawns an object for you. The object can be spawned and configured in a single call. In the example below, factory `Greeter` spawns and configures `Greeting` objects. We annotate the factory with `@Package`, so it can be imported anywhere in the code.
To the end-user, calling `Greeter()` is simpler than manually spawning and configuring a `Greeting` every time it is needed.
```
// Factory example
using Greeter; // import the factory
object "Application"
{
state "main"
{
// This will print:
// Hello, alex!
g = Greeter("alex");
g.greet();
exit();
}
}
// File: greeting.ss
object "Greeting"
{
public name = "anon";
fun greet()
{
Console.print("Hello, " + name + "!");
}
}
@Package
object "Greeter"
{
// Greeter is a factory. It spawns and configures
// a Greeting object for you. As it is a package,
// it can be imported and used anywhere.
fun call(name)
{
g = spawn("Greeting");
g.name = name;
return g;
}
}
```
In the example above, objects spawned by the factory will be children of the factory. If you need the parent of the spawned object to be the caller, simply write `g = caller.spawn("Greeter")`. Know that `caller` points to the object that called the function (or `null` if not applicable).
Iterators
---------
As seen in the [loops](/tutorials/loops#foreach) section, the foreach loop may be used to iterate through an iterable collection. In SurgeScript, an iterable collection is an object that implements the iterator protocol described below.
You may implement your own iterable collections by tagging them as `"iterable"` and implementing function `iterator()`. If you have ever used Java, you'll find this to be familiar.
```
// Iterable collections are tagged "iterable"
// and implement function iterator()
object "MyCollection" is "iterable"
{
fun iterator()
{
// function iterator() takes no arguments and
// returns a new iterator object
}
}
```
For each iterable collection you define, you must define its iterator object. The iterator object must be tagged `"iterator"` and implement functions `next()` and `hasNext()` (both take no arguments):
```
// Iterators are tagged "iterator" and
// implement functions next() and hasNext()
object "MyIterator" is "iterator"
{
fun next()
{
// returns the next element of the collection
// and advances the iteration pointer
// the iterable collection is usually the parent
// object, i.e., collection = parent
}
function hasNext()
{
// returns true if the enumeration isn't over
// returns false if there are no more elements
}
}
```
You may iterate over an iterable collection using the following code:
```
it = collection.iterator();
while(it.hasNext()) {
x = it.next();
// do something with x
// x is an element of the collection
Console.print(x);
}
```
Or, alternatively, using the compact foreach:
```
foreach(x in collection) {
Console.print(x);
}
```
For the sake of completion, the following code demonstrates how to implement a custom iterable collection that hold even numbers from 0 up to 20 without having to store them explicitly in memory:
```
object "Application"
{
evenNumbers = spawn("Even Numbers").upTo(20);
state "main"
{
// print all the numbers of the iterable collection
foreach(number in evenNumbers)
Console.print(number);
// we're done!
exit();
}
}
object "Even Numbers" is "iterable"
{
lastNumber = 0;
fun iterator()
{
return spawn("Even Numbers Iterator").upTo(lastNumber);
}
fun upTo(num)
{
// upTo() is a chainable function that
// is NOT part of the iterator protocol
// (but it's useful for this example)
lastNumber = Number(num);
return this;
}
}
object "Even Numbers Iterator" is "iterator"
{
nextNumber = 0;
lastNumber = 0;
fun next()
{
currentNumber = nextNumber;
nextNumber += 2;
return currentNumber;
}
fun hasNext()
{
return nextNumber <= lastNumber;
}
fun upTo(num)
{
// upTo() is a chainable function that
// is NOT part of the iterator protocol
// (but it's useful for this example)
lastNumber = Number(num);
return this;
}
}
```
The output of this code is:
```
0
2
4
6
8
10
12
14
16
18
20
```
**CHALLENGE:** can you write an iterable collection called *Fibonacci Sequence* containing the first *N* [Fibonacci numbers](https://en.wikipedia.org/wiki/Fibonacci_number) without storing them all explicitly in memory? It should be used as follows:
```
// Desired output (for N=10): 0 1 1 2 3 5 8 13 21 34
sequence = spawn("Fibonacci Sequence").ofLength(10);
foreach(number in sequence)
Console.print(number);
```
|