Classes
Define classes with the class
keyword:
Dog: class {
name: String
race: Race
init: func (=name, =race)
bark: func { "Woof!" println() }
}
Then, call new
on it to make an instance of it:
dog := Dog new()
An instance of a class is also called an object.
Members
Members are variables tied to an instance:
Dog: class {
// declare a field named 'name'
name: String
init: func (=name)
}
d1 := Dog new("Rita")
d2 := Dog new("Igloo")
"d1's name is = #{d1 name}" println()
"d2's name is = #{d2 name}" println()
Built-in members
There are a few members always available on classes. You can access the
class of any object via the class
member. For example:
// will be equal to 'Dog'
dog class name
// since objects are reference, will be the size of a pointer
dog class size
// the actual size of a dog object, including members
dog class instanceSize
Static members
Static members belong to a class, rather than to an instance.
Node: class {
count: static Int = 0
init: func {
This count += 1
}
}
for (i in 0..10) {
Node new()
}
"Number of nodes: #{Node count}" println()
In the code above, count
is “shared” among all instances of node - hence,
incrementing it in the constructor will be “remembered” the next time a node
is created. So, we really are counting the number of nodes being created.
Static fields can also be accessed without explicitly referring to This
.
The declare-assignment operator, :=
, also works with the static
keyword before
the right-hand-side value:
Node: class {
count := static 0
init: func {
count += 1
}
}
// etc.
Properties
The shortest and sweetest way to define a property is to use the ::=
operator:
Rectangle: class {
width, height: Int
area ::= width * height
}
Contrary to a variable declaration, the value of area
will be
recomputed every time it is being accessed. Contrary to a function
call, one does not need parenthesis to call its getter, nor can it
pass any argument.
Properties are mostly useful as shorthands for an expression that is often computed, but that would be overkill as a method.
In the technical jargon, we say that properties are, virtual
members that exist as read-only, write-only, or read-write behind
getters and setters.
Here’s an example of long-form, read-only property:
Person: class {
lastName, firstName: String
fullName: String {
get {
"%s %s" format(lastName, firstName)
}
}
}
Note that when specifying a getter, one does not need a return type, as it is the type of the property itself.
Similarly, when specifying a setter, one only needs an argument name not its type:
Person: class {
lastName, firstName: String
name: String {
set (name) {
tokens := name split(" ")
assert(tokens size == 2)
(firstName, lastName) = (tokens[0], tokens[1])
}
}
}
Empty getters and setters are valid as well, for a simple read-write property:
Person: class {
name: String { get set }
}
The advantage is the following - since a property is only accessed via its
getters and setters, which are methods, changing the structure of the Person
class will not necessarily trigger a recompile on the modules which use it,
nor will they need to explicitly import that module, if they get a Person
instance
from somewhere else.
This is a way to work around what is known as the Fragile Base Class Problem.
Methods
Methods are function declarations in the class body, that are called on a particular instance:
Dog: class {
bark: func {
"Woof!" println()
}
}
dog := Dog new()
dog bark()
this and This
In a method, the special variable this
is accessible, and refers to the object
the method is being called on.
Example usage of this
Building: class {
height: Int
// argument name shadows member name
setHeight: func (height: Int) {
if (height < 0 || height > 300) return
// using `this` explicitly to differenciate them
this height = height
}
}
This
, on the other hand, refers to the type currently being defined:
Engine: class {
logger := Log getLogger(This name)
}
In the example above, we are using the name of the class we are currently defining,
instead of typing out "Engine"
directly — that way, if we rename the class, the
code will still be valid. It’s a good way to avoid repeating yourself.
Static methods
Static methods also belong to a specific class, but they’re not tied to
a particular instance. Hence, you don’t have access to this
in a static
method because it’s not called on an instance:
Map: class {
tiles := Map<Tile> new()
generate: static func (width, height: Int) -> This {
m := This new()
for (y in 0..height) for (x in 0..width) {
m addTile(x, y)
}
m
}
addTile: func (x, y: Int) { /* ... */ }
}
In some languages, new
is a keyword used to create objects. In ooc,
it’s just a static method doing some allocation and initialization, and
returning a new instance. See “Constructors” for more details.
Constructors
Define the init
method (with a suffix to have different constructors), and
a new
static method will get defined automatically.
Dog: class {
name: String
init: func (=name)
init: func ~default { name = "Fido" }
}
For alternative instanciation strategies, defining a custom, static new
method, returning an instance of type This
, works just as well:
Dog: class {
pool := static Stack<This> new()
new: static func -> This {
if (pool empty?()) {
obj := This alloc()
obj __defaults__()
obj
} else {
pool pop()
}
}
free: func {
pool push(this)
}
}
We can clearly see that the alloc
static method here does memory allocation
for the object, but what about __defaults__
? It contains initializers, discussed
in the next section.
Initializers
We’ve discussed methods, but not all code that belong to a class is in an explicit method. For example, in this code, declaration and initialization are clearly separate:
Group: class {
number: Int
init: func {
number = 42
}
}
But what happens with the following code?
Group: class {
number := 42
init: func {
}
}
The resulting executable does the same. The class contains a field
of type Int, initially equal to 0, but there’s an implicit __defaults__
method that contains all code outside of a method, that gets executed before
the init
method is called.
Above, the example is a pattern you’ll see often - however, one can put any amount of code directly in the class declaration:
Dog: class {
"You made a dog!" println()
init: func
}
Every time Dog new()
is called, the "You made a dog!"
string will get
printed.
Inheritance
Extends
Simple inheritance is achieved through the extends
keyword:
Animal: class {}
Dog: class extends Animal {}
In this case, an instance of Dog
will also be an instance of Animal
,
and it inherits all its methods and members.
For example, a function expecting an Animal
can be passed a Dog
instead.
That is, if your code is designed correctly. For some encyclopedic knowledge
on the matter, check out the Liskov Substition Principle
Super
Calling super
will call the definition of a method in the super-class.
SimpleApp: class {
init: func {
loadConfig()
}
// ...
}
NetworkedApp: class extends SimpleApp {
init: func {
super()
initNetworking()
}
}
When one just wants to relay a constructor, one can use super func
:
MyException: class extends Exception {
init: super func
}
Which is equivalent to the following:
MyException: class extends Exception {
init: func {
super()
}
}
super func
can take a suffix, and it relays argument as well. It is useful
when you really don’t have much more to do in the constructor of the sub-class.
Please bear in mind that super func
is relatively hackish - it is documented
here for completeness’ sake, but it is more of a rapid coding trick than a good
practice, really.
Class hierarchy
The class hierarchy can be explored via built-in members and methods on objects and classes:
// in this case, the Object class - otherwise, whatever super class it has
dog class super
// evaluates to true
dog instanceOf?(Dog)
// also evaluates to true
dog instanceOf?(Object)
// evaluates to false
dog instanceOf?(Cat)
The equivalent of instanceOf?
called on classes, is inheritsFrom?
// true
Dog inheritsFrom?(Dog)
// true
Dog inheritsFrom?(Object)
// false
Dog inheritsFrom?(Cat)
Adding methods after definition
This applies to classes, covers, and enums alike. The extend
keyword can add superficial methods to any type, even if it is
defined in another module.
It is useful to add convenience methods of your own without having to modify the original library.
extend Float {
negated: func -> This { -this }
}
if (-3.14 == 3.14 negated()) {
"Everything is fine" println()
}
Virtual properties (that do not correspond to a real instance variable,
but rather compute their value from other information everytime) can also
be added in an extend
block:
extend Int {
plusFive: This { get {
this + 5
} }
}
See the Properties section for more info on properties.