The Alusus language follows the principle of open levels: it provides low-level features, then uses those features to build higher-level ones. Among the low-level features Alusus provides is the injection feature, which is the fundamental mechanism through which object-oriented programming features are built. Rather than natively supporting the various object-oriented features such as inheritance and polymorphism, Alusus provides the injection feature and leaves it to the programmer or to libraries to use this feature to build inheritance, polymorphism, and other such features. So what does this feature actually do?
In short, the injection feature allows the user to define a variable within a given scope, then make that variable's members available in the enclosing scope. In other words, if we have a variable v and define it with the injection property within scope s , then the members belonging to v are injected directly into scope s , such that you can access those elements through s directly without needing to go through v . So if v has a member called m , and v is defined with the injection property within s , we can access m by writing s.m instead of s.v.m .
In fact, if you look at how features such as inheritance work in object-oriented languages, you'll find that this feature is the essential building block behind them. Inheritance is really nothing more than composition, combined with making the elements of the base class directly available within the derived class. The same applies to features such as the virtual function table (vtable), which is nothing more than a class whose elements are made available within the scope of the class that depends on it.
This example shows how the injection feature can be used to define a class that inherits from another class.
class Base {
def i: Int;
}
class Child {
@injection def base: Base;
def j: Int;
}
function test {
def c: Child;
c.i = 5;
c.j = 6;
Console.print("%d, %d\n", c.i, c.j);
}
test();
Output of this example:
5, 6
We notice in this example that the class Child defines within itself a variable of type Base and adds the @injection modifier to it. This modifier is what makes the elements of base available within the scope of Child , which is why in the test function we can write c.i , which the compiler automatically translates into c.base.i .
The following example demonstrates using the polymorphism pattern with inheritance. It relies on defining a function in the base class in the form of a pointer (a function pointer), then in the derived class overriding the body of that function with a body specific to the derived class. When the function is called through a pointer to the base class, the derived class's body is executed instead of the base class's body.
class Base {
def i: Int;
handler this.getVal(): Int as_ptr {
return this.i * 2;
}
}
class Child {
@injection def base: Base;
handler (this:Base).getVal(): Int set_ptr {
return this.i * 3;
}
}
function test {
def c: SrdRef[Base] = SrdRef[Child].construct();
c.i = 5;
Console.print("%d\n", c.getVal());
}
test();
Output of the example:
15
This example is similar to the previous one, except that here the handler is defined in the base class and marked as_ptr , then redefined in the derived class and marked set_ptr . This latter marking makes the compiler modify the existing pointer's value rather than defining a new pointer. In this case, the original pointer is defined in the base class rather than in the derived class, but since base is marked with the @injection modifier, that pointer becomes available within the scope of the derived class, and so we are able to modify that pointer's value. Notice also that the definition in the derived class specifies a type for this , namely the base class's type, because the original definition we are seeking to modify is defined in the base class, and therefore it carries that type in the this argument.
In the previous example we saw an implementation of polymorphism, but it did not use unified virtual function tables (vtables) — that is, creating a single table of function pointers per class, with the object holding a pointer to that table, instead of placing the function pointers within the object itself. This saves memory, because objects of the same class share the same function pointers, and so it is enough to share the table between them instead of each object keeping its own copy of that table. This example demonstrates using the injection feature to create a function table shared among the objects of a class, and also shows how to derive a new class and create a table specific to it.
class Base {
class VTable {
handler (this:Base).getV1(): Int as_ptr;
}
@shared def _vtable: VTable(VTable().{
handler (this:Base).getV1(): Int set_ptr {
return this.i;
}
});
@no_bind @injection def vtable: ref[VTable](_vtable);
def i: Int;
}
function testA {
def b: Base;
b.i = 5;
Console.print("%d\n", b.getV1());
}
class Child {
class VTable {
handler (this:Child).getV2(): Int as_ptr;
}
@shared def _baseVtable: Base.VTable(Base.VTable().{
handler (this:Base).getV1(): Int set_ptr {
return this.j;
}
});
@shared def _vtable: VTable(VTable().{
handler (this:Child).getV2(): Int set_ptr {
return this.i + this.j;
}
});
@injection def base: Base;
base.vtable~no_deref = _baseVtable;
@no_bind @injection def vtable: ref[VTable](_vtable);
def j: Int;
}
function testB {
def c: Child;
c.i = 5;
c.j = 3;
Console.print("%d\n", c.getV1());
}
function printBase(base: ref[Base]) {
Console.print("Base: %d\n", base.getV1());
}
function testC {
def b: Base;
def c: Child;
b.i = 5;
c.i = 6;
c.j = 7;
printBase(b);
printBase(c);
}
testA();
testB();
testC();
Output of the example:
5 3 Base: 5 Base: 7
Let's first look at the Base class. Notice first that it defines, inside itself, another class called VTable . This is the class that will hold the function pointers (virtual functions); in this example it contains a single function called getV1 , though we could add more. Also notice that we defined the function as a pointer without giving it a body, because the body will be specified in the Base class rather than in the VTable class. Also notice that we specified the type of this to be the base class's type, not the vtable class's type. Outside the VTable class we defined a variable of that type called _vtable , and made it shared via the @shared modifier so that it becomes a single global variable not tied to one object, and we initialized this object with a body for the getV1 function. Then, within the object itself, we defined vtable to be a pointer to the shared object _vtable , so that every new object of this class carries a pointer to the same table. By marking this pointer with @injection , the functions within this table become available within the scope of the object itself, and so we're able to call b.getV1() , which the compiler translates into b.vtable.getV1() . Also notice that we marked the vtable definition with the @no_bind modifier, which is necessary to make the compiler pass the object's own pointer as the this value for the functions, instead of using the table's own pointer. Let me clarify this further: when the compiler calls a member function, it passes it a pointer to the object — that is, the getV1 function actually receives a single argument (the this value), and behind the scenes the compiler passes this pointer, so the user only needs to write b.getV1() instead of b.getV1(b) . However, when we use the injection feature and the compiler automatically translates the call from b.getV1 into b.vtable.getV1 , does it pass the function a pointer to b or to b.vtable ? By default it passes b.vtable , since it is calling a function within vtable . But once we add the @no_bind modifier to the vtable definition, the compiler refrains from capturing the table's pointer as the this value, and instead retains its current value, which in this case will be the pointer to b .
One might say this is a complicated way to build function tables compared to the simplicity offered by other languages such as C++, but keep in mind that here we're talking about building the vtable feature itself, not merely using it as is the case in C++ — which certainly does the same thing behind the scenes. The difference here is that in Alusus, everything is open to the user, who has full control. One can then build a simple library that carries out these steps on the user's behalf, making the feature as easy to use as it is in other languages, while also allowing the creation of other patterns not available in mainstream languages, according to the user's needs.
The next step is to create a derived class that extends Base and overrides the body of the function defined in the table. In the Child class we do what we did in the Base class — define a table, then create an object of it and add a pointer to it within the objects — but we additionally define a new copy of the Base class's table to specify new bodies for Base 's functions, then we swap the table pointer within the base object so that it points to the new copy of the function table.
The following example demonstrates creating an interface and then implementing that interface in a class, again relying on the injection feature, and using two different approaches:
class Interface {
handler this.getVal(): Int as_ptr;
}
class MyType {
def i: Int;
class InterfaceImpl {
@injection def interface: Interface;
@injection def myType: ref[MyType];
handler (this:Interface).getVal(): Int set_ptr {
return this.i;
}
}
@injection def interface: InterfaceImpl;
interface.myType~no_deref = this;
handler this~cast[ref[Interface]] {
return this.interface;
}
}
class MyType2 {
def i: Int;
@injection def interface: Interface;
handler (this:Interface).getVal(): Int set_ptr {
alignThis[interface];
return this.i;
}
handler this~cast[ref[Interface]] {
return this.interface;
}
}
function printInterface(interface: ref[Interface]) {
Console.print("%d\n", interface.getVal());
}
function test {
def myType: MyType;
def myType2: MyType2;
myType.i = 42;
myType2.i = 43;
Console.print("%d\n", myType.getVal());
Console.print("%d\n", myType2.getVal());
printInterface(myType);
printInterface(myType2);
}
test();
Output of the example:
42 43 42 43
This example defines the class Interface to serve as an interface that can be implemented by different classes. This class contains a single function (in the form of a function pointer) called getVal , and the interface does not provide a body for this function. The interface is then implemented in three different classes, in different ways.
In the first class, MyType , we define an inner class named InterfaceImpl . This inner class derives from the interface class and then defines a body for the getVal function. Notice here that this function's body is able to access the elements of the outer class (MyType ) directly through this , as if we had defined the function directly inside the outer class rather than within an inner class. This is achieved through the myType pointer, defined as a member variable in the InterfaceImpl class using the injection feature. In the MyType class we define an object of InterfaceImpl and set that pointer's value. This approach is similar to inner classes in Java, where an inner class can automatically access the elements of its outer class. Also notice that the injection feature was also used when defining the interface object within MyType , and so we're able to access the interface's functions directly from the class, as we see in the call myType.getVal() in the test function.
In the second class, all we do is derive the class from the interface — that is, define it as a variable with the injection modifier — then specify the body of its function. However, notice here that the body of the getVal function begins with the statement alignThis[interface] . This statement shifts the this value, which would otherwise point to the interface object within MyType2 , so that it instead points to the beginning of the class, aligning it so that we can access the elements of MyType2 . We didn't need to do this in the first approach because there we were reaching the containing class through a variable inside the inner class, rather than directly.
We see from this article that the Alusus language has given us a single feature that lets us achieve various object-oriented programming capabilities, such as derivation, polymorphism, interfaces, and inner classes like those found in Java. As mentioned earlier, these examples are more elaborate than what you'd find in other languages, because here we are not merely using object-oriented features — we are building them ourselves. In the future we plan to release libraries that provide these features in a smooth style similar to what other languages offer, so that the user gets high-level object-oriented features along with a clear understanding of what happens behind the scenes, and the ability to take control or invent new patterns whenever needed.