Concept


Classes are the basic building block of OOP (Object Oriented Programming). It is a new type that is defined by the user, and it contains members that we can access and use using an instance of that class (we can also call it an object).

For example, let's consider a car class. There can be many cars with different names and brands, but all of them will share some common attributes, all of them will have 4 wheels, a speed limit, mileage, and many other attributes. So here the car will be the class and its brand, wheels, speed limits, and the mileage will be the attributes. We can consider the speed and the mileage to be data members, whereas the member functions (methods) are pressing on brakes and increase the speed.

Data members are data variables defined inside the class body, and every object of that class will include instances of those varaibles. Member functions on other hand are functions defined inside the class body and used to process these variables.

Defining Classes and Declaring Objects


To define a new class we write the word "class" followed by a name followed by the class body inside curly brackets. The next example show how to define a class named "Car" which contains two variables, the speed limit and the mileage.

class Car {
  def speed_limit: int;
  def mileage: int;
}

To use the class we need to instantiate objects from it, as follows:

def audi: Car;

Here we defined an object that represents an Audi car from type `Car`. Accessing members of the object is done by writing the name of the object followed by a dot followed by the name of member. For example, the speed limit for audi object could be written to as follows:

audi.speed_limit = 190;

Example 1:
We will now create an object that represents a car then create objects from it.

import "Srl/Console.alusus"
import "Srl/String.alusus"
use Srl;
// Create a Car class with some attributes
class Car {
  def brand: String;
  def model: String;
  def year: int;
  def speed_limit: int;
  def mileage: int;
}

// Create an object of Car
def carObj1: Car; // creating an object from the class and fill its data
carObj1.brand = "BMW";
carObj1.model = "X5";
carObj1.year = 1999;
carObj1.speed_limit = 200;
carObj1.mileage = 10000;
// Create another object of Car
def carObj2:Car; // create another object
carObj2.brand = "Ford";
carObj2.model = "Mustang";
carObj2.year = 1969;
carObj2.speed_limit = 160;
carObj2.mileage = 8500;

// Print attribute values
Console.print("-----------------------\n");
Console.print("\tcarObj1\n");
Console.print("-----------------------\n");
Console.print("Brand: %s\n", carObj1.brand.buf);
Console.print("Model: %s\n", carObj1.model.buf);
Console.print("Year: %d\n", carObj1.year);
Console.print("Speed limit: %d\n", carObj1.speed_limit);
Console.print("Mileage: %d\n", carObj1.mileage);
Console.print("-----------------------\n");
Console.print("\tcarObj2\n");
Console.print("-----------------------\n");
Console.print("Brand: %s\n", carObj2.brand.buf);
Console.print("Model: %s\n", carObj2.model.buf);
Console.print("Year: %d\n", carObj2.year);
Console.print("Speed limit: %d\n", carObj2.speed_limit);
Console.print("Mileage: %d\n", carObj2.mileage);
Console.print("-----------------------\n");

/*
-----------------------
carObj1
-----------------------
Brand: BMW
Model: X5
Year: 1999
Speed limit: 200
Mileage: 10000
-----------------------
carObj2
-----------------------
Brand: Ford
Model: Mustang
Year: 1969
Speed limit: 160
Mileage: 8500
-----------------------
*/

Defining Member Functions


