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— justimplements 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
@TraitNameannotation - 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.