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 {
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:
// 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 {
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:
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 {
token = authentication::login("alice", "secret")
allowed = authorization::can_access(token, "admin")
return 0
}Alias them:
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:
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(a: int, b: int) -> int {
return stats::mean(a, b)
}main.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.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> {
// Not `user = ...`: `user` is the imported module's name here.
created = user::new_user(name, email)
// Save to database
return Ok(created)
}main.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:
// 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 - the import line says what it is:
import services.authentication
import utils.validation
import models.userBad - you have to open the file to find out:
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