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:
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:
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:
// 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.wynImport with dot notation:
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:
import utils.math
import models.user
import services.authRelative imports are anchored explicitly: self:: starts from the current file's directory, root:: starts from the project root:
import self::sibling_module
import root::config
import root::company.infoUse 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:
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:
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:
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
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)
}// 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.wynEach 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.wynEach 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.wynconfig/settings.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:
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:
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:
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:
// 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
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:
// 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 usersUse Clear Names
// Good: clear purpose
import services.authentication
import utils.validation
import models.user
// Bad: unclear
import stuff
import helpers
import thingsMinimize Public API
Only expose what's necessary:
// 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 privateDocument Public Functions
// 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
Math library: Create a
mathmodule with functions for basic operations (add, subtract, multiply, divide). Make some functions public, some private.String utilities: Create a
string_utilsmodule with functions for common string operations. Import and use it in main.Nested modules: Create a
utilsdirectory withmath.wynandstring.wyninside. Import both in main using nested module syntax.Re-export: Create a
libmodule that re-exports functions from multiple other modules, creating a unified API.Refactor: Take a large single-file program and split it into multiple modules. Notice how it becomes easier to understand.
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.
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