Object-Oriented Programming

Coco uses a composition-focused approach to OOP. Types, traits, and methods inside type definitions give you all the tools you need. Use them when they make sense, skip them when they don't.

Types and Methods

Types are the building blocks. They group data and methods together:

type Point {
    x i32
    y i32

    fn distance: f64 {
        $dx = $this.x as f64;
        $dy = $this.y as f64;
        return ($dx  $dx + $dy  $dy).sqrt();
    }
}

$p = Point { x: 3, y: 4 };
echo $p.distance();  // 5.0

Characteristics:

  • Value semantics — copying copies the data
  • Composition via embedding — include other types
  • Methods inside the type block
  • Best for all data modeling

Composition Example

Types can embed other types for composition:

type Animal {
    name string
    age i32

    fn new($name string, $age i32): Animal {
        return Animal { name: $name, age: $age };
    }

    fn speak {
        echo "Some generic animal sound";
    }

    fn get_name: string {
        return $this.name;
    }
}

Characteristics:

  • Clean data grouping
  • Methods defined inside the type
  • Composition through embedding
  • Best for all types of objects

Methods and $this

Methods are functions associated with a type. Inside instance methods, $this refers to the current object.

Instance Methods

type Counter {
    count i32

    fn new(): Counter {
        return Counter { count: 0 };
    }

    // Immutable $this — can read but not modify
    fn get: i32 {
        return $this.count;
    }

    // Mutable $this — can modify fields
    fn increment {
        $this.count += 1;
    }

    // Takes ownership of $this (consumes the object)
    fn destroy() {
        echo "Counter destroyed with count: {$this.count}";
    }
}

$c = Counter::new();
$c.increment();
echo $c.get();  // 1

The $this Reference

Inside instance methods, $this automatically refers to the current object. The method can:

Usage Meaning Use Case
Borrow Read or modify fields Accessing/modifying fields via $this
Consume Take ownership Consuming/destroying the object

Note: The compiler detects whether you mutate through $this and enforces exclusive access automatically.

Static Methods

Methods without $this are static (called on the type, not an instance):

type Math {
    fn add($a i32, $b i32): i32 {
        return $a + $b;
    }
}

$sum = Math::add(5, 3);  // No instance needed

Constructors

Constructors are just static methods that return an instance. Convention is to name them new:

type Person {
    name string
    age i32

    // Primary constructor
    fn new($name string, $age i32): Person {
        return Person { name: $name, age: $age };
    }

    // Alternative constructor
    fn with_name($name string): Person {
        return Person { name: $name, age: 0 };
    }
}

$p1 = Person::new("Alice", 30);
$p2 = Person::with_name("Bob");

No special constructor syntax — just static methods that return instances. Simple and explicit.

Composition

Coco uses composition instead of inheritance. Types embed other types to reuse behavior.

Basic Composition

type Animal {
    name string

    fn new($name string): Animal {
        return Animal { name: $name };
    }

    fn speak {
        echo "Some sound";
    }

    fn get_name: string {
        return $this.name;
    }
}

type Dog {
    Animal        // Embed Animal
    breed string

    fn new($name string, $breed string): Dog {
        return Dog {
            Animal: Animal::new($name),
            breed: $breed
        };
    }

    // Override the embedded type's method
    fn speak {
        echo "Woof! I'm {$this.name}";
    }

    // New method specific to Dog
    fn get_breed: string {
        return $this.breed;
    }
}

$dog = Dog::new("Rex", "Labrador");
$dog.speak();  // "Woof! I'm Rex"
echo $dog.get_name();  // "Rex" (from embedded Animal)

Method Overriding

Types with embedded types can override methods by defining a method with the same name:

type Shape {
    id i32

    fn area: f64 {
        return 0.0;  // Default implementation
    }
}

type Circle {
    Shape
    radius f64

    fn new($radius f64): Circle {
        return Circle { Shape: Shape { id: 0 }, radius: $radius };
    }

    // Overrides Shape::area
    fn area: f64 {
        return 3.14159  $this.radius  $this.radius;
    }
}

Accessing Embedded Type Methods

Access the embedded type's methods explicitly when needed:

