Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Struct Types

A struct defines a named data type with declared fields, optional type annotations, and optional default values. Structs are the primary mechanism for defining domain-specific types in Forge.

Definition

Structs are defined with the struct keyword (classic syntax) or the thing keyword (natural syntax). Both forms are equivalent.

StructDef → ( struct | thing ) Identifier { FieldList }

FieldList → ( Field ( , Field )* ,? )?

FieldIdentifier : TypeAnnotation ( = Expression )? | has Identifier : TypeAnnotation

// Classic syntax
struct Point {
    x: Int,
    y: Int
}

// Natural syntax — identical result
thing Point {
    x: Int,
    y: Int
}

Field type annotations are part of the struct definition syntax. Each field has a name, a colon, and a type name.

Default Values

Fields may have default values. If a field with a default is omitted during construction, the default value is used:

thing Config {
    host: String = "localhost",
    port: Int = 8080,
    debug: Bool = false
}

set cfg to craft Config {}
say cfg.host   // localhost
say cfg.port   // 8080
say cfg.debug  // false

Fields without defaults are required — omitting them during construction produces a runtime error.

set prod to craft Config { host: "api.example.com", port: 443 }
say prod.host   // api.example.com
say prod.debug  // false (default)

Construction

There are two ways to create a struct instance:

Direct Construction (Classic)

Use the type name followed by a field initializer block:

let p = Point { x: 3, y: 4 }

Craft Construction (Natural)

Use the craft keyword:

set p to craft Point { x: 3, y: 4 }

Both forms produce the same value: an object with the declared fields plus a __type__ field set to the struct name.

Field Access

Fields are accessed with dot notation:

let p = Point { x: 3, y: 4 }
say p.x  // 3
say p.y  // 4

The __type__ Field

Every struct instance has an internal __type__ field containing the struct name as a string. This field is set automatically during construction and is used by the runtime for method dispatch, interface satisfaction checking, and typeof():

let p = Point { x: 3, y: 4 }
say typeof(p)  // Point

Methods

Methods are attached to a struct using give (natural) or impl (classic) blocks. See Method Blocks for full details.

give Point {
    define distance(it) {
        return math.sqrt(it.x * it.x + it.y * it.y)
    }
}

let p = Point { x: 3, y: 4 }
say p.distance()  // 5.0

The first parameter it is the receiver. When p.distance() is called, p is automatically bound to it.

Static Methods

If a method’s first parameter is not it, it is a static method called on the type rather than on an instance:

give Person {
    define infant(name) {
        return craft Person { name: name, age: 0 }
    }
}

set baby to Person.infant("Bob")

Multiple give Blocks

Multiple give blocks for the same type are permitted. Methods accumulate across all blocks:

give Person {
    define greet(it) {
        return "Hi, I'm " + it.name
    }
}

give Person {
    define birthday(it) {
        return craft Person { name: it.name, age: it.age + 1 }
    }
}

Composition with has

The has keyword inside a struct body embeds one type within another, enabling field and method delegation:

thing Address {
    street: String,
    city: String
}

thing Employee {
    name: String,
    has addr: Address
}

The has keyword provides two delegation mechanisms:

  1. Field delegation. Accessing a field that does not exist on the outer type delegates to the embedded type. emp.city resolves to emp.addr.city.

  2. Method delegation. Calling a method that does not exist on the outer type delegates to the embedded type. emp.full() resolves to emp.addr.full().

give Address {
    define full(it) {
        return it.street + ", " + it.city
    }
}

set emp to craft Employee {
    name: "Charlie",
    addr: craft Address { street: "123 Main St", city: "Portland" }
}

say emp.city     // Portland (delegated)
say emp.full()   // 123 Main St, Portland (delegated)

The explicit path (emp.addr.city) also works and produces the same result.

Struct vs. Object

FeatureObjectStruct
Type identityNone (generic Object)Named (__type__ field)
Field declarationsNoYes, with type annotations
Default valuesNoYes
MethodsNoYes (via give/impl)
Interface satisfactionNoYes (via give...the power)
Construction{ key: value }Name { } or craft Name { }