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 {
    sum = math::add(5, 3)
    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 {
    sum = math::add(1, 2)
    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 {
    token = authentication::login("alice", "secret")
    allowed = authorization::can_access(token, "admin")
    return 0
}

Alias them:

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

fn main() -> int {
    x = m::add(1, 2)
    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:

utils.wyn:

wyn
import utils.math
import utils.stats

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

pub fn mean(a: int, b: int) -> int {
    return stats::mean(a, b)
}

main.wyn:

wyn
import utils

fn main() -> int {
    print(utils::add(1, 2))
    print(utils::mean(10, 20))
    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.

Keep the types on a module's public surface simple. Scalars, strings and structs cross a module boundary cleanly; a plain import lib whose function takes an array (pub fn mean(values: [int])) is rejected today, so a facade that fans out to array work should take scalars or a string and build the array on the far side.

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> {
    // Not `user = ...`: `user` is the imported module's name here.
    created = user::new_user(name, email)
    // Save to database
    return Ok(created)
}

main.wyn:

wyn
import config.settings
import handlers.user_handler

fn main() -> int {
    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
}

One caveat on today's compiler: a module function that returns Result<Struct, E> - like load_config() and handle_get_user() above - type-checks across a module boundary but does not yet code-generate correctly. Until it does, return the struct directly from the module and do the Result wrapping in the caller.

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:

shared.wyn:

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

a.wyn:

wyn
import shared

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

b.wyn:

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 ​

Good - the import line says what it is:

wyn
import services.authentication
import utils.validation
import models.user

Bad - you have to open the file to find out:

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