type Employee {
    Person
    employee_id i32

    fn new($name string, $age i32, $id i32): Employee {
        return Employee {
            Person: Person::new($name, $age),
            employee_id: $id
        };
    }

    fn display {
        // Access embedded Person's fields
        echo "Name: {$this.name}, Age: {$this.age}";
        echo "Employee ID: {$this.employee_id}";
    }
}

Encapsulation

Control access through method design. Fields are public by default, but you control the API through methods:

type BankAccount {
    balance f64
    account_id i32
    owner string

    fn new($owner string): BankAccount {
        return BankAccount {
            balance: 0.0,
            account_id: generate_id(),
            owner: $owner
        };
    }

    fn deposit($amount f64) {
        $this.balance += $amount;
    }

    fn get_balance: f64 {
        return $this.balance;  // Controlled access through method
    }
}

$account = BankAccount::new("Alice");
echo $account.owner;        // Direct field access
$account.deposit(100.0);    // Method call
echo $account.get_balance();  // Controlled access

Note: Fields are accessible directly, but convention is to use methods for controlled access and validation.

Polymorphism

Use traits for polymorphism:

trait Speaker {
    fn speak;
}

type Dog {
    name string

    fn new($name string): Dog {
        return Dog { name: $name };
    }

    @Speaker
    fn speak {
        echo "Woof!";
    }
}

type Cat {
    name string

    fn new($name string): Cat {
        return Cat { name: $name };
    }

    @Speaker
    fn speak {
        echo "Meow!";
    }
}

fn make_speak<T: Speaker>($animal &T) {
    $animal.speak();
}

$dog = Dog::new("Rex");
$cat = Cat::new("Whiskers");

make_speak(&$dog);  // "Woof!"
make_speak(&$cat);  // "Meow!"

Compile-time dispatch — generic functions are monomorphized for each type.

Composition vs Traits

When should you use composition vs traits?

Composition: Embedding Types

type Vehicle {
    wheels i32
}

type Car {
    Vehicle
    doors i32
}

Use when:

  • You want to include another type's data
  • You want to reuse or override methods
  • There's a clear "has-a" relationship

Traits: "Can-do" Capabilities

trait Drawable {
    fn draw;
}

type Circle {
    radius f64

    @Drawable
    fn draw {
        echo "Drawing circle";
    }
}

type Square {
    side f64

    @Drawable
    fn draw {
        echo "Drawing square";
    }
}

Use when:

  • Types share behavior but not data
  • Multiple unrelated types need the same capability
  • You want generic functions over multiple types

Combining Both

Use both composition and traits together:

type FlyingCar {
    car Car
    plane Plane

    fn drive {
        $this.car.drive();
    }

    fn fly {
        $this.plane.fly();
    }
}

Benefits:

  • Flexible — compose any behaviors
  • No diamond problem
  • Easier to test and maintain

Traits in Detail

Traits provide shared behavior across types without inheritance. They're perfect for composition and code reuse.

Basic Trait Implementation

Types implement trait methods using the @TraitName annotation:

trait Drawable {
    fn draw;
}

trait Describable {
    fn describe: string;
}

type Dog {
    name string
    age i32

    fn new($name string, $age i32): Dog {
        return Dog { name: $name, age: $age };
    }

    // Implementing Drawable
    @Drawable
    fn draw {
        echo "Drawing dog: {$this.name}";
    }

    // Implementing Describable
    @Describable
    fn describe: string {
        return "A dog named {$this.name} who is {$this.age} years old";
    }
}

The compiler verifies that all trait methods are implemented and match the trait signatures.

Default Trait Implementations

Traits can provide default implementations. Types use them automatically unless they override:

trait Describable {
    fn describe: string {
        return "A describable object";  // Default implementation
    }

    fn print_description {
        echo $this.describe();
    }
}

type Cat {
    name string

    fn new($name string): Cat {
        return Cat { name: $name };
    }
}

// NOT implementing describe() - will use default
// print_description() also available via default

type Bird {
    species string

    fn new($species string): Bird {
        return Bird { species: $species };
    }

    // Override the default
    @Describable
    fn describe: string {
        return "A {$this.species} bird";
    }
}

// print_description() still available via default implementation

$cat = Cat::new("Whiskers");
echo $cat.describe();  // "A describable object" (default)
$cat.print_description();  // "A describable object"

