1. Trait
Traits are used to define as their name suggest traits that can be
implemented by a class. To define them, the keyword trait
is
used. A trait is not a type, and can only be implemented by class,
for that reason, a variable or a value cannot be of a trait type.
An example of a trait, is presented in the following source block. The
idea of this trait, is to ensure that every class, implementing it,
have a public method named print
that can be called without
parameters.
trait Printable {
pub def print (self);
}
When a class implements a trait, all the method declared in the trait
are added in the definition of the class. If the class is not abstract
all the method of the traits must have a body. For example, if a non
abstract class implement the trait Printable
, thus it must
override the method print
to add a body to it.
import std::io
class Point {
// ... Constructors and attributes
impl Printable {
// Try to remove the following definition
pub over print (self) {
print ("Point {", self._x, ", ", self._y, "}");
}
}
}
A trait can provide a default behavior for the method it defines. The
body of the method is validated for each implementation. In the
following example, the method print
defined in the trait
Printable
, prints the typeid of the class that implement the
traits. One can note, that the behavior of this function is different
for each class that implements it, that is why it is only validated
when a class implement the trait.
mod main;
trait Printable {
pub def print (self) {
import std::io;
print (typeof (self)::typeid);
// Here we don't know the type yet
}
}
class Point {
// ... Constructors and attributes
impl Printable; // 'print' method prints ("main::Point")
}
class Line {
// ... Constructors and attributes
impl Printable; // 'print' method prints ("main::Line")
}
Warning: If a trait is never implemented by any class, and have methods with default behavior, then it is never validated. Thus errors can be present in this trait, but still pass the compilation. One can see the trait as a kind of template, this problem being present in template symbol as well (cf. Templates).
1.1. Inherit a Class implementing a Trait
The methods of a trait can be overriden by heir classes. In order to do this, heir classes must reimplement the trait, and override the methods.
1.1.1. Simple reimplementation
In the following example, the class Shape
implements
the trait Printable
, this trait has a method print
with a
default behavior. The class Circle
does not reimplement the
trait, thus when calling the method print of a Circle
value, the
value main::Shape
is printed on the stdout. On the other hand,
the class Rectange
reimplement the traits, thus the value
main::Rectangle
is printed.
mod main;
trait Printable {
pub def print (self) {
import std::io;
println (typeof (self)::typeid);
}
}
class @abstract Shape {
self () {}
impl Printable;
}
class Circle over Shape {
pub self () {}
}
class Rectangle over Shape {
pub self () {}
impl Printable; // reimplement the method print with typeof (self) being main::Rectangle
}
def main () {
let c = Circle::new ();
c.print ();
let r = Rectangle::new ();
r.print ();
}
Results:
main::Shape
main::Rectangle
1.1.2. Override implemented method
Implemented method cannot be overriden without reimplementing the
trait. In the following example, a class Shape
implement the
trait Printable
, and the class Circle
inherits
Shape
, and tries to override the method print
.
mod main;
import std::io;
trait Printable {
pub def print (self);
}
class @abstract Shape {
self () {}
impl Printable {
pub over print (self) {
println ("main::Shape");
}
}
}
class Circle over Shape {
pub self () {}
pub over print (self) {}
}
Errors:
Error : when validating main::Circle
--> main.yr:(18,7)
18 ┃ class Circle over Shape {
╋ ^^^^^^
┃ Error : cannot override the trait method (const self) => main::Shape::print ()-> void outside the implementation of the trait
┃ --> main.yr:(21,14)
┃ 21 ┃ pub over print (self) {}
┃ ╋ ^^^^^
┃ ┃ Note :
┃ ┃ --> main.yr:(12,18)
┃ ┃ 12 ┃ pub over print (self) {
┃ ┃ ╋ ^^^^^
┃ ┗━━━━━┻━
┗━━━━━┻━
ymir1: fatal error:
compilation terminated.
To prevent the previous error, the class Circle
have to
reimplement the trait Printable
. When reimplementing a trait in
a heir class, the parent overriding is not taken into account, and the
method of the trait is used. In the following example, the class
Shape
implement the trait Printable
, that have a method
print
with no default behavior. The class Circle
tries to
reimplement the trait, but without overriding the print
method. This source code is rejected by the compiler, the class
Circle
is not abstract, but has a method with no body.
mod main;
import std::io;
trait Printable {
pub def print (self);
}
class @abstract Shape {
self () {}
impl Printable {
pub over print (self) {
println ("main::Shape");
}
}
}
class Circle over Shape {
pub self () {}
impl Printable;
}
Errors:
Error : when validating main::Circle
--> main.yr:(18,7)
18 ┃ class Circle over Shape {
╋ ^^^^^^
┃ Error : the class main::Circle is not abstract, but does not override the empty parent method (const self) => main::Printable::print ()-> void
┃ --> main.yr:(18,7)
┃ 18 ┃ class Circle over Shape {
┃ ╋ ^^^^^^
┃ ┃ Note :
┃ ┃ --> main.yr:(5,10)
┃ ┃ 5 ┃ pub def print (self);
┃ ┃ ╋ ^^^^^
┃ ┗━━━━━┻━
┗━━━━━┻━
ymir1: fatal error:
compilation terminated.
To resolve that problem, the class Circle
must add a body to the
method print
. It can happen that a trait defines multiple
methods, and that only some have to be reimplemented by the heir
class. In that case, there is no magical solution, maybe a
contribution can enhance that, but every methods must be
reimplemented. In order to mimic the behavior of the parent
implementation, the super
keyword can be used.
class Circle over Shape {
pub self () {}
impl Printable {
pub over print (self)-> void {
self::super.print (); // call the print method of the super class
}
}
}
1.2. Trait privacy
Trait implementation is always public. For that reason, privacy
specifier (pub
, prot
and prv
) have no meaning on
implementation.
On the other hand, the trait methods implementation follows the same rule as the overriding of a parent method. That is to say, the privacy defined inside the trait must be the same as the privacy defined inside the implementation.
1.3. Trait usage
As said in the beginning of this chapter, traits do not define
types, thus they cannot be used to define the type of a variable. For
example, the following source code has no meaning, Printable
does not define a type.
mod main;
import std::io;
trait Printable {
pub def print (self);
}
def foo (a : Printable) {
a.print ();
}
Errors:
Error : expression used as a type
--> main.yr:(8,15)
8 ┃ def foo (a : Printable) {
╋ ^^^^^^^^^
ymir1: fatal error:
compilation terminated.
If the previous example, is not a valid ymir code, the behavior can
still be implemented in the language. Traits gain interest when
coupled with templates, and a template test can be used to check that
a class implement a trait. More complete information, and example
about templates, and traits specialization are presented in chapter
Templates,
but a brief example is presented in the following source code. In this
example, two classes U
and V
implement the trait
Printable
. The function foo
takes a parameter whose type
is not specified but must implement the trait Printable
. Thus
the function is callable with both U
or V
as argument.
mod main;
import std::io;
trait Printable {
pub def print (self) {
println (typeof (self)::typeid);
}
}
class U {
pub self () {}
impl Printable;
}
class V {
pub self () {}
impl Printable;
}
/**
* Accept every type, that implements the trait Printable
*/
def foo {I impl Printable} (a : I) {
a.print ();
}
def main () {
foo (U::new ());
foo (V::new ());
}
Results:
main::U
main::V