Object-Oriented Programming
Classes, objects, methods, and inheritance in Strada.
Overview
Strada's OOP model is inspired by Perl:
- Classes are defined using
package - Objects are blessed hash references
- Methods are functions that take
$selfas the first parameter - Automatic method registration enables
$obj->method()syntax
Defining a Class
package Dog;
# Constructor
func new(str $name, int $age) scalar {
my hash %self = {
"name" => $name,
"age" => $age
};
return bless(\%self, "Dog");
}
# Method
func speak(scalar $self) void {
say($self->{"name"} . " says woof!");
}
# Getter method
func get_name(scalar $self) str {
return $self->{"name"};
}
# Setter method
func set_age(scalar $self, int $age) void {
$self->{"age"} = $age;
}
Creating and Using Objects
func main() int {
# Create object using constructor
my scalar $dog = Dog::new("Rex", 3);
# Call methods with arrow syntax
$dog->speak(); # "Rex says woof!"
# Access properties
my str $name = $dog->get_name();
say("Dog's name: " . $name);
# Modify properties
$dog->set_age(4);
return 0;
}
The bless() Function
bless() associates a hash reference with a class name, turning it into an object:
# bless(reference, classname) -> object
my hash %data = { "x" => 10 };
my scalar $obj = bless(\%data, "MyClass");
Package Auto-Prefixing
Functions inside a package block are automatically prefixed with the class name:
package Animal;
# This becomes Animal_new
func new(str $name) scalar {
my hash %self = { "name" => $name };
return bless(\%self, "Animal");
}
# This becomes Animal_speak
func speak(scalar $self) void {
say($self->{"name"} . " makes a sound");
}
# Call with either syntax:
# Animal::new("Rex") or $obj->speak()
- Only applies to simple packages (no
::in name) - Skips
mainfunction andpackage main; - Skips functions that already look class-prefixed (e.g.,
Dog_speak) externfunctions are NOT auto-prefixed
Calling Functions in Current Package
Use ::func() to call a function in the current package without repeating the package name:
package Calculator;
func add(int $a, int $b) int {
return $a + $b;
}
func compute(int $x, int $y) int {
# These are equivalent:
my int $sum = ::add($x, $y); # Preferred
# my int $sum = .::add($x, $y); # Alternate
# my int $sum = __PACKAGE__::add($x, $y); # Explicit
return $sum;
}
::func()— Preferred shorthand.::func()— Alternate shorthand__PACKAGE__::func()— Explicit form
All three resolve to PackageName_func() at compile time.
Note: __PACKAGE__ alone (without ::) returns the package name as a string at runtime.
Inheritance
Implement inheritance by blessing with a derived class name and calling parent methods:
package Animal;
func new(str $name) scalar {
my hash %self = { "name" => $name };
return bless(\%self, "Animal");
}
func speak(scalar $self) void {
say($self->{"name"} . " makes a sound");
}
package Dog;
func new(str $name, str $breed) scalar {
my hash %self = {
"name" => $name,
"breed" => $breed
};
return bless(\%self, "Dog");
}
# Override parent method
func speak(scalar $self) void {
say($self->{"name"} . " barks: Woof!");
}
func get_breed(scalar $self) str {
return $self->{"breed"};
}
package Cat;
func new(str $name) scalar {
my hash %self = { "name" => $name };
return bless(\%self, "Cat");
}
func speak(scalar $self) void {
say($self->{"name"} . " meows: Meow!");
}
UNIVERSAL Methods
All objects have access to isa() and can() methods:
my scalar $dog = Dog::new("Rex", "German Shepherd");
# Check if object is of a class
if ($dog->isa("Dog")) {
say("It's a Dog!");
}
# Check if object has a method
if ($dog->can("speak")) {
$dog->speak();
}
if ($dog->can("fly")) {
say("Dog can fly");
} else {
say("Dogs cannot fly");
}
Polymorphism
func make_sound(scalar $animal) void {
# Polymorphic method call
$animal->speak();
}
func main() int {
my scalar $dog = Dog::new("Rex", "Lab");
my scalar $cat = Cat::new("Whiskers");
make_sound($dog); # "Rex barks: Woof!"
make_sound($cat); # "Whiskers meows: Meow!"
# Store in array for iteration
my array @animals = [$dog, $cat];
foreach my scalar $animal (@animals) {
$animal->speak();
}
return 0;
}
Function Pointers in Objects
Objects can store function references as properties:
package Button;
func new(str $label, scalar $on_click) scalar {
my hash %self = {
"label" => $label,
"on_click" => $on_click
};
return bless(\%self, "Button");
}
func click(scalar $self) void {
my scalar $handler = $self->{"on_click"};
$handler->();
}
# Usage
func main() int {
my scalar $btn = Button::new("Submit", func () {
say("Button clicked!");
});
$btn->click(); # "Button clicked!"
return 0;
}
Method Chaining
Return $self from methods to enable chaining:
package Builder;
func new() scalar {
my hash %self = { "parts" => [] };
return bless(\%self, "Builder");
}
func add(scalar $self, str $part) scalar {
push($self->{"parts"}, $part);
return $self; # Return self for chaining
}
func build(scalar $self) str {
return join(", ", $self->{"parts"});
}
# Usage with chaining
func main() int {
my str $result = Builder::new()
->add("header")
->add("body")
->add("footer")
->build();
say($result); # "header, body, footer"
return 0;
}
AUTOLOAD
When a method is called on an object but doesn't exist in its package (or any parent), Strada normally dies with an error. Define an AUTOLOAD method to catch these calls instead:
package Proxy;
func new(str $target) scalar {
my hash %self = ();
$self{"target"} = $target;
return bless(\%self, "Proxy");
}
func AUTOLOAD(scalar $self, str $method, scalar ...@args) scalar {
say("Called: " . $method);
return "handled";
}
$self— the object instance$method— name of the undefined method that was called...@args— all arguments passed to the undefined method- Real methods always take priority over AUTOLOAD
- AUTOLOAD is inherited through parent classes
$obj->can("missing")returns 0 even if AUTOLOAD exists
Dynamic Method Dispatch
Call methods where the method name is stored in a variable. This is useful for dispatch tables, plugin systems, and dynamic APIs.
my str $method = "speak";
$obj->$method(); # Calls $obj->speak()
$obj->$method($arg1); # With arguments
$obj->$method; # Without parens (accessor style)
- Uses the same runtime dispatch as static
$obj->method() - All OOP features work: inheritance, AUTOLOAD, method modifiers
- Spread arguments supported:
$obj->$method(...@args)
Operator Overloading
Strada supports Perl-style operator overloading via use overload. This lets classes define custom behavior for built-in operators like +, -, *, ., "", ==, <=>, etc.
package Vector;
func new(num $x, num $y) scalar {
my hash %self = ();
$self{"x"} = $x;
$self{"y"} = $y;
return bless(\%self, "Vector");
}
func add(scalar $self, scalar $other, int $reversed) scalar {
return Vector::new(
$self->{"x"} + $other->{"x"},
$self->{"y"} + $other->{"y"});
}
func to_str(scalar $self) str {
return "(" . $self->{"x"} . ", " . $self->{"y"} . ")";
}
use overload
"+" => "add",
'""' => "to_str";
package main;
func main() int {
my scalar $a = Vector::new(1.0, 2.0);
my scalar $b = Vector::new(3.0, 4.0);
my scalar $c = $a + $b; # Calls Vector::add
say("Result: " . $c); # Calls to_str via "" overload
# Output: Result: (4, 6)
return 0;
}
- Arithmetic:
+,-,*,/,%,** - String:
.(concatenation),""(stringify) - Unary:
neg(unary minus),!,bool - Comparison:
==,!=,<,>,<=,>=,<=> - String comparison:
eq,ne,lt,gt,le,ge,cmp
- Binary:
func add(scalar $self, scalar $other, int $reversed) scalar - Unary:
func negate(scalar $self) scalar - Stringify:
func to_str(scalar $self) str
The $reversed parameter is 1 when $self was the right operand (important for non-commutative operators like - and /).
Zero overhead: If no use overload appears in the program, the generated C code is identical to code without overloading — no dispatch checks, no function calls, no extra branches.
Moose-Style OOP
Strada supports a declarative, Moose-inspired OOP system on top of its core bless-based model. This provides automatic constructors, accessor generation, inheritance via extends, role composition via with, and method modifiers (before, after, around).
Declaring Attributes with has
Use has inside a package to declare object attributes. The compiler generates getters (and setters for rw attributes) and an automatic new() constructor.
package Person;
# Read-only attribute, required in constructor
has ro str $name (required);
# Read-write attribute with default value
has rw int $age = 0;
# Read-write attribute with no default (defaults to undef)
has rw str $nickname;
func greet(scalar $self) void {
say("Hi, I'm " . $self->name() . ", age " . $self->age());
}
package main;
func main() int {
# Auto-generated new() takes named arguments
my scalar $p = Person::new("name", "Alice", "age", 30);
say($p->name()); # "Alice" (getter)
say($p->age()); # 30 (getter)
$p->set_age(31); # setter (rw only)
say($p->age()); # 31
$p->greet(); # "Hi, I'm Alice, age 31"
return 0;
}
has [ro|rw] type $name [= default] [(options)];
ro(default) — read-only: generates a getter ($obj->name())rw— read-write: generates both getter and setter ($obj->set_name($val))= default— default value used when the attribute is not passed to the constructor(required)— attribute must be passed to the constructor (no default)(lazy, builder => "method")— lazy evaluation via a builder method
The auto-generated new() constructor takes named arguments as alternating key-value pairs: Class::new("key1", val1, "key2", val2). Missing attributes fall back to their default value (via the // defined-or operator).
If you define an explicit func new(...) in the package, the auto-generated constructor is skipped.
Inheritance with extends
Use extends to declare that a class inherits from one or more parent classes. Child classes inherit parent attributes and their auto-generated constructor includes parent attributes.
package Animal;
has ro str $species (required);
has rw int $energy = 100;
func speak(scalar $self) void {
say($self->species() . " (energy: " . $self->energy() . ")");
}
package Dog;
extends Animal;
has ro str $name (required);
has rw int $age = 0;
func bark(scalar $self) void {
say($self->name() . " barks!");
}
package main;
func main() int {
# Constructor includes both Dog and Animal attributes
my scalar $d = Dog::new("name", "Rex", "species", "dog", "age", 3);
$d->bark(); # "Rex barks!"
$d->speak(); # "dog (energy: 100)" - inherited method
say($d->isa("Dog")); # 1
say($d->isa("Animal")); # 1
return 0;
}
Role Composition with with
Use with to compose roles (mixins) into a class. Roles work similarly to inheritance — the class gains the role's attributes and methods.
package Printable;
func to_string(scalar $self) str {
return "[Object]";
}
package Widget;
with Printable;
has ro str $label (required);
Method Modifiers: before, after, around
Method modifiers let you hook into method calls without modifying the original method. This is useful for logging, validation, and cross-cutting concerns.
before
Runs before the original method. Receives $self as its argument.
package Dog;
extends Animal;
has ro str $name (required);
before "bark" func(scalar $self) void {
say("[preparing to bark]");
}
func bark(scalar $self) void {
say($self->name() . " barks!");
}
# $d->bark() prints:
# [preparing to bark]
# Rex barks!
after
Runs after the original method. Receives $self as its argument.
after "bark" func(scalar $self) void {
say("[done barking]");
}
# $d->bark() now prints:
# [preparing to bark]
# Rex barks!
# [done barking]
around
Wraps the original method. The around modifier receives the original method as a function pointer in the first element of its args, allowing you to call, skip, or modify the original behavior.
around "speak" func(scalar $self) void {
say("[around: before speak]");
# Call the original method via the args mechanism
# or implement custom logic entirely
say("[around: after speak]");
}
before "method" func($self) void { ... }— runs before the methodafter "method" func($self) void { ... }— runs after the methodaround "method" func($self) void { ... }— wraps the method entirely- The method name is specified as a string literal
- Multiple modifiers can be applied to the same method
- Modifiers are registered at runtime and dispatched automatically
beforeandaftermodifiers cannot change the method's return value
Complete Moose-Style Example
Here is a full example combining has, extends, and method modifiers:
package Animal;
has ro str $species (required);
has rw int $energy = 100;
func speak(scalar $self) void {
say($self->species() . " (energy: " . $self->energy() . ")");
}
package Dog;
extends Animal;
has ro str $name (required);
has rw int $age = 0;
has rw str $nickname;
before "bark" func(scalar $self) void {
say("[preparing to bark]");
}
func bark(scalar $self) void {
say($self->name() . " barks!");
}
after "bark" func(scalar $self) void {
say("[done barking]");
}
package main;
func main() int {
my scalar $d = Dog::new("name", "Rex", "species", "dog", "age", 3);
say($d->name()); # "Rex"
say($d->age()); # 3
$d->set_age(4); # setter (rw)
say($d->age()); # 4
say($d->energy()); # 100 (inherited)
$d->set_energy(80); # inherited setter
say($d->energy()); # 80
$d->speak(); # "dog (energy: 80)" - inherited method
$d->bark(); # "[preparing to bark]" / "Rex barks!" / "[done barking]"
say($d->isa("Dog")); # 1
say($d->isa("Animal")); # 1
return 0;
}
Strada supports two OOP approaches that can coexist in the same program:
- Manual OOP (shown earlier on this page): Write your own constructors, getters, and setters. Use
bless()directly. Full control over object construction. - Moose-style OOP: Use
has,extends,with, and method modifiers for a declarative approach. The compiler generates the boilerplate. Best for typical class hierarchies.
Both styles produce the same blessed hash references at runtime. You can mix and match freely.
Best Practices
- Always use
packageto define classes - Name constructors
newby convention - First parameter of methods should be
scalar $self - Store instance data in the blessed hash
- Use
hasfor simple attribute declarations with auto-generated accessors - Use getters/setters for controlled access to properties
- Return
$selffrom setters for method chaining - Use
before/aftermodifiers for cross-cutting concerns (logging, validation) - Use
core::weaken()on back-references (child→parent) to prevent circular reference leaks