Object-Oriented Programming

Classes, objects, methods, and inheritance in Strada.

Overview

Strada's OOP model is inspired by Perl:

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()
Auto-Prefix Rules
  • Only applies to simple packages (no :: in name)
  • Skips main function and package main;
  • Skips functions that already look class-prefixed (e.g., Dog_speak)
  • extern functions 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;
}
Package Call Syntax
  • ::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";
}
AUTOLOAD Details
  • $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)
Dynamic Dispatch Details
  • 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;
}
Supported Operators
  • Arithmetic: +, -, *, /, %, **
  • String: . (concatenation), "" (stringify)
  • Unary: neg (unary minus), !, bool
  • Comparison: ==, !=, <, >, <=, >=, <=>
  • String comparison: eq, ne, lt, gt, le, ge, cmp
Handler Signatures
  • 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 Syntax

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]");
}
Method Modifier Details
  • before "method" func($self) void { ... } — runs before the method
  • after "method" func($self) void { ... } — runs after the method
  • around "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
  • before and after modifiers 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;
}
Manual vs. Moose-Style OOP

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

OOP Tips
  • Always use package to define classes
  • Name constructors new by convention
  • First parameter of methods should be scalar $self
  • Store instance data in the blessed hash
  • Use has for simple attribute declarations with auto-generated accessors
  • Use getters/setters for controlled access to properties
  • Return $self from setters for method chaining
  • Use before/after modifiers for cross-cutting concerns (logging, validation)
  • Use core::weaken() on back-references (child→parent) to prevent circular reference leaks