11 Types
HULK is ultimately an object-oriented language with simple inheritance and nominal typing. It also has features of structural typing via protocols, which support language features such as iterables, which we will explain later.
This section explains the basics of HULK’s nominal typing system.
A type in HULK is basically a collection of attributes and methods, encapsulated under a type name. Attributes are always private, which means they can’t be read or writen to from any code outside the type in which they are defined (not even inheritors), while methods are always public and virtual.
11.1 Declaring types
A new type is declared using the type
keyword followed by a name, and a body composed of attribute definitions and method definitions. All attributes must be given an initialization expression. Methods, like functions, can have a single expression or an expression block as body;
Point {
type = 0;
x = 0;
y
getX() => self.x;
getY() => self.y;
setX(x) => self.x := x;
setY(y) => self.y := y;
}
The body of every method is evaluated in a namespace that contains global symbols plus an especial symbol named self
that references the current instance. The self
symbol is not a keyword, which means it can be hidden by a let
expression, or by a method argument.
However, when referring to the current instance, self
is not a valid assignment target, so the following code should fail with a semantic error:
type A {// ...
f() {
self := new A(); // <-- Semantic error, `self` is not a valid assignment target
} }
11.2 Instantiating types
To instantiate a type you use the keyword new
followed by the type name:
let pt = new Point() in
print("x: " @ pt.getX() @ "; y: " @ pt.getY());
As you can see, type members are accessed by dot notation (instance.member
).
You can pass arguments to a type, that you can use in the initialization expressions. This achieves an effect similar to having a single constructor.
Point(x, y) {
type = x;
x = y;
y
// ...
}
Then, at instantiation time, you can pass specific values:
let pt = new Point(3,4) in
print("x: " @ pt.getX() @ "; y: " @ pt.getY());
Each attribute initialization expression is evaluated in a namespace that contains the global symbols and the type arguments, but no the self
symbol. This means you cannot use other attributes of the same instance in an attribute initialization expression. This also means that you cannot assume any specifc order of initialization of attributes.
11.3 Inheritance
Types in HULK can inherit from other types. The base of the type hierarchy is a type named Object
which has no public members, which is the type you implicitely inherit from by default. To inherit from a specific type, you use the inherits
keyword followed by the type name:
Point {
type PolarPoint inherits rho() => sqrt(self.getX() ^ 2 + self.getY() ^ 2);
// ...
}
By default, a type inherits its parent type arguments, which means that to construct a PolarPoint
you have to pass the x
and y
that Point
is expecting:
let pt = new PolarPoint(3,4) in
print("rho: " @ pt.rho());
If you want to define a different set of type arguments, then you have to provide initialization expressions for the parent type at the declaration:
PolarPoint(phi, rho) inherits Point(rho * sin(phi), rho * cos(phi)) {
type // ...
}
During construction, the expressions for type arguments of the parent are evaluated in a namespace that contains global symbols plus the type arguments of the inheritor. Like before, you cannot assume a specific order of evaluation.
In HULK, the three builtin types (Number
, String
, and Boolean
) implicitely inherit from Object
, but it is a semantic error to inherit from these types.
11.4 Polymorphism
All type methods in HULK are virtual by definition, and can be redefined by an inheritor provided the exact same signature is used:
Person(firstname, lastname) {
type = firstname;
firstname = lastname;
lastname
name() => self.firstname @@ self.lastname;
}
NOTE:
@@
is equivalent to@ " " @
. It is a shorthand to insert a whitespace between two concatenated strings. There is no@@@
or beyond, we’re not savages.
type Knight inherits Person {name() => "Sir" @@ base();
}
let p = new Knight("Phil", "Collins") in
print(p.name()); // prints 'Sir Phil Collins'
The base
symbol in every method refers to the implementation of the parent (or the closest ancestor that has an implementation).