Paths for Referring to Items in the Module Tree
A path is a way to refer to an item in the module tree. Paths can take two forms: absolute and relative.
Absolute Paths
An absolute path starts from the crate root and uses the full path to an item:
public module restaurant {
public module food {
public module appetizers {
public fn bruschetta() {
println!("Making bruschetta")
}
}
}
}
fn main() {
// Absolute path
restaurant.food.appetizers.bruschetta()
}
This is the most explicit and clear form. It tells readers exactly where the item comes from.
Relative Paths
A relative path starts from the current module and builds from there. You can reference items without writing the full path:
module restaurant {
module food {
public fn appetizer() {
println!("Appetizer")
}
public fn describe() {
// Relative path - within the same module
appetizer()
}
}
module house {
fn greet() {
// This won't work - food is a sibling, not parent
// food.appetizer() // Error!
}
}
}
Within a module, you can call other items in the same module directly. However, sibling modules require you to use their full name relative to a common parent.
Understanding the Module Hierarchy
Think of the module tree like a filesystem:
crate_root
├── restaurant (module)
│ ├── food (module)
│ │ ├── appetizers (module)
│ │ │ └── bruschetta (function)
│ │ └── mains (module)
│ │ └── pasta (function)
│ └── house (module)
│ └── greet (function)
└── main (function)
To access an item, you navigate this tree. For example:
restaurant.food.appetizers.bruschetta- Start at root, go to restaurant, then food, then appetizers, then call bruschettarestaurant.house.greet- Start at root, go to restaurant, then house, then call greet
Public vs. Private in Paths
Paths work only for public items. If an item or any parent module is private, you can't access it from outside:
module restaurant { // Private (not marked public)
public fn greeting() { }
}
fn main() {
restaurant.greeting() // Error! restaurant is private
}
To fix:
public module restaurant { // Make parent public
public fn greeting() { }
}
fn main() {
restaurant.greeting() // OK
}
This is the public visibility rule: all ancestors in the path must be public for you to access an item.
Paths in Imports
When you import, you're creating a new name binding using a path:
import restaurant.food.appetizers as starters
fn main() {
starters.bruschetta() // starters is the imported name
}
The import statement says: "Follow the path restaurant.food.appetizers and bind the result to the name starters."
Paths with Generics and Complex Types
When dealing with generic types or trait objects, paths can include type information:
import std.collections.HashMap
fn main() {
// HashMap is a path referring to a generic type
let map: HashMap[String, Int] = HashMap()
}
The path std.collections.HashMap refers to the type itself, which can be instantiated with type arguments.
Documenting Paths
When you document your code, include paths in comments and doc comments:
/// Represents a restaurant's menu.
///
/// The `Restaurant` struct is found at `restaurant.Restaurant`.
/// To add items, use the `addItem` method from `restaurant.menu.Menu`.
public struct Restaurant {
name: String,
}
This helps users understand how to navigate your module structure.
Common Path Patterns
Pattern 1: Accessing Sibling Modules
module server {
module http {
fn handleRequest() { }
}
module websocket {
fn handleConnection() {
// To call sibling module, use full path from parent
http.handleRequest() // This won't work!
// Instead:
}
}
}
From within websocket, you need to reference http as a sibling. The safest approach is to use the full path:
module server {
module http {
public fn handleRequest() { }
}
module websocket {
fn handleConnection() {
// Use the full path (but this requires http to be public)
server.http.handleRequest()
}
}
}
Or more simply, within the same crate, you can use the parent module:
public module server {
public module http {
public fn handleRequest() { }
}
public module websocket {
fn handleConnection() {
// Within the same public parent, access via dot notation
http.handleRequest() // This works within the server module
}
}
}
Pattern 2: Accessing Parent Items
public module restaurant {
public module kitchen {
fn cook() {
// You can't easily reference parent items
// Instead, structure code to avoid this need
}
}
public fn announceReady() {
println!("Food is ready!")
}
}
fn main() {
// You access via the full path
restaurant.announceReady()
}
If you need parent functionality in a child module, consider:
- Passing it as a parameter
- Creating a shared utility module
- Restructuring to avoid the parent dependency
Pattern 3: Items in the Same Module
public module restaurant {
public fn appetizer() {
println!("Appetizer")
}
public fn mainCourse() {
// Within the same module, call directly
appetizer() // This works
}
}
Re-exports and Path Visibility
When you re-export an item, you create an alternative path to it:
// src/lib.ox
external module internals
public import internals.helpers.createMessage
// Now there are two paths to the same item:
// 1. internals.helpers.createMessage (private, internal only)
// 2. createMessage (public, preferred)
Users see the shorter path, while your internal structure remains hidden.
Full Paths in Error Messages
When the compiler reports an error, it uses paths to tell you about items:
error: cannot find function `bruschetta` in module `restaurant::food::appetizers`
--> main.ox:3:5
|
3 | bruschetta()
| ^^^^^^^^^^ not found in this scope
|
help: consider using the full path with the item's type
|
3 | restaurant.food.appetizers.bruschetta()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The compiler shows the full path and suggests how to fix it.
Comparison with Rust
In Rust:
- Absolute paths start with
crate:: - Relative paths use
self::orsuper:: - Paths use
::not. - You can use
superto reference parent modules
Rust example:
#![allow(unused)] fn main() { // Absolute path crate::restaurant::food::appetizers::bruschetta(); // Relative path with super super::other_module::do_something(); use crate::restaurant::food::appetizers as starters; }
Oxide equivalent:
// Absolute path
restaurant.food.appetizers.bruschetta()
// Relative path (within same module)
otherModule.doSomething()
import restaurant.food.appetizers as starters
Oxide's simpler path syntax (no ::, crate::, or super::) makes navigation more intuitive, especially for those familiar with object-oriented languages.
Summary
- Paths refer to items in the module tree using dot notation
- Absolute paths start from the crate root
- Relative paths navigate from the current module
- Privacy rules: all ancestors in a path must be public
- Paths are clear: they explicitly show where items come from
- Importing creates shorter names for paths
- Re-exports create alternative paths to the same items
- Oxide uses simple dot notation, unlike Rust's
::syntax
Now that you understand paths, let's look at practical file organization strategies.