$bird = Bird::new("Eagle");
echo $bird.describe();  // "A Eagle bird" (overridden)
$bird.print_description();  // "A Eagle bird"

Rules:

  • If you don't implement a trait method, the default is used (if one exists)
  • You can override defaults by implementing the method
  • If no default exists, you must implement the method

Method Conflict Resolution

When two traits define methods with the same name, implement each separately:

trait Renderable {
    fn render;
}

trait Displayable {
    fn render;
}

type Widget {
    id i32
    label string

    fn new($id i32, $label string): Widget {
        return Widget { id: $id, label: $label };
    }

    // Each trait's render() is implemented separately
    @Renderable
    fn render {
        echo "Rendering widget #{$this.id}";
    }

    @Displayable
    fn render {
        echo "Displaying widget: {$this.label}";
    }
}

// Calling ambiguous methods requires qualification
$widget = Widget::new(42, "Button");

// $widget.render();  // ERROR: Ambiguous - which render()?

// Use qualified syntax
Renderable::render($widget);   // "Rendering widget #42"
Displayable::render($widget);  // "Displaying widget: Button"

If methods don't conflict (different names), no special syntax needed:

trait Drawable {
    fn draw;
}

trait Clickable {
    fn on_click;
}

type Button {
    label string

    fn new($label string): Button {
        return Button { label: $label };
    }

    @Drawable
    fn draw {
        echo "Drawing button";
    }

    @Clickable
    fn on_click {
        echo "Button clicked";
    }
}

$btn = Button::new("Submit");
$btn.draw();      // No ambiguity
$btn.on_click();  // No ambiguity

Trait Inheritance

Traits can require other traits. Types implementing the child trait automatically satisfy the parent trait:

trait Comparable {
    fn compare($other &Self): i32;
}

trait Sortable requires Comparable {
    fn sort($items &[Self]) {
        // Default implementation using compare()
        $n = $items.len();
        for $i in 0..$n {
            for $j in 0..($n - $i - 1) {
                if $items[$j].compare(&$items[$j + 1]) > 0 {
                    $temp = $items[$j];
                    $items[$j] = $items[$j + 1];
                    $items[$j + 1] = $temp;
                }
            }
        }
    }
}

type Wolf {
    name string
    pack_rank i32

    fn new($name string, $pack_rank i32): Wolf {
        return Wolf { name: $name, pack_rank: $pack_rank };
    }

    // Must implement compare() to satisfy Comparable
    // (which is required by Sortable)
    @Comparable
    fn compare($other &Wolf): i32 {
        return $this.pack_rank - $other.pack_rank;
    }
}

// sort() is provided by Sortable's default implementation

// Usage
$wolves = [
    Wolf::new("Alpha", 1),
    Wolf::new("Beta", 2),
    Wolf::new("Omega", 5),
];
Wolf::sort(&$wolves);  // Uses default sort() which calls compare()

Key points:

  • You don't need to write implements Sortable, Comparable — just implements Sortable
  • You must implement methods from required traits (unless they have defaults)
  • This creates a trait hierarchy without code duplication

Using Traits with Encapsulation

You can provide a public API that uses trait methods internally:

trait Comparable {
    fn compare($other &Self): i32;
}

type InternalCache {
    data Vec<string>
    priority i32

    fn new($priority i32): InternalCache {
        return InternalCache { data: Vec::new(), priority: $priority };
    }

    // Implement the trait
    @Comparable
    fn compare($other &InternalCache): i32 {
        return $this.priority - $other.priority;
    }

    // Public API wraps the trait method
    fn is_higher_priority($other &InternalCache): bool {
        return $this.compare($other) > 0;
    }
}

$cache1 = InternalCache::new(10);
$cache2 = InternalCache::new(5);
echo $cache1.is_higher_priority(&$cache2);  // true

Why this pattern?

  • Clear API — users see meaningful method names
  • Implementation flexibility — can change how comparison works
  • The trait provides the contract for generic code

Traits Example

All types use the @TraitName annotation for trait implementation:

trait Area {
    fn area: f64;
}

type Rectangle {
    width f64
    height f64

    @Area
    fn area: f64 {
        return $this.width * $this.height;
    }
}

type Circle {
    radius f64

    @Area
    fn area: f64 {
        return 3.14159  $this.radius  $this.radius;
    }
}

