Structs, Properties and Impl
Sec does not use classes. Instead, it separates type identity, stored data, behavior and controlled access into distinct language constructs. Each construct has one responsibility, and the constructs are combined to form complete application types.
A named type defines identity. A struct defines stored data. An impl
block defines behavior and type-local declarations. A property defines controlled
or computed access to values. Interfaces, described in a later chapter, define behavioral
contracts that other code may depend on.
Separation of Responsibilities
Sec deliberately separates the major parts of an object-like type:
type Vehicle struct {
_speed: Speed,
}
impl Vehicle {
property Speed: Speed {
get {
return _speed
}
}
fn Stop() void {
_speed = Speed(0)
}
}
In this example:
type Vehiclegives the type its identity.structdefines the data stored in eachVehicle.impl Vehicledefines behavior associated with that type.property Speedcontrols how the speed is exposed.fn Stopdefines an operation performed by the type.
This design avoids mixing stored representation with behavior. Code that needs to understand memory layout can focus on the struct declaration. Code that needs to understand behavior can focus on the impl block.
No Classes
Sec has no class declaration and no classical implementation inheritance. A struct is not a class, and an impl block does not create a hidden object or runtime type hierarchy.
Structs are value-oriented data definitions. Behavior is attached explicitly through impl blocks. Reuse is achieved through composition, interfaces and normal functions rather than through inheritance.
Structs
A struct defines the stored fields of a named type. Structs contain data only. They do not contain methods, properties, executable statements or initialization code.
Declaring a Struct
A named struct type is declared with type Name struct:
type Coordinate struct {
x: Meter,
y: Meter,
z: Meter,
}
The declaration creates a distinct named type called Coordinate. Every value of
this type contains the fields x, y and z.
The type keyword is required because a struct declaration always introduces a
named type. Sec does not use anonymous class-like declarations.
Struct Fields
Each field uses typed identifier syntax:
fieldName: Type
This is the same name-first syntax used by function parameters and explicit variable types.
type User struct {
id: CustomerID,
name: string,
age: Age,
}
Field names must be unique within the struct. Declaring the same field more than once is a compile-time error.
type InvalidUser struct {
name: string,
name: string,
} // Error: duplicate field "name" in struct InvalidUser.
Every referenced field type must exist. Unknown types are rejected during semantic analysis.
Field Separators
Struct fields are comma-separated. Line breaks do not separate fields.
Valid multi-line declaration:
type Coordinate struct {
x: Meter,
y: Meter,
z: Meter,
}
Valid single-line declaration:
type Coordinate struct { x: Meter, y: Meter, z: Meter }
A trailing comma before the closing brace is allowed and recommended for multi-line structs.
Invalid declaration:
type Coordinate struct {
x: Meter
y: Meter
}
The compiler reports that a comma or closing brace was expected after the preceding field. Field lists never depend on whitespace or line endings.
Struct Literals
A struct value is created with a struct literal:
let point := Coordinate{
x: Meter(10),
y: Meter(20),
z: Meter(0),
}
Struct literals use named fields. Positional struct initialization is not supported because it makes code depend on declaration order and becomes difficult to read when several fields have the same type.
Invalid positional construction:
let point := Coordinate{Meter(10), Meter(20), Meter(0)}
A field may appear at most once in a struct literal. Unknown fields and values of incompatible types are compile-time errors.
let point := Coordinate{
x: Meter(10),
x: Meter(20),
} // Error: duplicate field x.
Field Access
Fields are accessed with member syntax:
let horizontal := point.x
Member access may be chained when fields contain other struct values:
let power := vehicle.engine.power
Access is resolved statically. The compiler verifies that each member exists on the type of the preceding expression.
Field Mutability
Struct fields do not declare independent mutability. Mutability belongs to the binding or reference through which the struct is accessed.
An immutable variable does not permit field assignment:
let point := Coordinate{
x: Meter(10),
y: Meter(20),
z: Meter(0),
}
point.x = Meter(15) // Error: point is immutable.
A mutable variable allows its fields to be changed:
let mut point := Coordinate{
x: Meter(10),
y: Meter(20),
z: Meter(0),
}
point.x = Meter(15)
This follows Sec's general mutability rule: values are immutable unless the declaration includes
mut.
Default Construction
A mutable variable may be declared with a struct type and without an initializer when the struct has a valid default value.
let mut coordinate: Coordinate
A struct has a valid default only when every stored field has a valid default. If any field type has no default value, the struct must be initialized explicitly.
This rule is derived from the default-value rules of the field types. A struct does not invent defaults for fields whose types cannot be default-constructed.
Struct Field Tags
Struct fields may carry optional metadata tags. Tags use backtick raw-string syntax and follow the same basic key-value model as Go struct tags.
type User struct {
id: int `json:"id" xml:"id" db:"user_id"`,
name: string `json:"name" xml:"name"`,
password: string `json:"-"`,
}
A field tag contains one or more metadata entries:
key:"value" key:"value"
Tags do not affect type checking or memory layout. They are preserved as metadata for serializers, database libraries, reflection, code generators and other tools.
The compiler does not hard-code a fixed set of tag names. Libraries may define their own keys.
Common examples include json, xml, db, yaml and
csv.
Malformed tag syntax is a compile-time parser error.
Impl Blocks
An impl block defines behavior and type-local declarations for an existing named type. It does not create a new type and does not define stored data.
Impl Target
The basic syntax is:
impl Vehicle {
}
The name after impl is the target type. That type must already exist or be registered
as part of the same compilation unit.
impl MissingType {
} // Error: unknown impl target MissingType.
An impl block attaches declarations to a named type. It cannot target an arbitrary expression or an undeclared type name.
Allowed Impl Members
An impl block may contain:
- properties,
- methods declared with
fn, - nested named types,
- nested enums.
impl Vehicle {
type Engine struct {
power: Kilowatt,
}
enum FuelType {
petrol,
diesel,
electric,
}
property Speed: Speed {
get {
return _speed
}
}
fn Stop() void {
_speed = Speed(0)
}
}
Raw fields, variable declarations and executable statements are not valid directly inside an impl block.
One Impl Block per Type
Each named type may have at most one impl block.
impl Vehicle {
fn Start() void {
}
}
impl Vehicle {
fn Stop() void {
}
} // Error: duplicate impl block for Vehicle.
This rule keeps the type-local namespace and behavior together. A reader can find all methods, properties and nested declarations for a type in one place instead of searching through multiple impl blocks spread across the project.
No Stored Data in Impl
Only a struct declaration defines stored data layout.
Invalid:
impl Vehicle {
let currentSpeed: Speed
}
An impl block may not contain a let declaration as a stored member. Local variables are
permitted inside methods, getters and setters, but not directly in the impl body.
Likewise, nested structs must still be introduced with type:
impl Vehicle {
type Engine struct {
power: Kilowatt,
}
}
Invalid:
impl Vehicle {
struct Engine {
power: Kilowatt,
}
}
Methods
Methods are functions declared inside an impl block. They describe operations associated with the target type.
impl Vehicle {
fn Stop() void {
_speed = Speed(0)
}
}
Methods may access the target type's fields and properties according to the normal visibility, mutability and borrowing rules.
Full method syntax, parameters, receivers and return behavior are described in the functions and
methods chapter. The important structural rule here is that methods belong in impl, not
inside the struct declaration.
Properties
A property defines controlled or computed access to a value associated with a named type. Properties are behavior and therefore belong inside impl blocks.
From calling code, a property is accessed with the same member syntax as a field. Internally, the property may read a backing field, calculate a value, validate assignments or perform other controlled behavior.
Property Syntax
A property declaration specifies a name, a type and one or more accessors:
property Name: Type {
get {
return value
}
set value {
storedValue = value
}
}
A property may contain:
- one
getaccessor, - one
setaccessor, or - one
try setaccessor.
A property must contain at least one accessor. It may not contain two getters or two setters.
Read-only Properties
A property with a getter and no setter is read-only:
impl Vehicle {
property Speed: Speed {
get {
return _speed
}
}
}
The property may be read:
let currentSpeed := vehicle.Speed
It may not be assigned:
vehicle.Speed = Speed(80) // Error: property Speed is read-only.
Writable Properties
A normal setter allows assignment to the property:
impl Vehicle {
property Speed: Speed {
get {
return _speed
}
set value {
_speed = value
}
}
}
The setter parameter is declared after set. Its type is the property type, so the
example parameter value has type Speed.
vehicle.Speed = Speed(80)
The assigned value must be assignable to the property type. Named type identity remains strict;
an unrelated numeric value does not become Speed implicitly.
Computed Properties
A property does not need a backing field. Its getter may calculate the returned value:
type Rectangle struct {
width: Meter,
height: Meter,
}
impl Rectangle {
property Area: decimal<m*m> {
get {
return width * height
}
}
}
Calling code uses normal member syntax even though no stored Area field exists:
let area := rectangle.Area
This allows the internal representation to change without changing the public access syntax.
Backing Fields
A property commonly exposes or controls access to an internal field:
type Vehicle struct {
_speed: Speed,
}
impl Vehicle {
property Speed: Speed {
get {
return _speed
}
set value {
_speed = value
}
}
}
The field and property are separate declarations. Sec does not create an implicit hidden backing field for a property.
A field beginning with a single underscore is module-private according to Sec's visibility rules. Such fields are useful as internal representation while a public property exposes controlled access.
Fallible Properties
Some assignments require validation or may fail for reasons that cannot be represented as a
simple compile-time type error. A fallible setter is declared with try set.
type VehicleError enum {
invalidSpeed,
}
impl Vehicle {
property Speed: Speed {
get {
return _speed
}
try set value {
if value < Speed(0) {
return Err(VehicleError.invalidSpeed)
}
_speed = value
}
}
}
Assignment to a fallible property must explicitly use try:
try vehicle.Speed = requestedSpeed
Assignment without try is rejected:
vehicle.Speed = requestedSpeed
// Error: assignment to fallible property Speed requires try.
A normal setter may not return Err(...). A setter that can fail must be declared as
try set so the possibility of failure remains visible to callers.
Property Type Rules
The property declaration defines the type observed by callers.
property TopSpeed: Speed {
get {
return _speed
}
}
Every getter return expression must be assignable to the declared property type. Returning a different named type is an error even when both types share the same underlying representation.
property InvalidSpeed: Speed {
get {
return Money(5.90)
}
} // Error: getter InvalidSpeed must return Speed, got Money.
A getter must return a value:
property InvalidSpeed: Speed {
get {
}
} // Error: getter InvalidSpeed must return Speed.
A setter parameter has the property type. Assignments performed inside the setter remain subject to the normal assignment, named type and mutability rules.
Property Access
Fields and properties use the same member-access operator:
let rawSpeed := vehicle._speed
let publicSpeed := vehicle.Speed
The compiler resolves whether the member is a stored field or a property from the static type of the expression. Calling code does not use explicit getter or setter function syntax.
This common syntax does not make fields and properties semantically identical. A field directly accesses stored data, while a property executes its declared accessor behavior.
Nested Types
An impl block may contain named types that logically belong to its target type. Nested types create a
type-local namespace without changing the rule that type introduces named types.
Nested Struct Types
impl Vehicle {
type Engine struct {
power: Kilowatt,
}
type FuelTank struct {
capacity: Liter,
}
}
These declarations create the named types Vehicle.Engine and
Vehicle.FuelTank.
The nested declaration still requires type. Writing struct Engine directly
is invalid.
Nested Enums
Enums may also be declared in an impl block:
impl Vehicle {
enum FuelType {
petrol,
diesel,
electric,
}
enum EngineState {
off,
idle,
running,
}
}
Enum values are accessed through the nested enum type:
let fuel := Vehicle.FuelType.petrol
Variant names must be unique within their enum. Different enums may reuse the same variant name.
Qualified Names
Outside the target type's impl block, nested types use their fully qualified names:
Vehicle.Engine
Vehicle.FuelType
Vehicle.FuelType.electric
Inside impl Vehicle, both short and fully qualified names are valid:
Engine
FuelType
Vehicle.Engine
Vehicle.FuelType
Qualification makes ownership clear and prevents unrelated nested type names from colliding in the module namespace.
Forward References
A struct may refer to a nested type declared later in its impl block. The compiler registers type names before resolving field types.
type Vehicle struct {
engine: Vehicle.Engine,
fuelType: Vehicle.FuelType,
}
impl Vehicle {
type Engine struct {
power: Kilowatt,
}
enum FuelType {
petrol,
diesel,
electric,
}
}
This is valid even though the nested declarations appear after the struct. Type declarations are resolved semantically rather than requiring strict textual declaration order.
Complete Example
type Speed decimal<m/s>
type Kilowatt decimal<kW>
type Liter decimal<L>
type VehicleError enum {
invalidSpeed,
}
type Vehicle struct {
_speed: Speed,
engine: Vehicle.Engine,
fuelType: Vehicle.FuelType,
}
impl Vehicle {
type Engine struct {
power: Kilowatt,
}
enum FuelType {
petrol,
diesel,
electric,
}
property Speed: Speed {
get {
return _speed
}
try set value {
if value < Speed(0) {
return Err(VehicleError.invalidSpeed)
}
_speed = value
}
}
property IsMoving: bool {
get {
return _speed > Speed(0)
}
}
fn Stop() void {
_speed = Speed(0)
}
}
let mut vehicle := Vehicle{
_speed: Speed(0),
engine: Vehicle.Engine{
power: Kilowatt(100),
},
fuelType: Vehicle.FuelType.electric,
}
try vehicle.Speed = Speed(25)
let moving := vehicle.IsMoving
vehicle.Stop()
The example demonstrates stored fields, a nested struct, a nested enum, a writable fallible property, a computed read-only property and a method. Each construct has a distinct role while all of them form one coherent named type.
Summary of Rules
- Sec has no classes.
type Name structdefines a named type and its stored data.- Struct fields use
name: Typesyntax. - Struct fields are comma-separated, and a trailing comma is allowed.
- Struct literals use named fields.
- Field mutation requires mutable access to the containing value.
- Struct field tags are metadata and do not affect type semantics.
- An impl block defines behavior and type-local declarations for an existing named type.
- Each named type may have at most one impl block.
- An impl block may not declare stored fields or top-level
letbindings. - Methods and properties belong inside impl blocks.
- A property may be read-only, writable or fallible.
- A property getter must return the declared property type.
- Assignment to a fallible property requires
try. - Nested types are accessed with qualified names outside the impl block.
- A struct may refer to nested types declared later in its impl block.