Functions
This article covers the language's basic philosophy on functions.
In mathematics, a function is a transformation from input values to output values. In programming, a function can either refer to a pure mathematical function that just transforms values or an algorithm that modifies the program state, or both. A function should have a name that describes what it does or the (result of the) transformation it represents.
Overloading and templates
Many languages support the overloading of function identifiers, so that different argument sets passed to the same function name may refer to different implementations. This feature arose before template programming was invented, to allow implementing the same operation for different types (such as single and double precision floating point versions of the same functions). However, this also allows that different implementations of the same function name perform wildly different tasks, which is problematic when reading code, as it is not always immediately obvious which overload of a function is referred to in a given invocation. In RL, there is no function overloading, as it already supports templated functions. This makes sure that many different argument sets can be supported, but all follow the same generic implementation. While this may be inconvenient to adapt to at first, it will simplify and speed up both the compilation of code, as well as the programmer's ability to read, navigate, and understand code. A simple implementation of a value-swapping function would look like this:
[T: TYPE]
swap(a: T&, b: T&) VOID := STATIC TRY
T::swap(a, b);
ELSE
{
tmp: T := &&b;
b := &&a;
a := &&tmp;
}
To handle different classes of input values, type reflection and polymorphism have to be used. A function should then look as follows:
Number
{
// Native "implements" function for type checking.
-> := TYPE SWITCH TYPE(THIS)
{
U1, U2, U4, U8,
S1, S2, S4, S8,
SINGLE, DOUBLE:
= TRUE;
DEFAULT:
= FALSE;
}
}
// Manually mark BigInt as Number.
BigInt -> Number { (/ ... /) }
[T: TYPE(Number)]
sqr(x: T #&) T := STATIC TRY
= x.sqr();
ELSE
= x * x;
In this example, we identify whether a type is a number in a way that handles different kinds of types separately.
Native types are handled explicitly, and class types must derive from the Number
class.
This predicate is then used at the bottom to limit the scope of sqr()
to numbers only.
By enforcing the single implementation paradigm, RL forces clean abstractions to be used and makes calling code more readable.
Although this generates some overhead when creating a new category of values, it makes the code more maintainable overall.
Abstractions can also be omitted at the cost of maintainability, which is only recommended for rapid prototyping or very small codebases.
Single implementations of functions also have the great advantage of allowing readable error messages:
sqr(FALSE)
would raise the error FALSE is not a Number
(or something along those lines), as Number
is the type constraint of the function's template argument.
In other languages, a much less clear error message would be generated.
If a complex type constraint, such as [T: TYPE(Number && Serializable)]
were used, then an error message could be FALSE is Serializable, but not a Number
.
Because there is only one possible constraint per template type, and only one possible template type per argument, errors can directly diagnose the problem.
Variants
Additionally, multiple variants of the same function can be implemented. These variants share a common name, but each variant needs to have a unique suffix:
Greeter {
Name: CHAR#\;
{name: CHAR#\}: Name(name);
greet(o: std::OStream&) VOID { o.write_all("Hi!"); }
greet formally(o: std::OStream&) VOID { o.write_all("Hello ", Name, '!'); }
}
sneed: Greeter := "Sneed";
sneed.greet(console);
sneed.greet formally(console);
sneed.greet(:formally, console);
Here, the main name of both functions is Greeter::greet
, but Greeter::greet formally
is a specialised variant.
This is particularly practical when investigating code bases, as search results would automatically group related functions together.
A variant function should do roughly the same thing as its main function, with only detail changes.
For a function that does completely different things, choose to create an unrelated function instead.
As shown in the above example, a function's variant can also be called by passing its name suffix as a symbolic constant as the first argument. This allows some more flexibility for templated code and allows a very constrained form of overload invocation as it exists in other languages, while still making overloads trivially distinguishable based on the types passed. A function's first argument type must not be a symbolic constant, unless it is a variant function already, to avoid ambiguity. If a function's first argument is a template argument, and the template type is a symbolic constant, then an existing variant of the same name will take precedence over the template function.
Overloading operators
:ok(vector)[i]
vector[:ok(i)]
++:ok(iterator)
:ok(callable)(a, b, c);
callable(:ok, a, b, c);
callable(:ok(a, b, c));
Overloading Constructors
Although functions cannot be overloaded, constructors can be. However, the rules for this are strict:
- The default, copy, and move constructors can always be implemented.
- One additional constructor is allowed to be implemented just like a normal constructor.
- All further constructors have to be of the following form, prefixed with a symbolic name:
Thus, a class would look like this:
MyClass
{
{}; // Default
{#&}; // Copy
{&&}; // Move
{args...}; // Free custom ctor.
:customCtor{args...}; // Named custom ctor.
:constantCtor{}; // No-arguments named custom ctor.
}
Custom constructors are invoked as follows:
default: MyClass;
copied: MyClass := default;
moved: MyClass := &&default;
freeCustom: MyClass(...); // or ::= <MyClass>(...).
custom: MyClass:customCtor(...);
custom := <MyClass:customCtor>(...)
custom := :customCtor(...);
constant: MyClass := :constantCtor; // parentheses optional in this case.
constant2: MyClass:constantCtor; // parentheses optional in this case.
d ::= <MyClass:constantCtor>(); // parentheses required in this case.
A constructor's overload is thus always trivially clear. The only comparison that needs to be done is whether a specific invocation fits the move or copy constructor better than the one free custom constructor that does not require a name. Thus, constructor resolution is simple and efficient, which speeds up possible compilation times dramatically, while still offering a fair bit of conciseness.