Invisible link to canonical for Microformats

ADR-020 Structs as Schema-Backed Immutable Records


Status

Accepted

Context

Slug currently represents structured domain data primarily using maps and conventions. Maps are excellent for dynamic/user-driven data (e.g., JSON, HTTP headers), but they are too flexible for long-lived “shaped” values used across modules and APIs. This leads to:

  • weak typo resistance (string keys)
  • lack of enforced shape (unknown/missing fields slip through)
  • noisy runtime checks spread across code
  • heavier runtime cost for repeated field access and matching

Slug values are immutable and Slug emphasizes explicitness and easy reasoning. We need a first-class data construct that provides:

  • a fixed, explicit field set (“shape”)
  • ergonomic construction, access, update, and matching
  • optional runtime validation without requiring a static type system
  • predictable module scoping (import/export) using existing val/var

Decision

Introduce Struct Schemas and Struct Values as first-class runtime concepts in Slug.

1. Definition

Hard rule: struct { ... } may only appear on the RHS of a val or var binding.

val User = struct {
  name,
  @num age,
  active = true,
}

This creates a struct schema value bound to User.

Field declaration rules

  • Each declared field either:

    • has a default expression, or
    • defaults to nil
  • No other implicit behavior.
  • Field order is fixed by the schema definition.
  • Field names are schema-defined identifiers (treated as symbol keys conceptually).

Type hint set (runtime validation)

Struct fields may include runtime validation hints. The initial supported hints are:

  • @num
  • @str
  • @bool
  • @bytes
  • @list
  • @map
  • @fn
  • @handle

Semantics:

  • A hinted field value must be of the hinted runtime type or nil.
  • No coercion is performed.
  • Violations raise a runtime error with a clear message indicating:

    • struct type name
    • field name
    • expected type hint
    • actual value type

Example:

val User = struct {
  @num age,
}

val u1 = User { age: 42 }     // ok
val u2 = User { age: nil }    // ok
val u3 = User { age: "42" }   // error

2. Construction

Struct values are constructed by applying a schema to a field-initializer block:

var u = User {
  name: "Slug",
  age: 42,
  active: true,
}

Construction rules:

  • Unknown fields are an error (typo resistance).
  • Missing fields are allowed and become:

    • the field default expression if present, else
    • nil
  • Duplicate fields are an error.
  • Trailing commas are allowed.
  • After construction, all hinted fields are validated.

3. Access

  • . on structs is field access.
  • Accessing a field not declared in the schema is an error.
u.name
u.age

4. Copy / Update

Single canonical operator: copy

u = u copy { age: 43 }
u = u copy { }

Semantics:

  • Produces a new struct value with the same schema.
  • The initializer block sets/overrides the listed fields only.
  • Unknown fields are an error.
  • Trailing commas are allowed.
  • Empty copy { } is a valid clone.
  • After copy, all hinted fields are validated.

5. Pattern Matching

Struct patterns support partial matching by default: fields not specified in the pattern are implicitly ignored.

match res {
  Response { status: 200, body } -> ok(body)
  Response { status, body }      -> error(status)
}

Rules:

  • A struct pattern must reference a struct schema name (e.g., Response).
  • Unknown fields in a pattern are an error (typo resistance).
  • Field forms:

    • field binds the field value to a local name of the same identifier
    • field: <pattern-or-literal> matches against a value/pattern
  • _ is supported but optional; the following are equivalent:
Response { status: 200, body: _ } -> ...
Response { status: 200 }          -> ...

6. Scoping, imports, exports

Struct schemas are values bound by val or var, so they follow standard Slug rules:

  • they can be defined in modules
  • imported/exported explicitly
  • shadowed in nested scopes (as permitted for other bindings)

Consequences

Positive

  • Clear, explicit “shaped” data without introducing OO features (no methods, no inheritance).
  • Strong typo resistance via unknown-field errors in construction, copy, and match.
  • Ergonomic immutable update via copy.
  • Runtime validation via type hints increases safety without adding a static type system.
  • Pattern matching becomes expressive and concise for domain/state handling.
  • Struct schemas being ordinary values preserves Slug’s simplicity and scoping model.

Negative

  • Adds new runtime concepts (schema + struct value) and new syntax forms to implement.
  • Runtime validation introduces overhead (mitigated by only validating hinted fields; still required at construct/copy).
  • Requires careful error messaging to keep failures understandable.

Neutral

  • Does not change Slug’s immutability model; structs follow existing “new value on change” semantics.
  • Map semantics remain unchanged; maps still serve dynamic/string-key use-cases (e.g., user data, JSON).
  • Future enhancements (e.g., additional hints like @required, serialization conventions, layout optimizations) can be layered without changing the core model.