Skip to content

Chapter 9: Modules

Code Organization at Scale

You can write everything in one file. For small programs, that's fine. But as your codebase grows, you need structure. You need to separate concerns, hide implementation details, and reuse code across projects.

Modules are how you organize code in Wyn.

The Basics

A module is just a file. Create math.wyn:

wyn
pub fn add(a: int, b: int) -> int {
    return a + b
}

pub fn multiply(a: int, b: int) -> int {
    return a * b
}

The pub keyword makes functions visible outside the module. Without it, they're private.

Now use it in main.wyn:

wyn
import math

fn main() -> int {
    var sum = math::add(5, 3)
    var product = math::multiply(4, 7)
    
    print(sum)      // 8
    print(product)  // 28
    
    return 0
}

The import math statement makes the math module available. You call its functions with math::function_name().

Public vs Private

By default, everything is private:

wyn
// math.wyn
fn helper(x: int) -> int {  // Private
    return x * 2
}

pub fn double(x: int) -> int {  // Public
    return helper(x)
}

From outside the module, you can only call double(). The helper() function is an implementation detail.

This is information hiding. It lets you change internal implementation without breaking code that uses your module.

Nested Modules

Organize related modules into directories:

project/
├── main.wyn
└── utils/
    ├── math.wyn
    └── string.wyn

Import with dot notation:

wyn
import utils.math
import utils.string

fn main() -> int {
    var sum = math::add(1, 2)
    var upper = string::to_upper("hello")
    return 0
}

The directory structure mirrors the module hierarchy. After importing, you refer to the module by the last segment of its path: import utils.math gives you math::add.

Module Paths

There are two kinds of imports:

Absolute imports start from the project root:

wyn
import utils.math
import models.user
import services.auth

Relative imports are anchored explicitly: self:: starts from the current file's directory, root:: starts from the project root:

wyn
import self::sibling_module
import root::config
import root::company.info

Use plain absolute imports for clarity. Use self:: when modules are tightly coupled, and root:: to be unambiguous from deep inside a module tree.

Aliasing Imports

Long module names get tedious:

wyn
import services.authentication
import services.authorization

fn main() -> int {
    var token = authentication::login("alice", "secret")
    var allowed = authorization::can_access(token, "admin")
    return 0
}

Alias them:

wyn
import utils.math as m
import utils.string as s

fn main() -> int {
    var x = m::add(1, 2)
    var y = s::to_upper("hello")
    return 0
}

Or import specific items:

wyn
import { add, multiply } from utils.math

fn main() -> int {
    var x = add(1, 2)      // No prefix needed
    var y = multiply(3, 4)
    return 0
}

Use this sparingly. It's less clear where functions come from.

Re-exporting

Sometimes you want to expose items from another module. Wrap them in public functions:

wyn
// utils.wyn
import utils.math
import utils.stats

pub fn add(a: int, b: int) -> int {
    return math::add(a, b)
}

pub fn mean(values: [int]) -> int {
    return stats::mean(values)
}
wyn
// main.wyn
import utils

fn main() -> int {
    var x = utils::add(1, 2)
    var y = utils::mean([1, 2, 3])
    return 0
}

This creates a facade-a simplified interface to a complex subsystem. Callers import one module and never need to know how the internals are split up.

Module Organization Patterns

By Feature

project/
├── main.wyn
├── users/
│   ├── model.wyn
│   ├── service.wyn
│   └── repository.wyn
├── orders/
│   ├── model.wyn
│   ├── service.wyn
│   └── repository.wyn
└── auth/
    ├── service.wyn
    └── middleware.wyn

Each feature gets its own directory. Related code stays together.

By Layer

project/
├── main.wyn
├── models/
│   ├── user.wyn
│   └── order.wyn
├── services/
│   ├── user_service.wyn
│   └── order_service.wyn
└── repositories/
    ├── user_repo.wyn
    └── order_repo.wyn

Each layer gets its own directory. Similar code stays together.

Choose based on your domain. Feature-based works better for most applications. Layer-based works better for frameworks.

Real-World Example: Web Server

web_server/
├── main.wyn
├── config/
│   └── settings.wyn
├── models/
│   ├── user.wyn
│   └── post.wyn
├── handlers/
│   ├── user_handler.wyn
│   └── post_handler.wyn
├── middleware/
│   ├── auth.wyn
│   └── logging.wyn
└── utils/
    ├── validation.wyn
    └── response.wyn

