Constructors
Coco offers two ways to create instances: static methods (explicit) and magic constructors (implicit). Both are valid—use whichever fits your needs.
Static Method: fn new()
By convention, types have a new static method:
type User {
id i32
name string
email string
created_at DateTime
fn new($name string, $email string): User {
return User {
id: generate_id(),
name: $name,
email: $email,
created_at: DateTime::now()
};
}
}
// Usage
$user = User::new("Alice", "alice@example.com");
The fn new() method can:
- Set default values
- Validate inputs
- Compute derived fields
- Do any setup work needed
Magic Constructor: new
For implicit construction, use a magic new method (no fn keyword):
type User {
id i32
name string
email string
created_at DateTime
// Magic constructor - no fn keyword
new($name string, $email string) {
$this.id = generate_id();
$this.name = $name;
$this.email = $email;
$this.created_at = DateTime::now();
}
}
// Implicit construction - calls magic new()
$user = User("Alice", "alice@example.com");
The magic new assigns directly to $this instead of returning a value. It's called implicitly when you use the type name as a function.
When to Use Which
| Pattern | Syntax | Use When |
|---|---|---|
fn new() |
User::new(...) |
You want explicit factory-style calls |
new |
User(...) |
You want implicit, class-like construction |
Both can coexist—use fn new() for explicit factories and magic new for the primary constructor.
Multiple Constructors
Unlike languages with constructor overloading, you just use different method names:
type User {
id i32
name string
email string
created_at DateTime
fn new($name string, $email string): User {
// Full constructor
}
fn guest(): User {
// Positional form - all fields in order
return User { 0, "Guest", "", DateTime::now() };
}
fn from_id($id i32): !User {
// Load from database
return database.fetch_user($id);
}
}
// Clear and explicit
$regular = User::new("Alice", "alice@example.com");
$guest = User::guest();
$loaded = User::from_id(42);
This is more readable than trying to distinguish constructors by parameter types.
Validation in Constructors
Return an error union for constructors that can fail:
type Email {
value string
fn new($value string): !Email {
if !$value.contains("@") {
return error("Invalid email: {}", $value);
}
return Email { value: $value };
}
}
type User {
id i32
name string
email Email
created_at DateTime
fn new($name string, $email string): !User {
if $name.is_empty() {
return error("Name cannot be empty");
}
$email_obj = Email::new($email); // Propagates if invalid
return User {
id: generate_id(),
name: $name,
email: $email_obj,
created_at: DateTime::now()
};
}
}
Now invalid objects can't exist—they fail at construction time.
Default Values
For types with many optional fields, use the Default trait:
type Config {
host string
port i32
timeout i32
retries i32
debug bool
@Default
fn default(): Config {
return Config {
host: "localhost",
port: 8080,
timeout: 30,
retries: 3,
debug: false
};
}
}
// Use defaults
$config = Config::default();
// Override specific fields
$config = Config {
port: 3000,
..Config::default()
};
Builder Pattern
For complex construction with many options:
type RequestBuilder {
url string
method string
headers HashMap<string, string>
body ?string
timeout ?i32
fn new($url string): RequestBuilder {
return RequestBuilder {
url: $url,
method: "GET",
headers: HashMap::new(),
body: null,
timeout: null
};
}
fn method($this RequestBuilder, $method string): RequestBuilder {
$this.method = $method;
return $this;
}
fn header($this RequestBuilder, $key string, $value string): RequestBuilder {
$this.headers.insert($key, $value);
return $this;
}
fn body($this RequestBuilder, $body string): RequestBuilder {
$this.body = Some($body);
return $this;
}
fn timeout($this RequestBuilder, $secs i32): RequestBuilder {
$this.timeout = Some($secs);
return $this;
}
fn build($this RequestBuilder): !Request {
return Request::from_builder($this);
}
}
// Usage - clear and chainable
$request = RequestBuilder::new("https://api.example.com")
.method("POST")
.header("Content-Type", "application/json")
.body("{\"name\": \"Alice\"}")
.timeout(30)
.build();
Factory Functions
Sometimes construction logic doesn't belong in the type itself:
// In a separate module
fn create_production_database(): !Database {
$config = load_config("prod.toml");
return Database::connect($config);
}
fn create_test_database(): Database {
return Database::in_memory();
}
Use factory functions when:
- Construction depends on external configuration
- You need to choose between different implementations
- The creation logic is complex enough to warrant its own function
Copy and Clone
For creating copies of existing instances:
type Point {
x i32
y i32
}
// If Point is Copy (small, stack-allocated)
$p1 = Point { x: 1, y: 2 };
$p2 = $p1; // Automatic copy
// For Clone types (heap-allocated)
type User {
name string
data Vec<u8>
@Clone
fn clone(&$this): User {
return User {
name: $this.name.clone(),
data: $this.data.clone()
};
}
}
$u1 = User::new("Alice");
$u2 = $u1.clone(); // Explicit clone
Converting Between Types
Use from and into conventions:
type User {
id i32
name string
email string
created_at DateTime
fn from_record($record DatabaseRecord): User {
return User {
id: $record.get_int("id"),
name: $record.get_string("name"),
email: $record.get_string("email"),
created_at: $record.get_datetime("created_at")
};
}
fn into_json(&$this): JsonValue {
return json!({
"id": $this.id,
"name": $this.name,
"email": $this.email
});
}
}
Initialization Lists
For types where you want to enforce all fields being set:
type Config {
required_field string
another_required i32
}
// No default, no new - must use struct literal
$config = Config {
required_field: "value",
another_required: 42
};
This makes it a compile error to forget a field.
Best Practices
Use new for the primary constructor. It's conventional and expected.
Name alternative constructors clearly. from_json, with_defaults, empty—the name should indicate what kind of instance you get.
Validate early. Check invariants in the constructor. An invalid object shouldn't exist.
Prefer builder for 4+ optional parameters. It's more readable than positional arguments.
Make invalid states unrepresentable. Use types that can't hold invalid data:
// Bad - email could be invalid
type User {
email string
}
// Good - email is always valid
type Email {
value string
} // Only created through validating constructor
type User {
email Email
}