Skip to content

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:

wyn
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:

wyn
struct User {
    name: string,
    age: int,
    email: string
}

Create an instance with struct literal syntax:

wyn
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:

wyn
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:

wyn
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:

wyn
const origin = Point { x: 0, y: 0 }
// origin.x = 10  // Error: cannot modify const

This 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:

wyn
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:

wyn
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:

wyn
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:

wyn
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:

wyn
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:

wyn
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:

wyn
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:

wyn
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

wyn
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

wyn
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:

wyn
// 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

wyn
// Good
struct CustomerOrder { }
struct PaymentMethod { }
struct ShippingAddress { }

// Bad
struct CO { }
struct PM { }
struct Addr { }

Abbreviations save typing but cost understanding. Prefer clarity.

If you're passing the same set of parameters to multiple functions, they probably belong in a struct:

wyn
// 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) -> int

What 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

  1. Rectangle: Define a Rectangle struct with width and height. Add methods for area, perimeter, and a method that checks if it's a square.

  2. Circle: Define a Circle struct with a radius. Add methods for area and circumference. Use 3.14159 for π.

  3. Temperature: Define a Temperature struct that stores a value and a unit (Celsius or Fahrenheit). Add methods to convert between units.

  4. Bank account: Define a BankAccount struct with balance. Add methods for deposit, withdraw, and checking if the account is overdrawn.

  5. Vector2D: Define a Vector2D struct with x and y. Add methods for addition, subtraction, magnitude, and dot product.

  6. Student: Define a Student struct with name and an array of grades. Add a method to calculate the average grade.

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

bash
curl -sSL wynlang.com/install.sh | sh
wyn init hello && cd hello
wyn run src/main.wyn

MIT License - v1.19.1