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:
fieldbinds the field value to a local name of the same identifierfield: <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.