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.

Don't let the word "struct" intimidate you-it's just short for structure, a way to group related values under one name. And like everything else in Wyn, what you make with it is an object: you attach methods to it and call them with a dot. There's no separate "class" concept to learn-structs are Wyn's objects.

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 {
    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
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 {
    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 {
    p = Point { x: 3, y: 4 }
    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 {
    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 {
    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 {
    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
struct User {
    name: string,
    age: int,
    email: string
}

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 {
    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 {
    p1 = Point { x: 5, y: 10 }
    p2 = Point { x: 5, y: 10 }
    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
struct Point {
    x: int,
    y: int
}

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 {
    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 {
    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.22.0