Composition
Instead of inheritance hierarchies, Coco uses composition—you build complex types by embedding simpler ones. It's more flexible and easier to understand.
Embedding Types
Embed a type by including it without a field name:
type Animal {
name string
age i32
fn speak {
echo "Some sound";
}
}
type Dog {
Animal // Embedded
breed string
}
$dog = Dog {
Animal: Animal { name: "Rex", age: 3 },
breed: "Labrador"
};
// Access embedded fields directly
echo $dog.name; // Rex
echo $dog.age; // 3
echo $dog.breed; // Labrador
The embedded type's fields become accessible directly on the outer type.
Method Forwarding
Methods from embedded types are available on the outer type:
type Animal {
name string
age i32
fn introduce {
echo "I'm {$this.name} and I'm {$this.age} years old";
}
}
$dog = Dog {
Animal: Animal { name: "Rex", age: 3 },
breed: "Labrador"
};
$dog.introduce(); // I'm Rex and I'm 3 years old
Overriding Methods
Define a method with the same name to override:
type Animal {
name string
fn speak {
echo "Some animal sound";
}
}
type Dog {
Animal
breed string
fn speak {
echo "Woof! I'm {$this.name}";
}
}
$dog = Dog { Animal: Animal { name: "Rex" }, breed: "Labrador" };
$dog.speak(); // Woof! I'm Rex
To call the embedded type's method:
type Dog {
Animal
breed string
fn speak_twice {
$this.Animal.speak(); // Call Animal's speak
$this.speak(); // Call Dog's speak
}
}
Multiple Embedding
Embed multiple types:
type Named {
name string
}
type Aged {
age i32
}
type Person {
Named
Aged
occupation string
}
$person = Person {
Named: Named { name: "Alice" },
Aged: Aged { age: 30 },
occupation: "Engineer"
};
echo $person.name; // Alice
echo $person.age; // 30
echo $person.occupation; // Engineer
Handling Conflicts
If embedded types have fields with the same name, you must access them explicitly:
type A {
value i32
}
type B {
value string
}
type Both {
A
B
}
$both = Both {
A: A { value: 42 },
B: B { value: "hello" }
};
// $both.value would be ambiguous
echo $both.A.value; // 42
echo $both.B.value; // hello
Composition vs Inheritance
Why composition over inheritance?
Flexibility. You can compose types in any combination. Inheritance locks you into a hierarchy.
// With inheritance, you'd need:
// Animal -> Mammal -> Dog
// Animal -> Bird -> Penguin
// But what about a Platypus? It's a mammal that lays eggs.
// With composition:
type Platypus {
Mammal
EggLayer
}
Explicit relationships. You always know where fields and methods come from.
No diamond problem. Multiple embedding is straightforward—conflicts are explicit.
Easy refactoring. You can change composition without breaking a class hierarchy.
Delegation Pattern
When you want to forward specific methods rather than all of them:
type Logger {
prefix string
fn log($message string) {
echo "[{$this.prefix}] {$message}";
}
}
type Service {
logger Logger
// other fields
fn log($message string) {
$this.logger.log($message); // Delegate to logger
}
}
$service = Service {
logger: Logger { prefix: "Service" }
};
$service.log("Started"); // [Service] Started
This gives you control over what's exposed.
Has-A vs Is-A
Composition represents "has-a" relationships:
type Car {
engine Engine // Car has an engine
wheels [4]Wheel // Car has wheels
}
Use traits for "is-a" or "can-do" relationships:
trait Drivable {
fn drive;
fn stop;
}
type Car {
engine Engine
wheels [4]Wheel
@Drivable
fn drive {
$this.engine.start();
// ...
}
@Drivable
fn stop {
// ...
}
}
Building Complex Types
Real-world example:
type Timestamps {
created_at DateTime
updated_at DateTime
}
type SoftDelete {
deleted_at ?DateTime
fn is_deleted: bool {
return $this.deleted_at != null;
}
fn delete {
$this.deleted_at = Some(DateTime::now());
}
}
type User {
Timestamps
SoftDelete
id i32
name string
email string
}
// User has all the fields and methods from both
$user = User {
Timestamps: Timestamps {
created_at: DateTime::now(),
updated_at: DateTime::now()
},
SoftDelete: SoftDelete { deleted_at: null },
id: 1,
name: "Alice",
email: "alice@example.com"
};
$user.delete();
echo $user.is_deleted(); // true
Constructor Patterns
For composed types, constructors often create embedded parts:
type User {
Timestamps
SoftDelete
id i32
name string
email string
fn new($name string, $email string): User {
$now = DateTime::now();
return User {
Timestamps: Timestamps {
created_at: $now,
updated_at: $now
},
SoftDelete: SoftDelete { deleted_at: null },
id: generate_id(),
name: $name,
email: $email
};
}
}
$user = User::new("Alice", "alice@example.com");
Best Practices
Compose for reuse. If multiple types need the same fields and behavior, extract them into an embeddable type.
Keep embedded types focused. Timestamps should just be timestamps, not timestamps plus audit logging plus versioning.
Be explicit about delegation. Don't embed just to get all methods forwarded—embed when the relationship is meaningful.
Use traits for behavior contracts. Embedding is for implementation reuse. Traits are for shared interfaces.
Name embedded types clearly. Timestamps, SoftDelete, Auditable—the name should indicate what behavior it adds.