There are two ways to define a member function:

  1. Using the modifier `@member` followed by the keyword `function` then the name of the function followed by the arguments enclosed in parentheses, in addition to the first argument being a reference to this class so that we can access other object members. Finally we put a colon followed by the return type of the function (in case the function does not return anything we don't put anything).
    @member function func_name (this: ref[this_type], arg2, arg3, ...): ret_type {
      // Function body
    }
    
  2. Using the improved form, we write the keyword `handler` followed by the word `this` then the name of the function and its arguments and the return type.
    handler this.func_name (arg1, arg2, ...): ret_type {
      // Function body
    }
    

Example 2:
Now we will write an example on how to define member functions inside the class (we will modify the previous example) in addition to how we call them.

import "Srl/Console.alusus"
import "Srl/String.alusus"
use Srl;

class Car {
  def brand: String = "BMW";
  def model: String = "X5";
  def year: int = 1999;
  def speed_limit: int;
  def mileage: int;
  def info: String;
  // define a function to get the basic information of the car (the brand + model + year) as a string.
  handler this.getInfo(): String {
    this.info =
      String("Brand: ") +
      this.brand +
      String("\nModel: ") +
      this.model +
      String("\nYear: ") +
      String.format("%i", this.year);
    return this.info;
  }
  // define a function to get the speed limit as a string.
  handler this.getSpeed(): String {
    return String("Speed: ") + this.speed_limit;
  }
  // define a function to get the mileage as a string.
  handler this.getMileage(): String{
    return String("Mileage: ") + this.mileage;
  }
  // define a function to set the speed limit.
  handler this.setSpeed(speed_limit: int) {
    this.speed_limit = speed_limit;
  }
  // define a function to set the mileage.
  handler this.setMileage(mileage: int) {
    this.mileage = mileage;
  }
}

def carObj1:Car;
// set the speed limit and the mileage.
carObj1.setSpeed(200);
carObj1.setMileage(10000);
// print car information
Console.print("-----------------------\n");
Console.print(carObj1.getInfo());
Console.print("\n-----------------------\n");
Console.print(carObj1.getSpeed());
Console.print("\n-----------------------\n");
Console.print(carObj1.getMileage());
Console.print("\n-----------------------");

// the output will be:
/*
-----------------------
Brand: BMW
Model: X5
Year: 1999
-----------------------
Speed: 200
-----------------------
Mileage: 10000
-----------------------
*/

Note: in the previous example you can also convert the definition of `info` to a property instead of a variable. It is used as a variable but it is a function. This implies that its value will be recomputed on every use instead of once in initialization.

import "Srl/Console.alusus"
import "Srl/String.alusus"
use Srl;
// Create a Car class with some attributes
class Car {
  def brand: String;
  def model: String;
  def year: int;
  def speed_limit: int;
  def mileage: int;

  handler this.info: String { // define a property
    return String("Brand: ") + this.brand + String("\nModel: ") + this.model;
  }
}

def carObj1: Car;
carObj1.brand = "BMW";
carObj1.model = "X5";
carObj1.year = 1999;
carObj1.speed_limit = 200;
carObj1.mileage =10000;

Console.print("-----------------------\n");
Console.print(carObj1.info);
Console.print("\n-----------------------");
/*
-----------------------
Brand: BMW
Model: X5
-----------------------
*/

Constructors (Initialization Operations)


Constructors are a special class members that are called by the compiler each time a new object is created from that class.
Constructors can be defined inside the class in the following way:

handler this~init (argument_defs) {
  // function body
};

When we define an object of a class (named object or temp object) and want a specific constructor to be called we add parenthesis after the name of the class and pass the arguments of the wanted constructor between them. The compiler will pick the constructor that matches the provided arguments.

Notes:

  1. Each class created contains at least one constructor. Even if you don't define a one, the compiler will generate a default one.
  2. Each time a new object is created from the class, the constructor must be called to do that.
  3. In case you defined a constructor the compiler will not generate a default one.
  4. You can define more than one constructor. And you can always create an empty constructor so that you can use it if you don't want to give an initial specified values for the attributes when creating an object.

Example:
In the next example we will define multiple constructors (we will modify the main example).

import "Srl/Console.alusus"
import "Srl/String.alusus"
use Srl;

class Car {
  def brand: String;
  def model: String;
  def year: int;
  def speed_limit: int;
  def mileage: int;
  // define a constructor without arguments to initialize the values of the brand and the model (the first constructor).
  handler this~init() {
    Console.print("Hi.. I'm the first Constructor!\n");
    this.brand = "Ford";
    this.model = "Mustang";
  }
  // define another constructor that initialize the same previous attributes but it takes
  // the value as an arguments when defining the object ((the second constructor).)
  handler this~init (brand: String, model: String) {
    Console.print("Hi.. I'm the second Constructor!\n");
    this.brand = brand;
    this.model = model;
  }
  // define a constructor that initialize all previous attributes and takes the values as arguments when defining the object (the third constructor).
  handler this~init (
    brand: String, model: String, year: int, speed_limit: int, mileage: int
  ) {
    Console.print("Hi.. I'm the third Constructor!\n");
    this.brand = brand;
    this.model = model;
    this.year = year;
    this.speed_limit = speed_limit;
    this.mileage = mileage;
  }

  handler this.basicInfo:String {
    return String("Brand: ") + this.brand + String("\nModel: ") + this.model;
  }
  handler this.fullInfo:String {
    return String("Brand: ") + this.brand + String("\nModel: ") + this.model
      + String("\nYear: ") +this.year+String("\nSpeed: ") +this.speed_limit
      + String("\nMileage: ") +this.mileage;
  }
}

def carObj1:Car; // here the first constructor will be called
Console.print("-----------------------\n");
Console.print(carObj1.basicInfo);
Console.print("\n-----------------------");
def carObj2:Car(String("BMW"), String("X5")); // here the second constructor will be called
Console.print("-----------------------\n");
Console.print(carObj2.basicInfo);
Console.print("\n-----------------------");
def carObj3:Car(String("BMW"), String("X5"), 1999, 200, 10000); // here the third constructor will be called
Console.print("-----------------------\n");
Console.print(carObj3.fullInfo);
Console.print("\n-----------------------");

/*
-----------------------
Hi.. I'm the first Constructor!
Brand: Ford
Model: Mustang
----------------------------------------------
Hi.. I'm the second Constructor!
Brand: BMW
Model: X5
----------------------------------------------
Hi.. I'm the third Constructor!
Brand: BMW
Model: X5
Year: 1999
Speed: 200
Mileage: 10000
-----------------------
*/

Destructors (Termination Operations)


Destructors are special member functions which are called by the compiler when the object is being terminated. This function allows you to do any operation directly before the object is terminated. Any class you define has a default destructor even if you don't define a one yourself, and it is called automatically before the object is terminated.

Notes:

  1. Each class created contains a destructor, even if you don't define a one, the compiler will define a default one for you.
  2. The destructor will be called automatically when the object is being terminated.
  3. You can define only one destructor in the class.
  4. You can not pass arguments to the destructor.

When is the destructor called?

  1. In case the object is created inside a function and the execution reach the function scope end (all the statements inside the function are executed).
  2. Generally, when the execution gets out any scope, the compiler will automatically call this function for every element defined in that scope. This function could be used to release any resources allocated for the object.
  3. In case the object is terminated manually by the user, or by a SrdRef for example.

Example:
We will modify the previous example by defining a constructor and a destructor.

import "Srl/Console.alusus"
import "Srl/String.alusus"
use Srl;

class Car {
  def brand: String;
  def model: String;
  // define a constructor
  handler this~init() {
    Console.print("Constructor is called!\n");
    this.brand = "Ford";
    this.model = "Mustang";
  }
  // define a destructor
  handler this~terminate() {
    Console.print("Destructor is called\n");
  };
}

if true {
  def carObj: Car;
} // here the scope of the previous object is end so its destructor will be called and the print statement will be executed.

/*
Constructor is called!
Destructor is called
*/

Shared Variables and Functions


Shared members are members that are not tied to a specific instance of a class. Shared variables are similar to global variables except that they are defined within the class scope. Shared functions are simply functions defined inside the class scope without receiving a `this` argument. Just like shared variables, shared functions are simply global functions defined within the class scope.

Define Shared Variables

- Defining the variable in the class as shared variable allows you to access its value directly from the class without the need to create an object.
- After assigning an initial value for the shared variable, we can change its value later by using the name of the class followed by a dot "." then the name of the variable.
- It is not possible to use the objects of that class to set the value of the shared variable (so we can access that variable only using the class).
- You can access this variable anywhere inside the class without using `this`, as if it is a global variable.
- The value of this variable cannot be set inside a function (give it an initial value) but it is possible to change its value.
- To define a variable as shared we use the keyword `@shared`.

Example:
We will create a class that represents a car, and we will define multiple cars (objects) that have the same brand, but with different models.

import "Srl/Console.alusus"
import "Srl/String.alusus"
use Srl;

class Car {
  @shared def brand: String;
  def model: String;
  def year: int;
  // define a constructor
  handler this~init (model: String, year: int) {
    Console.print("Constructor is called!\n")
    this.model=model;
    this.year=year;
  }
  // print the information of the car.
  handler this.info: String {
    return
      String("Brand: ") + brand + // note that we didn't use `this` with the shared variable
      String("\nModel: ") + this.model +
      String("\nYear: ") + String.format("%i", this.year);
  }
}

Car.brand = "BMW"; // note that we didn't need to create an object to change the brand.
def carObj1: Car(String("BMW_8_Series"), 2020);
def carObj2: Car(String("BMW_M6"), 2018);
def carObj3: Car(String("BMW_i8"), 2019);
Console.print("-----------------------\n");
Console.print(carObj1.info);
Console.print("\n-----------------------\n");
Console.print(carObj2.info);
Console.print("\n-----------------------\n");
Console.print(carObj3.info);
Console.print("\n-----------------------");

/*
Constructor is called!
Brand: BMW
Model: BMW_8_Series
Year: 2020
-----------------------
Constructor is called!
Brand: BMW
Model: BMW_M6
Year: 2018
-----------------------
Constructor is called!
Brand: BMW
Model: BMW_i8
Year: 2019
-----------------------
*/

Defining Shared Functions

- The idea of the shared function is simply defining a function inside the class that could be called directly using the class without the need to create an object.
- The function defined as shared, could only use the shared variables (or the global variables outside the class).
- We define a shared function as we define the regular one, but without using `@member` modifier on it.

Example:
We will use a shared variable and function to know the number of objects created from the class (we will modify the base example).
At first, we will define a shared variable named `counter` and a shared function named `getCounter` which returns the value if `counter` when it is called.
In class `Car` we modify the constructor to make it add 1 to `counter` when it is called, in this way every time an object is created the counter will increase by 1, so the variable will hold the number of objects created. Finally, we create 3 object from this class and called `getCounter` each time to know how many objects were created.

import "Srl/Console.alusus"
import "Srl/String.alusus"
use Srl;

class Car {
  @shared def brand: String;
  def model: String;
  def year: int;
  @shared def counter: int;
  handler this~init(model: String, year: int){
    counter++;
    this.model = model;
    this.year = year;
  }
  function getCounter(): int {
    return counter;
  }
}

Car.brand="BMW";
Car.counter=0;
def carObj1: Car(String("BMW_8_Series"), 2020);
def carObj2: Car(String("BMW_M6"), 2018);
def carObj3: Car(String("BMW_i8 "), 2019);
Console.print("-----------------------\n");
Console.print("%d\n", Car.getCounter());
Console.print("%s\n", carObj1.model.buf);
Console.print("%d\n", Car.getCounter());
Console.print("%s\n", carObj2.model.buf);
Console.print("%d\n", Car.getCounter());
Console.print("%s\n", carObj3.model.buf);
Console.print("%d\n", Car.getCounter());
Console.print("-----------------------\n");

/*
-----------------------
0
BMW_8_Series
1
BMW_M6
2
BMW_i8
3
-----------------------
*/

The above code prints 0 on first call instead of 3 which is the number of objects created, to find the reason see the next simple modifications on the previous code and the explanation of that.

import "Srl/Console.alusus"
import "Srl/String.alusus"
use Srl;

class Car {
  @shared def brand: String;
  def model: String;
  def year: int;
  @shared def counter: int;

  handler this~init(model: String, year: int) {
    counter++;
    this.model = model;
    this.year = year;
  }

  function getCounter(): int {
    return counter;
  }
}

Car.brand = "BMW";
Car.counter = 0;
def carObj1: Car(String("BMW_8_Series"), 2020);
def carObj2: Car(String("BMW_M6"), 2018);
def carObj3: Car(String("BMW_i8 "), 2019);
Console.print("-----------------------\n");
Console.print("%s\n", carObj1.model.buf);
Console.print("%d\n", Car.counter);
Console.print("%s\n", carObj2.model.buf);
Console.print("%d\n", Car.counter);
Console.print("%s\n", carObj3.model.buf);
Console.print("%d\n", Car.counter);
Console.print("-----------------------\n");

func test {
  def carObj1: Car(String("BMW_8_Series"), 2020);
  def carObj2: Car(String("BMW_M6"), 2018);
  def carObj3: Car(String("BMW_i8 "), 2019);
  Console.print("-----------------------\n");
  Console.print("%s\n", carObj1.model.buf);
  Console.print("%d\n", Car.count);
  Console.print("%s\n", carObj2.model.buf);
  Console.print("%d\n", Car.count);
  Console.print("%s\n", carObj3.model.buf);
  Console.print("%d\n", Car.count);
  Console.print("-----------------------\n");
}
test();

/*
-----------------------
BMW_8_Series
1
BMW_M6
2
BMW_i8
3
-----------------------
-----------------------
BMW_8_Series
6
BMW_M6
6
BMW_i8
6
-----------------------
*/

The reason is that global variables are not initialized until they are accessed. So if you define a variable and don't use it, it is as if you didn't define it at all. In such case the compiler will ignore it and won't allocate memory for it. This only applies to global variables, not the local ones. So we can see from the previous example that `counter` increased only after we use that variable, so after we have used the variable for the first time, its value became 1, and in the second time it increased to 2, and so on. In case of local variables (as we saw in `test` function), it is initialized at declaration so as soon as we define the 3 local variables and even before using them, the value of the counter becomes 6.

Operator Overriding


We can override the operators applied on the objects and provide our own functionality for the operation. For example, you can override the operator = of a class so that every time this operator is used on an object of that class the compiler calls the function you provided for that operator.

To override an operator we use `handler` command in a similar way to what we have done when defining constructors.

Example:
Now we will define a class that represents an integer, then we will override + operator for it.

import "Srl/Console.alusus"
import "Srl/String.alusus"
use Srl;
class IntegerNumber {
  def number: int;
  // define a default constructor the initialize the number with 0.
  handler this~init() {
    this.number = 0;
  }
  // define a constructor that initializes the number with a given number
  handler this~init(initialValue: int) {
   this.number = initialValue;
  }
  // override the operator + for this object.
  handler this + int: int {
    return this.number + value;
  }
  // override += operator also.
  handler this += int: int {
    return this.number += value;
  }
}

// Now let's apply what we have done.
def num1: IntegerNumber;
def num2: IntegerNumber(15);
def res:int;
res = num1 + 15;
Console.print("res: %d\n", res);
num1 += 5;
num2 += 5;
Console.print("num1: %d\n", num1.number);
Console.print("num2: %d\n", num2.number);
/*
res: 15
num1: 5
num2: 20
*/

Note how we override the operator + in the previous example to do the addition operation on the object.
Where `this` points to the object itself (`num1` or `num2` in our example), and `+` is the required operator, whereas `int` is the data type of the right operand. The return type will be `int` which is the data type of `number`.
In a similar manner we override the operator +=.

Important Notes


- The compiler distinguishes between basic classes and the classes with custom initialization (constructors or destructors).
- The only case that class is considered to be a basic class (which is `struct` in C) is if the class only contains variables definitions with basic types and without overriding anything. Otherwise the class will be considered a class with custom initialization (contains constructors).
- Back to the previous example, if we tried to copy the object `num2` to `num1`:

def num1: IntegerNumber;
def num2: IntegerNumber(15);
num1 = num2;

We will get an error, and this is because the compiler does not automatically provide an assign operation for classes with custom initilization. This is intentional, because the compiler has no way of guarnteeing that a basic copy operation is safe with such classes, so it leaves it to the user to define a custom = operation for that class. For example, if the class allocates memory in the constructor and frees it in the destructor then doing a copy of that class will result in a segmentation fault because we'll end up with two objects trying to free the same memory block in their destructors.
So we should always (in cases of non-basic classes) follows the next 2 rules:

  1. Overriding `=` operator to be able to use it with this object (as we did in the previous example when we tried to copy `num2` to `num1`).
  2. Define another constructor that takes a reference to an object with the same class (copy constructor) in case you want to pass the object as an argument to a functions, return it from a function, or initialize another object from it (we will see an example for each case).

Solution:
We will solve the previous problem, which is the inability to use the operator `=`.
To solve the problem of using `=` operator, we should follow rule (1).

import "Srl/Console.alusus"
import "Srl/String.alusus"
use Srl;
class IntegerNumber {
  def number:int;
  handler this~init() {
    this.number = 0;
  }
  handler this~init(initialValue: int) {
    this.number = initialValue;
  }
  handler this + int: int {
    return this.number + value;
  }
  handler this += int: int {
    return this.number += value;
  }
  // overriding the operator `=` to solve the problem.
  handler this = IntegerNumber {
    this.number = value.number;
  }
}

def num1: IntegerNumber;
def num2: IntegerNumber(15);
num1 = num2; // now we are able to use `=` operator to copy one object to another.

We will define a function that create an object and return it, then we will use it.

import "Srl/Console.alusus"
import "Srl/String.alusus"
use Srl;
class IntegerNumber {
  def number:int;
  handler this~init() {
    this.number = 0;
  }
  handler this = IntegerNumber {
    this.number = value.number;
  }
}
// define a function that create an object from class `IntegerNumber` and return it.
function getIntegerNumber(): IntegerNumber {
  def num:IntegerNumber;
  return num;
}
def obj:IntegerNumber;
obj = getIntegerNumber(); // this will give an error.

As you can see in the above example, it will gave an error when we tried to call this function because we did not follow rule (2). So to solve the problem, we should define a constructor with an argument (copy constructor) as follows:

import "Srl/Console.alusus"
import "Srl/String.alusus"
use Srl;
class IntegerNumber {
  def number:int;
  handler this~init() {
    this.number = 0;
  }
  handler this = IntegerNumber {
    this.number = value.number;
  }
  // define a constructor with a reference to an object from the same class as argument.
  handler this~init(src: ref[IntegerNumber]) {
    this.number = src.number;
  }
}

function getIntegerNumber(): IntegerNumber {
  def num:IntegerNumber;
  return num; // here `this~init(src)` will be called.
}
def obj: IntegerNumber;
obj = getIntegerNumber();

The same thing applies when we have an operation of initializing one object from another, or a function that takes an argument of this class type. For example, here we have a function that takes an object from previous class then prints `number` value.

function printNumber (num: IntegerNumber) {
  print(num.number);
}
def num: IntegerNumber;
printNumber(num);

Of course this will gave us an error, because we did not follow rule (2).

Class Templates


Templates allow the user to define a class that receives arguments at compile time during variable instantation. This is a pretty powerful tool as it allows us to customize the class per instance, which saves us from having to re-define the class for each different use case. For example, the SrdRef class is a template class and it is not possible to implement it without templates since it will mean re-defining it for each different content type.

Classes templates can be defined by defining arguments that the class will use in its body, and passing those arguments when defining an object of that class. The definition takes the following form:

class class_name [ template_arg_defs ] {
  // class_body
}

The usage takes the following form:

def var_name: class_name[template_args];

To understand the class templates look at the next example.
We want to define two classes, each of them will represent a point that contains two variables (x and y coordinates). But the data of one of them is of type `Int` while the other is of type `Float`.
The solution without using class templates:

import "Srl/Console.alusus";
use Srl.Console;
// define a point with `Int` data.
class IntegerPoint {
  def x: int;
  def y: int;
  handler this~init(x: int, y: int) {
    this.x = x;
    this.y = y;
  }
}
// define a point with `Float` data.
class FloatPoint {
  def x: float[64];
  def y: float[64];
  handler this~init(x: float, y: float) {
    this.x = x;
    this.y = y;
  }
}
def point1: IntegerPoint(5, 3);
def point2: FloatPoint(5.0, 3.0);
print("Integer point: (%d, %d)\n", point1.x, point1.y);
print("Float point: (%0.1f, %0.1f)", point2.x, point2.y);
/*
Integer point: (5, 3)
Float point: (5.0, 3.0)
*/

Using class templates:

import "Srl/Console.alusus";
use Srl.Console;
// define one template class.
class Point [T: type] {
  def x: T;
  def y: T;
  handler this~init(x: T, y: T) {
    this.x = x;
    this.y = y;
  }
}
def point1: Point[int](5, 3); // define a point with `Int` data.
def point2: Point[float[64]](5.0, 3.0); // define a point with `Float` data.
print("Integer point: (%d, %d)\n", point1.x, point1.y)
print("Float point: (%0.1f, %0.1f)", point2.x, point2.y)
/*
Integer point: (5, 3)
Float point: (5.0, 3.0)
*/

Where:
T: it is a placeholder for the data type until it is specified when defining an object from that class. Which means we delayed specifying the data type until we define an object.
type: it means a type or genre, which means that `T` is a placeholder for the data type. It is one of the 5 template's arguments that we will mention soon.

Note that we got the same result, but with simpler and shorter code.

Templates arguments can be one of the following 5 types:

  1. type: which means the placeholder is a data type.
  2. function: which means the placeholder is a function.
  3. module: which means the placeholder is a module.
  4. integer: which means the placeholder is an value with type `Int`.
    import "Srl/Console.alusus";
    use Srl.Console;
    class Point [T:type, Value:integer] {
      def x: T = Value;
      def y: T = Value;
    }
    def point: Point[int, 8];
    print("Integer point: (%d,%d)\n", point.x, point.y);
    /*
    Integer point: (8,8)
    */
    
  5. string: which means the placeholder is a string literal.
  6. ast: used in the class preprocessing, it allows the user to pass a source code as an argument to the template.
  7. ast_ref: used in the class preprocessing, it allows the user to pass a reference to a source code as an argument to the template.

For more information visit the Language Reference.

It is also possible to set an initial values for template arguments. For example, in the previous code we can put the default values for `Value` to be used in case it is not passed, as follows:

import "Srl/Console.alusus";
use Srl.Console;
class Point [T:type, Value:integer = 5] {
  def x: T = Value;
  def y: T = Value;
}
def point: Point[int];
print("Integer point: (%d,%d)\n", point.x, point.y);
/*
Integer point: (5,5)
*/