Skip to content

Structures

Structs in Helix

Structs in Helix

In 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 Structs

To 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 Instances

Structs 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 Structs

Structs 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 Overview

The 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 Requires
struct 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 Interfaces

Structs 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 Interface
interface 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 Overloading

Structs 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 Operators
struct 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 Operator
struct 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 Structs

Structs 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 Struct
interface Addable {
op + fn (self, other: &Addable) -> &Addable;
}
struct Point {
let x: int;
let y: int;
}
// Extending Point to implement Addable
extend 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 Structs

Conclusion

Conclusion

Structs 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.

References

References