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
}
Copyright (c) 2025 Ocean Softworks, Sharkk