Consistent syntax:

  • All types use @TraitName annotation
  • Default implementations work automatically
  • Conflict resolution uses qualified calls

Generic Traits

Traits can have type parameters:

trait Container<T> {
    fn add($item T);
    fn get($index usize): Option<T>;
}

type Box<T> {
    items Vec<T>

    fn new<T>(): Box<T> {
        return Box { items: Vec::new() };
    }

    @Container
    fn add<T>($item T) {
        $this.items.push($item);
    }

    @Container
    fn get<T>($index usize): Option<T> {
        if $index < $this.items.len() {
            return Some($this.items[$index]);
        }
        return None;
    }
}

// Usage
$int_box = Box::<i32>::new();
$int_box.add(42);
$int_box.add(100);

$str_box = Box::<string>::new();
$str_box.add("hello");

Complete Example: Trait Composition

Here's a comprehensive example showing multiple traits working together:

trait Drawable {
    fn draw;
}

trait Describable {
    fn describe: string {
        return "A describable object";
    }
}

trait Comparable {
    fn compare($other &Self): i32;
}

trait Sortable requires Comparable {
    fn sort($items &[Self]) {
        // Default bubble sort using compare()
        $n = $items.len();
        for $i in 0..$n {
            for $j in 0..($n - $i - 1) {
                if $items[$j].compare(&$items[$j + 1]) > 0 {
                    $temp = $items[$j];
                    $items[$j] = $items[$j + 1];
                    $items[$j + 1] = $temp;
                }
            }
        }
    }
}

type Card {
    suit string
    rank i32

    fn new($suit string, $rank i32): Card {
        return Card { suit: $suit, rank: $rank };
    }

    @Drawable
    fn draw {
        echo "Drawing {$this.rank} of {$this.suit}";
    }

    @Describable
    fn describe: string {
        return "{$this.rank} of {$this.suit}";
    }

    @Comparable
    fn compare($other &Card): i32 {
        return $this.rank - $other.rank;
    }
}

// sort() available via Sortable's default implementation

// Usage
$cards = [
    Card::new("Hearts", 10),
    Card::new("Spades", 3),
    Card::new("Diamonds", 7),
];

for $card in &$cards {
    $card.draw();
}

Card::sort(&$cards);  // Sort by rank

echo "After sorting:";
for $card in &$cards {
    echo $card.describe();
}

Best Practices

Use Composition

Prefer composition over deep nesting:

// Good: Composition
type Animal {
    name string
}

type Cat {
    Animal
    diet Diet        // Composition
    habitat Habitat  // Composition
}

Guideline: Use embedding for "is-a" and fields for "has-a" relationships.

Favor Small, Focused Types

// Good: Single responsibility
type User {
    name string
    email string

    fn validate: bool { / ... / }
}

type UserRepository {
    db Database

    fn save($user &User) { / ... / }
    fn find($id i32): Option<User> { / ... / }
}

// Bad: Too many responsibilities in one type
// Keep data types separate from service types

Control Your API

Expose a clean interface through methods:

type Stack<T> {
    items Vec<T>  // Fields accessible but...

    // ...users should use methods
    fn push<T>($item T) { / ... / }
    fn pop<T>(): Option<T> { / ... / }
}

// Convention: access through methods, not direct field access

Prefer Traits for Shared Behavior

// Good: Trait for shared capability
trait Serializable {
    fn to_json: string;
}

type User {
    name string
    id i32

    @Serializable
    fn to_json: string {
        return "{\"name\": \"{$this.name}\", \"id\": {$this.id}}";
    }
}

type Product {
    name string
    price f64

    @Serializable
    fn to_json: string {
        return "{\"name\": \"{$this.name}\", \"price\": {$this.price}}";
    }
}

The OOP Toolkit

Coco gives you a modern OOP toolkit:

  • Types — group data and behavior
  • Composition — embed types for reuse
  • Traits — shared behavior across types
  • Polymorphism — generic functions over trait bounds
  • @TraitName annotations — clean trait implementation

Use them when they make your code clearer. Skip them when simple types and functions work better.

OOP is a tool, not a religion. Use it where it fits.

Copyright (c) 2025 Ocean Softworks, Sharkk