Tutorial 9.3 (Nested functions) - Aerll/rpp GitHub Wiki

Nested functions

In r++ there is a yet another type of function called a nested function. This is the mechanism, that allows us to write long chains of function calls:

Insert(1).Chance(50).NoDefaultPosRule();

At first glance this doesn't really seem any different than 3 separate function calls, however this way, we can prevent users from misusing our interface. For example, if these functions weren't nested, we would be able to call them in the wrong order:

Chance(50);
Insert(1);
NoDefaultPosRule();

Which is extremely error-prone. We would need to somehow check the order of function calls and throw an error whenever Insert() is not called first. And while nested functions can be called in any order, we can at least ensure, that our main function is always called first and build our interface from there.

On the other hand, nested functions greatly improve readability from the user's perspective. Since everything needs to be done in a single chain of function calls, it always ends up being in one place.

Creating a nested function

There are 2 parts to a nested function. First is a normal function which specifies its nested functions:

function->type signature nested(name1, name2, name3, ...)
    ...
    return value;
end

Here the only new thing is:

  • nested(...) -> a list of nested function names belonging to this function.

Now for the second part we write:

nested function->type name.signature
    ...
    return value;
end

Which is also nothing new except for:

  • nested keyword, indicating this is a nested function
  • name - name of the function this nested function belongs to

Important thing to note here, is that returning from a nested function isn't very practical. It exists purely to enable functions like If.

Now we will build a simple interface to shift specific tiles by a given offset. First let's prepare our functions:

function->null ShiftTiles(array int indices) nested(By)
    // ...
end

nested function->null ShiftTiles.By(coord offset)
    // ...
end

Invoking nested functions

invoke(nested) is a mechanism exclusive to a function specifying nested functions. It gives us a way to execute all of the nested functions called by the user. In our case it will look like this:

function->null ShiftTiles(array int indices) nested(By)
    invoke(nested);
end

Now let's see what happens, when we call it:

function->null ShiftTiles(array int indices) nested(By)
    warning("ShiftTiles");
    invoke(nested);
    warning("ShiftTiles");
end

nested function->null ShiftTiles.By(coord offset)
    warning("By");
    // ...
end

ShiftTiles(1, 2).By([10, 0]);

This code will display ShiftTiles, By, ShiftTiles. As expected invoke(nested) is simply calling the nested function. If we for example wrote this:

ShiftTiles(1, 2);

The same code would display ShiftTiles, ShiftTiles.

Passing data between functions

Now that we have our interface set up, we need to write the code responsible for creating the rules. All of the logic will find itself inside the By function. Why is that? Simply because shifting without an offset does nothing, therefore ShiftTiles function should do exactly that.

However, if we were to write it now, we would quickly stumble upon a tiny inconvenience. How will our nested function know which tiles it should shift? We need to somehow transfer indices from the ShiftTiles function to the By function. All we need to do, is to save a copy of the indices and then be able to access them from within the By function. Does that ring any bells?

We will make use of a global variable and declare it above our interface:

array int shift:indices;

function->null ShiftTiles(array int indices) nested(By)
    invoke(nested);
end

nested function->null ShiftTiles.By(coord offset)
    // ...
end

Note: in r++, global variable names starting with g: or s: are used by base.r and therefore reserved.

This lets us create a copy of the indices, right before we invoke nested functions:

function->null ShiftTiles(array int indices) nested(By)
    shift:indices = indices;
    invoke(nested);
end

And now we can finally write our rules:

nested function->null ShiftTiles.By(coord offset)
    for (i = 0 to shift:indices.last)
        Insert(0).If(IndexAt([0, 0]).Is(shift:indices[i]));
    end
    for (i = 0 to shift:indices.last)
        Insert(shift:indices[i]).If(IndexAt(offset).Is(shift:indices[i])).NoDefaultPosRule();
    end
end

That's it, our interface is ready.

And this wraps up the tutorials, now you know everything you need to know about r++. If you want more, you can head on to the examples folder, which contains automappers for every tileset shipped with DDNet. It will teach you how to approach more complex problems.

Full code

#include "base.r"

array int shift:indices;

function->null ShiftTiles(array int indices) nested(By)
    shift:indices = indices;
    invoke(nested);
end

nested function->null ShiftTiles.By(coord offset)
    for (i = 0 to shift:indices.last)
        Insert(0).If(IndexAt([0, 0]).Is(shift:indices[i]));
    end
    for (i = 0 to shift:indices.last)
        Insert(shift:indices[i]).If(IndexAt(offset).Is(shift:indices[i])).NoDefaultPosRule();
    end
end
⚠️ **GitHub.com Fallback** ⚠️