Chapter 5: Structs
Grouping Data That Belongs Together
You've been working with individual values-integers, strings, booleans. But real programs deal with entities that have multiple attributes. A user has a name, email, and age. A point has x and y coordinates. A configuration has a host, port, and timeout.
Structs let you group related data:
struct Point {
x: int,
y: int
}Now Point is a type, just like int or string. You can create points, pass them to functions, and store them in collections.
Creating and Using Structs
Define a struct with the struct keyword:
struct User {
name: string,
age: int,
email: string
}Create an instance with struct literal syntax:
fn main() -> int {
var user = User {
name: "Alice",
age: 30,
email: "[email protected]"
}
print(user.name) // Alice
print(user.age) // 30
print(user.email) // [email protected]
return 0
}Field order doesn't matter in the literal:
var user = User {
email: "[email protected]",
name: "Alice",
age: 30
}But you must provide all fields. The compiler won't let you forget one.
Accessing and Modifying Fields
Use dot notation:
fn main() -> int {
var point = Point { x: 10, y: 20 }
print(point.x) // 10
print(point.y) // 20
point.x = 15 // Modify field
print(point.x) // 15
return 0
}If the struct is declared with const, you can't modify it:
const origin = Point { x: 0, y: 0 }
// origin.x = 10 // Error: cannot modify constThis is the same immutability principle we've seen before. Use const by default.
Structs as Function Parameters
Pass structs to functions like any other value:
struct Point {
x: int,
y: int
}
fn distance_from_origin(p: Point) -> int {
return p.x * p.x + p.y * p.y
}
fn main() -> int {
var p = Point { x: 3, y: 4 }
var dist = distance_from_origin(p)
print(dist) // 25 (3² + 4² = 9 + 16)
return 0
}Remember, Wyn passes by value. The function gets a copy of the struct. If you modify the copy, the original is unchanged.
For small structs, this is fine. For large ones, it can be inefficient. We'll address that later.
Methods on Structs
Here's where structs get interesting. You can define methods that operate on struct instances:
struct Rectangle {
width: int,
height: int
}
fn Rectangle.area(self) -> int {
return self.width * self.height
}
fn Rectangle.perimeter(self) -> int {
return 2 * (self.width + self.height)
}
fn main() -> int {
var rect = Rectangle { width: 10, height: 5 }
print(rect.area()) // 50
print(rect.perimeter()) // 30
return 0
}The syntax is fn TypeName.method_name(self) -> ReturnType. The self parameter is the struct instance. Inside the method, you access fields with self.field_name.
This is Wyn's object-oriented side. Structs are objects. Methods are how you interact with them.
Nested Structs
Structs can contain other structs:
struct Point {
x: int,
y: int
}
struct Line {
start: Point,
end: Point
}
fn main() -> int {
var line = Line {
start: Point { x: 0, y: 0 },
end: Point { x: 10, y: 10 }
}
print(line.start.x) // 0
print(line.end.x) // 10
return 0
}Access nested fields with multiple dots: line.start.x.
Constructor Functions
While Wyn doesn't have special constructor syntax, you can write functions that create structs:
struct User {
name: string,
age: int,
email: string
}
fn new_user(name: string, age: int, email: string) -> User {
return User {
name: name,
age: age,
email: email
}
}
fn main() -> int {
var user = new_user("Alice", 30, "[email protected]")
print(user.name)
return 0
}This is useful when you want to validate inputs or set default values:
fn new_user_safe(name: string, age: int, email: string) -> User {
// Validate age
if (age < 0) {
age = 0
}
if (age > 150) {
age = 150
}
return User {
name: name,
age: age,
email: email
}
}The Builder Pattern
For structs with many fields, especially optional ones, use the builder pattern:
struct Config {
host: string,
port: int,
timeout: int,
debug: bool
}
fn default_config() -> Config {
return Config {
host: "localhost",
port: 8080,
timeout: 30,
debug: false
}
}
fn main() -> int {
var config = default_config()
config.port = 3000
config.debug = true
print(config.host) // "localhost"
print(config.port) // 3000
return 0
}Start with sensible defaults, then override what you need.
Struct Equality
Wyn doesn't automatically generate equality methods. You write them yourself:
fn Point.equals(self, other: Point) -> bool {
return self.x == other.x and self.y == other.y
}
fn main() -> int {
var p1 = Point { x: 5, y: 10 }
var p2 = Point { x: 5, y: 10 }
var p3 = Point { x: 3, y: 7 }
print(p1.equals(p2)) // true
print(p1.equals(p3)) // false
return 0
}This is more verbose than automatic equality, but it's explicit. You decide what equality means for your type.
Value Semantics
Structs have value semantics-they're copied when assigned:
fn main() -> int {
var p1 = Point { x: 5, y: 10 }
var p2: Point = p1 // Creates a copy
p2.x = 20
print(p1.x) // 5 - unchanged
print(p2.x) // 20 - modified
return 0
}This is safe but can be expensive for large structs. Most of the time, it's fine. When it's not, you'll know-your program will be slow.
Real-World Examples
Modeling a Person
struct Person {
first_name: string,
last_name: string,
age: int,
email: string
}
fn Person.full_name(self) -> string {
return self.first_name + " " + self.last_name
}
fn Person.is_adult(self) -> bool {
return self.age >= 18
}
fn Person.can_vote(self) -> bool {
return self.is_adult() // Methods can call other methods
}
fn main() -> int {
var person = Person {
first_name: "Alice",
last_name: "Smith",
age: 30,
email: "[email protected]"
}
print(person.full_name()) // "Alice Smith"
print(person.can_vote()) // true
return 0
}Modeling a Date
struct Date {
year: int,
month: int,
day: int
}
fn Date.is_valid(self) -> bool {
if (self.month < 1 or self.month > 12) {
return false
}
if (self.day < 1 or self.day > 31) {
return false
}
if (self.year < 1900 or self.year > 2100) {
return false
}
return true
}
fn Date.is_leap_year(self) -> bool {
if (self.year % 400 == 0) {
return true
}
if (self.year % 100 == 0) {
return false
}
if (self.year % 4 == 0) {
return true
}
return false
}
fn main() -> int {
var date = Date { year: 2024, month: 2, day: 29 }
print(date.is_valid()) // true
print(date.is_leap_year()) // true
return 0
}Design Principles
Keep Structs Focused
Each struct should represent a single concept:
// Good: focused structs
struct User {
name: string,
email: string
}
struct Address {
street: string,
city: string,
zip: string
}
// Bad: too many responsibilities
struct UserWithEverything {
name: string,
email: string,
street: string,
city: string,
order_history: [string],
preferences: [string],
session_token: string
}If a struct has more than 7-8 fields, consider breaking it up.
Use Descriptive Names
// Good
struct CustomerOrder { }
struct PaymentMethod { }
struct ShippingAddress { }
// Bad
struct CO { }
struct PM { }
struct Addr { }Abbreviations save typing but cost understanding. Prefer clarity.
Group Related Data
If you're passing the same set of parameters to multiple functions, they probably belong in a struct:
// Bad: passing many parameters
fn process(width: int, height: int, depth: int) -> int
fn validate(width: int, height: int, depth: int) -> bool
fn display(width: int, height: int, depth: int) -> int
// Good: group into struct
struct Dimensions {
width: int,
height: int,
depth: int
}
fn process(dims: Dimensions) -> int
fn validate(dims: Dimensions) -> bool
fn display(dims: Dimensions) -> intWhat You've Learned
Structs group related data. They're the foundation of data modeling in Wyn. You define them with struct, create instances with struct literals, and access fields with dot notation.
Methods let you attach behavior to structs. This is Wyn's object-oriented side-structs are objects, methods are how you interact with them.
Value semantics mean structs are copied when assigned. This is safe but can be expensive. Most of the time, it's fine.
Good struct design is about grouping related data, keeping structs focused, and using descriptive names.
Exercises
Rectangle: Define a
Rectanglestruct with width and height. Add methods for area, perimeter, and a method that checks if it's a square.Circle: Define a
Circlestruct with a radius. Add methods for area and circumference. Use 3.14159 for π.Temperature: Define a
Temperaturestruct that stores a value and a unit (Celsius or Fahrenheit). Add methods to convert between units.Bank account: Define a
BankAccountstruct with balance. Add methods for deposit, withdraw, and checking if the account is overdrawn.Vector2D: Define a
Vector2Dstruct with x and y. Add methods for addition, subtraction, magnitude, and dot product.Student: Define a
Studentstruct with name and an array of grades. Add a method to calculate the average grade.Refactor: Take code that uses multiple related variables and refactor it to use a struct. Notice how it becomes clearer.
Next: Chapter 6: Collections
Try It Yourself
Install Wyn and run this example:
curl -sSL wynlang.com/install.sh | sh
wyn init hello && cd hello
wyn run src/main.wyn