config/settings.wyn:

wyn
pub struct Config {
    host: string,
    port: int,
    db_url: string
}

pub fn load_config() -> Result<Config, string> {
    // Load from file or environment
    return Ok(Config {
        host: "localhost",
        port: 8080,
        db_url: "postgres://localhost/mydb"
    })
}

models/user.wyn:

wyn
pub struct User {
    id: int,
    name: string,
    email: string
}

pub fn new_user(name: string, email: string) -> User {
    return User {
        id: 0,  // Will be set by database
        name: name,
        email: email
    }
}

handlers/user_handler.wyn:

wyn
import models.user

pub fn handle_get_user(id: int) -> Result<User, string> {
    // Fetch user from database
    return Ok(user::new_user("Alice", "[email protected]"))
}

pub fn handle_create_user(name: string, email: string) -> Result<User, string> {
    var user = user::new_user(name, email)
    // Save to database
    return Ok(user)
}

main.wyn:

wyn
import config.settings
import handlers.user_handler

fn main() -> int {
    var config = settings::load_config()
    
    match config {
        Ok(cfg) => {
            print("Starting server on ${cfg.host}:${cfg.port}")
            
            // Start server...
        },
        Err(msg) => {
            print("Failed to load config: " + msg)
            return 1
        }
    }
    
    return 0
}

Circular Dependencies

Don't do this:

wyn
// a.wyn
import b

pub fn func_a() -> int {
    return b::func_b()
}

// b.wyn
import a

pub fn func_b() -> int {
    return a::func_a()
}

This is a circular dependency. The compiler will reject it.

If you need this, you've probably organized your code wrong. Extract the shared functionality into a third module:

wyn
// shared.wyn
pub fn shared_func() -> int {
    return 42
}

// a.wyn
import shared

pub fn func_a() -> int {
    return shared::shared_func()
}

// b.wyn
import shared

pub fn func_b() -> int {
    return shared::shared_func()
}

Module Best Practices

Keep Modules Focused

Each module should have a single responsibility:

wyn
// Good: focused modules
// user_service.wyn - user business logic
// user_repository.wyn - user data access
// user_validator.wyn - user validation

// Bad: kitchen sink module
// user.wyn - everything related to users

Use Clear Names

wyn
// Good: clear purpose
import services.authentication
import utils.validation
import models.user

// Bad: unclear
import stuff
import helpers
import things

Minimize Public API

Only expose what's necessary:

wyn
// Good: minimal public API
pub fn create_user(name: string) -> Result<User, string>
pub fn get_user(id: int) -> Option<User>

fn validate_name(name: string) -> bool  // Private helper
fn hash_password(pwd: string) -> string  // Private helper

// Bad: exposing internals
pub fn create_user(name: string) -> Result<User, string>
pub fn get_user(id: int) -> Option<User>
pub fn validate_name(name: string) -> bool  // Should be private
pub fn hash_password(pwd: string) -> string  // Should be private

Document Public Functions

wyn
// Authenticates a user with the given credentials.
// Returns Ok(user) on success, Err(message) on failure.
pub fn authenticate(email: string, password: string) -> Result<User, string> {
    // Implementation
}

What You've Learned

Modules organize code into logical units. Files are modules. Directories create module hierarchies.

Use pub to make items public. Everything else is private by default. This is information hiding-it lets you change internals without breaking users.

Import modules with import. Use :: to access module items. Alias long names for convenience.

Organize by feature or by layer, depending on your domain. Keep modules focused. Minimize the public API. Avoid circular dependencies.

Good module structure makes code easier to navigate, understand, and maintain.

Exercises

  1. Math library: Create a math module with functions for basic operations (add, subtract, multiply, divide). Make some functions public, some private.

  2. String utilities: Create a string_utils module with functions for common string operations. Import and use it in main.

  3. Nested modules: Create a utils directory with math.wyn and string.wyn inside. Import both in main using nested module syntax.

  4. Re-export: Create a lib module that re-exports functions from multiple other modules, creating a unified API.

  5. Refactor: Take a large single-file program and split it into multiple modules. Notice how it becomes easier to understand.

  6. Web server structure: Design a module structure for a web application with users, posts, and comments. Don't implement it-just create the directory structure and empty files.

  7. Circular dependency: Try to create a circular dependency between two modules. See the compiler error. Then fix it by extracting shared code.


Next: Chapter 10: Generics

MIT License - v1.19.1