Structures
Structs in Helix
Structs in HelixIn Helix, structs are used exclusively for storing aggregate types. Unlike classes, structs cannot have methods, constructors, or derived relationships. However, they can:
- Contain other structs.
- Contain variables (fields).
- Contain Type Aliases/Definitions.
- Contain anonymous or named enumerations.
- Overload anonymous operators (
op
without aliases). - Use interfaces with the
with
keyword to implement variables and operators.
Structs are a lightweight and efficient way to organize data that does not require the additional functionality of classes.
Declaring Structs
Declaring StructsTo declare a struct, use the struct
keyword followed by the name of the struct and its fields.
struct Point { let x: int; let y: int;}
Creating Struct Instances
Creating Struct InstancesStructs do not have constructors. Instead, you initialize them directly by assigning values to their fields:
- Implicit Object Initialization:
{ .field1 = value1, .field2 = value2 }
syntax. This syntax does requires the type to be specified.
fn main() { let p: Point = { .x = 10, .y = 20 }; print(f"Point: ({p.x}, {p.y})");}
- Explicit Object Initialization: Using the
Point { x = 10, y = 20 }
syntax. This syntax does not require the type to be specified.
fn main() { let p = Point { x = 10, y = 20 }; // type can be inferred print(f"Point: ({p.x}, {p.y})");}
Using requires
with Structs
Using requires with StructsStructs can define generic parameters using the requires
keyword. This allows you to create structs that can store or manipulate data types generically.
Syntax Overview
Syntax OverviewThe requires
declaration specifies type parameters and optionally enforces constraints on those parameters. Constraints are covered in more detail in Requires.
struct Box requires <T> { let value: T;}
Example: Struct with Requires
Example: Struct with Requiresstruct Box requires <T> { let value: T;}
fn main() { let int_box: Box::<int> = { .value = 42 }; let str_box: Box::<string> = { .value = "Hello, Helix" };
print(int_box.value); // Outputs: 42 print(str_box.value); // Outputs: Hello, Helix}
Structs with
Interfaces
Structs with InterfacesStructs can implement variables and anonymous operators (op
) from interfaces using the with
keyword. However:
- Only variables and operators are supported.
- Functions in interfaces cannot be implemented by structs.
Example: Struct with Interface
Example: Struct with Interfaceinterface Movable { let dx: int; let dy: int;
op + fn (self, other: &Movable) -> &Movable;}
struct Point with Movable { let dx: int; // Implementing dx from Movable let dy: int; // Implementing dy from Movable
// Implementing the + operator op + fn (self, other: &Point) -> &Point { self.dx += other.dx; self.dy += other.dy;
return self; }}
fn main() { let p1 = Point { x = 10, y = 20, dx = 0, dy = 0 }; let p2 = Point { x = 5, y = 15, dx = 0, dy = 0 };
let p3 = p1 + p2; // Calls the overloaded + operator print(f"New Point: ({p3.x}, {p3.y})");}
Operator Overloading
Operator OverloadingStructs can overload anonymous operators (operators without an alias). This allows intuitive usage such as a + b
instead of explicit function calls.
Example: Overloading Arithmetic Operators
Example: Overloading Arithmetic Operatorsstruct Vector { let x: float; let y: float;
op + fn (self, other: &Vector) -> Vector { // we are using implicit object creation here because it doesn't affect readability return { .x = self.x + other.x, .y = self.y + other.y }; }
op - fn (self, other: &Vector) -> Vector { return { .x = self.x - other.x, .y = self.y - other.y }; }}
fn main() { let v1: Vector = Vector { x = 1.5, y = 2.5 }; let v2: Vector = Vector { x = 3.0, y = 1.0 };
let v3 = v1 + v2; // Calls overloaded + let v4 = v1 - v2; // Calls overloaded -
print(f"Vector v3: ({v3.x}, {v3.y})"); print(f"Vector v4: ({v4.x}, {v4.y})");}
Overloading the Cast Operator (as
)
Overloading the Cast Operator (as)Structs can define custom casting behavior with the as
operator. This allows explicit conversions between types.
Example: Custom Cast Operator
Example: Custom Cast Operatorstruct Point { x: int; y: int;
op as fn (self) -> string { return f"Point({self.x}, {self.y})"; }}
fn main() { let p = Point { x = 10, y = 20 }; let p_str = p as string; // Calls the custom cast operator
// NOTE: `as string` is implicitly called in the print function if not explicitly cast. print(p_str); // Output: Point(10, 20)}
Extending Structs
Extending StructsStructs can be extended to add new fields or operators. This allows you to expand the functionality of existing structs without modifying their original definition.
Example: Extending a Struct
Example: Extending a Structinterface Addable { op + fn (self, other: &Addable) -> &Addable;}
struct Point { let x: int; let y: int;}
// Extending Point to implement Addableextend Point with Addable { op + fn (self, other: &Point) -> &Point { self.x += other.x; self.y += other.y;
return self; }}
fn main() { let p1 = Point { x = 10, y = 20 }; let p2 = Point { x = 5, y = 15 };
let p3 = p1 + p2; // Calls the overloaded + operator print(f"New Point: ({p3.x}, {p3.y})");}
Best Practices for Structs
Best Practices for StructsConclusion
ConclusionStructs in Helix are lightweight and efficient for storing aggregate data. By restricting their functionality to fields and anonymous operators, Helix ensures that structs remain simple and focused on their purpose.