Tutorial 9.1 (Functions)
Functions are nothing more than reusable blocks of code. We can essentially wrap any logic in a function and run it from elsewhere. It's a useful tool, which makes the code less repetitive and much easier to read.
Unlike everything we wrote up until now, functions are not executed by the program and instead we need to call them. In a way it's similar to variables, where we first introduce a name, and then this name refers to some value. The difference is that functions aren't values, therefore we need to use a slightly more sophisticated syntax.
In r++ all functions have a signature, which is used to identify them. It's basically the full name of a function, which includes the name itself and a list of parameter types. Here are some examples:
Function(int i)
Function(coord c)
Function(int x, int y)
Function(int x, int y, int z)
AnotherFunction()
YetAnotherOne(range r, coord c)
As you may have noticed, there are 4 signatures with the same name, yet they would be perfectly valid to use. If we write down the full name for each of these, we get the following:
name: Function -> types: int
name: Function -> types: coord
name: Function -> types: int int
name: Function -> types: int int int
We can see that the types are different, therefore all signatures are different.
But it doesn't end there, signature is also unique to the kind of function we're using. We can use the same signature for a function and a nested function. Here's a minimal example (which you don't have to understand) showcasing it:
#include "base.r"
AutoMapper("");
function->null test() nested(test)
warning("test()");
invoke(nested);
end
nested function->null test.test()
warning("test.test()");
end
test().test();
Even though both functions have the same signature test()
, the program has no problem with distinguishing them and displays 2 warnings test()
and test.test()
.
To create a function we write:
function->type signature
...
return value;
end
Let's break it down:
- every function starts with a
function
keyword and a return type, followed by asignature
, and ends with theend
keyword -
type
- type of the value returned by the function,null
indicates that nothing is returned -
signature
- consists of functions's name and a list of parameters -
return value;
- stops the execution of the function and returnsvalue
, in a non-returning functionreturn
is not required, but can still be used withoutvalue
-
...
- this is where we write our usual r++ code, it will be executed when the function is called
return
is simply a way to pass a value back to where from the function was called. In other words, a function call becomes that value. Here's a simple example of a function returning a sum of 2 values:
function->int Sum(int a, int b)
return a + b;
end
Now when we call this function with values 5
and 10
, it will give us the result of 5 + 10
.
But wait there's more, if we combine it with if
, we can return different results. We could for example create a simple calculator:
function->int Calculate(int a, int b, int op)
if (op == 1)
return a + b;
end
if (op == 2)
return a - b;
end
if (op == 3)
return a * b;
end
if (op == 4)
return a / b;
end
error("Invalid operation");
end
When we call this function, we can specify the op
and the function will return the result of that operation.
Note: Execution of all functions (except for
function->null
) must always end with areturn
. If there's a possibility for the function to not return anything, and it's not afunction->null
, you will get an error. In the function above, it is valid to omit thereturn
at the end, sinceerror()
will immediately halt the program.
Another important thing to note is that we can actually return from a non-returning function:
function->null DoSomething(int tile)
if (tile == 0)
return;
end
Replace(tile, 0);
end
Calling a function is pretty simple and we already did it several times. To call the above function Sum
, we write the following:
Sum(5, 10);
Since Sum
returns a value, we can use it in the exact same contexts as variables. For example we could assign the result of Sum
to a variable:
int sum = Sum(5, 10); // sum = 15
Overloading is essentially having 2 or more functions with the same name, but with different parameters. For example we could have a function, which creates a run, that clears all tiles:
function->null Sweep()
NewRun();
OverrideLayer();
Insert(0);
end
However we could also provide an additional overload, which clears a specific tile:
function->null Sweep(int tile)
NewRun();
OverrideLayer();
Replace(tile, 0);
end
Now we can call both overloads as follows:
Sweep(); // calls 'function->null Sweep()'
Sweep(1.N); // calls 'function->null Sweep(int tile)'
Depending on the values passed, different function gets executed.
Important thing to note here, is that the values don't necessarily need to be of the same type. As long as the program is able to convert them, we're fine. For example we could pass a coord
to our function:
Sweep([1, 0].N);
This can however lead to some unexpected results in cases where the program has no way of knowing which function we mean to call. Suppose we have 2 functions:
function->null SweepAt(int tile, coord at)
NewRun();
OverrideLayer();
Insert(0).If(IndexAt(at).Is(tile));
end
function->null SweepAt(coord at, int tile)
NewRun();
OverrideLayer();
Insert(0).If(IndexAt(at).Is(tile));
end
This may seem nice, the user can pass his values in any order and the code will just work:
SweepAt(1, [0, 0]); // calls 'function->null SweepAt(int tile, coord at)'
SweepAt([0, 0], 1); // calls 'function->null SweepAt(coord at, int tile)'
Great. Now let's imagine, that we have some object and we want to remove its anchor from the map using our SweepAt
function:
object o = Indices(1);
SweepAt(o.anchor, [0, 0]); // calls `function->null SweepAt(coord at, int tile)`
And here we have an oopsie. We clearly meant to call function->null SweepAt(int tile, coord at)
, yet we ended up calling the other overload.
Why is that? This is due to the types of values we passed. You may think that the anchor
is an int
, however surprise, it's a coord
. The program is looking for the best match for each of the values and prioritizes those signatures. Since our first value is a coord
, the better match here is obviously a coord
, so now function->null SweepAt(coord at, int tile)
is considered a better candidate. The second value can be converted to an int
, therefore this signature wins the contest.
In this example we will be writing something similar to the grass_main automapper from tutorial 4.2. This time however, we won't be doing it for any specific tileset.
Our goal is to create a function, which will automatically generate a run for borders. What we need is 13 values: a filler, 4 walls, 4 outer corners and 4 inner corners. Let's write them down as parameters:
function->null Border(
int fill,
int wall:top, int wall:right, int wall:bottom, int wall:left,
int outer:topLeft, int outer:topRight, int outer:bottomRight, int outer:bottomLeft,
int inner:topLeft, int inner:topRight, int inner:bottomRight, int inner:bottomLeft
)
end
Now that our function is all set and ready, we can write our run. We're essentially gonna borrow a part of the code from the GrAss
example. But before we do that, we should think about what could go wrong here. As we know, all of the values come from the outside, therefore we should make sure they're all valid. If any of the values are not in range [0-255]
, we throw an error:
if (s:debug)
array int values = util:ArrayInt(
fill, wall:top, wall:right, wall:bottom, wall:left,
outer:topLeft, outer:topRight, outer:bottomRight, outer:bottomLeft,
inner:topLeft, inner:topRight, inner:bottomRight, inner:bottomLeft
);
for (i = 0 to values.last)
if (values[i] < 0 or values[i] > 255)
error("Values must be in range [0-255].");
end
end
end
This will prevent the user from passing incorrect values, while also informing him about the mistake.
Note:
s:debug
is a flag used in base.r, which enables error checking. It is important to include it, since it allows the user to turn it off and speed up the execution.
Note: This is purely informational, in reality we don't have to validate anything, as long as we're using functions provided in base.r to create rules. They validate every value anyway.
Now that the values are validated, we can write all of the rules:
// fill everything
Insert(fill);
// insert walls
Insert(wall:top).If(IndexAt([0, 0]).IsWall(top));
Insert(wall:right).If(IndexAt([0, 0]).IsWall(right));
Insert(wall:bottom).If(IndexAt([0, 0]).IsWall(bottom));
Insert(wall:left).If(IndexAt([0, 0]).IsWall(left));
// insert outer corners
Insert(outer:topLeft).If(IndexAt([0, 0]).IsOuterCorner(topLeft));
Insert(outer:topRight).If(IndexAt([0, 0]).IsOuterCorner(topRight));
Insert(outer:bottomRight).If(IndexAt([0, 0]).IsOuterCorner(bottomRight));
Insert(outer:bottomLeft).If(IndexAt([0, 0]).IsOuterCorner(bottomLeft));
// insert inner corners
Insert(inner:topLeft).If(IndexAt([0, 0]).IsInnerCorner(topLeft));
Insert(inner:topRight).If(IndexAt([0, 0]).IsInnerCorner(topRight));
Insert(inner:bottomRight).If(IndexAt([0, 0]).IsInnerCorner(bottomRight));
Insert(inner:bottomLeft).If(IndexAt([0, 0]).IsInnerCorner(bottomLeft));
And this pretty much wraps it up. We can put our function in a separate file and it's ready to use. Now we can apply it to different tilesets.
// grass_main.r++
#include "base.r"
#include "border.r"
AutoMapper("Border");
Border(1, 16, 17, 18, 19, 32, 33, 34, 35, 48, 49, 50, 51);
// winter_main.r++
#include "base.r"
#include "border.r"
AutoMapper("Border");
Border(1, 19, 2.V, 98, 2, 16, 20, 100, 96, 4, 3, 7, 8);
function->null Border(
int fill,
int wall:top, int wall:right, int wall:bottom, int wall:left,
int outer:topLeft, int outer:topRight, int outer:bottomRight, int outer:bottomLeft,
int inner:topLeft, int inner:topRight, int inner:bottomRight, int inner:bottomLeft
)
if (s:debug)
array int values = util:ArrayInt(
fill, wall:top, wall:right, wall:bottom, wall:left,
outer:topLeft, outer:topRight, outer:bottomRight, outer:bottomLeft,
inner:topLeft, inner:topRight, inner:bottomRight, inner:bottomLeft
);
for (i = 0 to values.last)
if (values[i] < 0 or values[i] > 255)
error("Values must be in range [0-255].");
end
end
end
NewRun();
// fill everything
Insert(fill);
// insert walls
Insert(wall:top).If(IndexAt([0, 0]).IsWall(top));
Insert(wall:right).If(IndexAt([0, 0]).IsWall(right));
Insert(wall:bottom).If(IndexAt([0, 0]).IsWall(bottom));
Insert(wall:left).If(IndexAt([0, 0]).IsWall(left));
// insert outer corners
Insert(outer:topLeft).If(IndexAt([0, 0]).IsOuterCorner(topLeft));
Insert(outer:topRight).If(IndexAt([0, 0]).IsOuterCorner(topRight));
Insert(outer:bottomRight).If(IndexAt([0, 0]).IsOuterCorner(bottomRight));
Insert(outer:bottomLeft).If(IndexAt([0, 0]).IsOuterCorner(bottomLeft));
// insert inner corners
Insert(inner:topLeft).If(IndexAt([0, 0]).IsInnerCorner(topLeft));
Insert(inner:topRight).If(IndexAt([0, 0]).IsInnerCorner(topRight));
Insert(inner:bottomRight).If(IndexAt([0, 0]).IsInnerCorner(bottomRight));
Insert(inner:bottomLeft).If(IndexAt([0, 0]).IsInnerCorner(bottomLeft));
// safety measure in case the user doesn't create a new run after calling this function
NewRun();
end