1. Class

An object is an instance of a class. A class is declared using the keyword class, followed by the identifier of the class. The complete grammar of a class definition is presented in the following code block.

class_decl := simple_class_decl | template_class_decl

simple_class_decl := 'class' (modifiers)? Identifier ('over' type)? '{' class_content '}'
template_class_decl := 'class' ('if' expression)? (modifiers)? Identifier templates ('over' type)? '{' class_content '}'
class_content :=   field
                 | method
                 | constructor
                 | impl
                 | destructor
modifiers := '@' ('{' modifier (',' modifier)* '}') | (modifier)
modifier := 'final' | 'abstract'


As many symbols, the class can be a template symbols. Templates are not presented in this chapter, and are left for a future chapter (cf. Templates).

1.1. Fields

A class can contain fields. A field is declared as a variable, using the keyword let. A field can have a default value, in that case the type is optional, however if the value is not set, the field must have a type. Multiple fields can be declared within the same let, with coma separating them.

class Point4D {
    let _x : i32;
    let _y = 0;
    let _z : i32, _w = 0;
}

1.1.1. Field privacy

All fields are protected by default, i.e. only the class defining them and its heirs have access to them. The keyword pub can be used to make the fields public, and accessible from outside the class definition. The keyword prv, on the other hand, can be use to make the field totally hidden from outside the class. Unlike prot, prv fields are not accessible by the heirs of a class.

A good practice is to enclose the privacy of the fields in their name definition. For example, a public field is named x, without any _ token. A protected fields always starts with a single underscore, _y, and private fields are surrounded by two underscores before and after the identifier. This is just a convention, the name has no impact on the privacy.

class A {
    pub  let x = 12;
    prot let _y : i32;
    prv  let __z__ : i32;
}

1.2. Constructor

To be instancible, a class must declare at least one constructor. The constructor is always named self, and takes parameters to build the object. The object is accessible within the constructor body through the variable self.

1.2.1. Field construction

The constructor must set the value to all the fields of the class, with the keyword with. Fields with default values are not necessarily set by this with statement, but can be redefined. The with statement has the effect of the initial value of the fields, meaning that the value of immutable fields is set by them.

class Point {
    let _x : i32;
    let _y = 0;

    /**
     * _y is already initialized
     * it is not mandatory to add it in the with initialization
     */
    pub self () with _x = 12 {}

    /**
     * But it can be redefined
     */
    pub self (x : i32, y : i32) with _x = x, _y = y {}
}


Field value is set only once. For example, if a class has a field _x with a default value, calling a function foo. And the constructor use the with statement to set the value of the field from the return value of the bar function, the function foo is never called.

def foo () -> i32 {
    println ("Foo");
    42
}

def bar () -> i32 {
    println ("bar")
    33
}

class A {
    let _x = foo ();

    pub self () with _x = bar () {} // foo is not called, bar is call instead
}


The point behind with field construction, is to ensure that all fields have a value, when entering the constructor body. This way, when instruction are made inside the constructor body, such as printing the value of the fields, their value is already set, and the object instance is already valid.

class A {
    let _x : i32;

    pub self () with _x = 42 {
        println (self._x); // access the field _x, of the current object instance
    } 
}

1.2.2. Create an object instance

Class are used to create object instance, by calling a constructor. This call is made using the double colon binary operator ::, with the left operand being the name of the class to instantiate, and the right operand the keyword new. After the keyword new a list of argument is presented, this list is the list of argument to pass to the constructor. The constructor with the best affinity is choosed and called on a allocated instance of the class. Constructor affinity is computed as function affinity (based on type mutability, and type compatibility – cf. Aliases and References).

import std::io

class A {

    pub self (x : i32) {
        println ("Int : ", x);
    }

    pub self (x : f32) {
        println ("Float : ", x);
    }

}

def main () {
    let _ = A::new (12);
    let _ = A::new (12.f);
}


Results

Int : 12
Float : 12.000000

1.2.3. Named constructor

A name can be added to the constructor. This name is an Identifier, and follows the keyword self. A named constructor can be called by its name when constructing a class. This way two constructor can share the same prototype, and be called from their name. A named constructor is not ignored when constructing a class with the new keyword, its named is just not considered.

In the following example, a class A contains two constructors, foo and bar, these constructors have one parameter of type i32.

import std::io

class A {
    let _x : i32;

    pub self foo (x : i32) with _x = x {
        println ("Foo ", self._x);
    }

    pub self bar (x : i32) with _x = x {
        println ("Bar ", self._x);
    }
}

def main () {
    let _ = A::foo (12);
    let _ = A::bar (12); 

    let _ = A::new (12);
}


The last object construction at line 19 is not possible, the call working with both foo and bar. The other constructions work fine, that is why the compiler returns only the following error.

Error : {self foo (x : i32)-> mut &(mut main::A), self bar (x : i32)-> mut &(mut main::A)} x 2 called with {i32} work with both
 --> main.yr:(19,17)
19  ┃     let _ = A::new (12);
    ╋                    ^
    ┃ Note : candidate self --> main.yr:(6,6) : self foo (x : i32)-> mut &(mut main::A)Note : candidate self --> main.yr:(10,6) : self bar (x : i32)-> mut &(mut main::A)
    ┗━━━━━━ 


ymir1: fatal error: 
compilation terminated.

1.2.4. Construction redirection

To avoid redondent code, a constructor can call another constructor in the with statement. This call redirection is performed by using the self keyword. In that case, because the fields are already constructed by the called constructor, they must not be reconstructed.

import std::io;

class A {
    let _x : i32;

    pub self () with self (12) {
        println ("Scd ", self._x);
    }

    pub self (x : i32) with _x = x {
        println ("Fst ", self._x);
    }

}


def main () {
    let _ = A::new (); // construct an instance of the class A
                       // from the constructor at line 6
}


Results:

Fst 12
Scd 12


Contribution: Redirection to named constructor does not work for the moment. This is not complicated, but has to be done.

1.2.5. Constructor privacy

As fields, the privacy of the constructor is protected by default. The keyword prv and pub can be used to change it.

1.3. Destructors

Object instances are destroyed by the garbage collector. Meaning that there is no way to determine when or even if an object instance will be destroyed. But lets say that such operation effectively happen (which is quite probable, let's no lie either), then a last function can be called just before the object instance is destroyed and irrecoverable. The destructor is called __dtor, and takes the parameter mut self. There is only one destructor per class, this destructor is optional and always public.

import std::io;

static mut I = 0;

class A {
    let _x : i32;
    pub self () with _x = I {
        I += 1;
    }

    __dtor (mut self) {
        println ("Destroying me ! ", self._x); 
    }

}

def foo () {
    let _ = A::new (); 
} // the object instance is irrecoverable at the end of the function

def main () {
    loop {
        foo ();
    }
}


Results:

Destroying me ! 765
Destroying me ! 1022
Destroying me ! 764
Destroying me ! 1023
Destroying me ! 763
Destroying me ! 1024
Destroying me ! 762
Destroying me ! 1025
Destroying me ! 761
Destroying me ! 1026
...


One can note from the result, that the order of destruction is totally unpredictible. Rely on class destructor is not the best practice. We will see in a future chapter disposable objects, that are destroyed in a more certain way. Destructors are a last resort to free unmanaged memory, if this was forgotten (for example, file handles, network socket, etc. if not manually disposed).

results matching ""

    No results matching ""