Import Statements and Bringing Names Into Scope

Once you've organized your code into modules, you need a way to bring those items into scope so you can use them without always writing the full path. This is where import statements come in.

Basic Import

The simplest form of an import brings an item directly into scope:

import restaurant.food.appetizers

fn main() {
    appetizers.bruschetta()  // Can use appetizers without the full path
}

Without the import, you'd need to write:

fn main() {
    restaurant.food.appetizers.bruschetta()  // Full path
}

Importing Specific Items

Import a single function or struct directly:

import restaurant.food.appetizers.bruschetta

fn main() {
    bruschetta()  // No prefix needed
}

This brings just bruschetta into scope.

Importing Multiple Items

Import multiple items from the same module using braces:

import restaurant.food.appetizers.{bruschetta, calamari, soup}

fn main() {
    bruschetta()
    calamari()
    soup()
}

Importing an Entire Module

Import a module and access its contents with dot notation:

import restaurant.food

fn main() {
    food.appetizers.bruschetta()
    food.mains.pasta()
    food.desserts.tiramisu()
}

Nested Imports

You can nest imports to bring multiple modules from a parent:

import restaurant.{food, payment, house}

fn main() {
    food.appetizer()      // Via imported module
    payment.process()     // Via imported module
    house.greet()         // Via imported module
}

This is equivalent to:

import restaurant.food
import restaurant.payment
import restaurant.house

Wildcard Imports

Import everything from a module using *:

import restaurant.food.*

fn main() {
    appetizers.bruschetta()
    mains.pasta()
}

Note: While wildcard imports are convenient, they can make code harder to read because it's not clear where appetizers comes from. Use them sparingly and mainly in tests or small scopes.

Renaming Imports

If two modules export items with the same name, or if you just prefer a different name, use as to rename:

import restaurant.food.appetizers as starters

fn main() {
    starters.bruschetta()
}

Multiple renames in one import:

import restaurant.{
    food.appetizers as starters,
    payment.creditCard as cardPayment,
}

fn main() {
    starters.bruschetta()
    cardPayment.process()
}

Relative Paths

Within the same file or module, you can use relative paths:

module restaurant {
    module food {
        public fn appetizer() { }
    }

    module house {
        fn greet() {
            // Access sibling module using dot notation
            let name = "appetizer"
            // Can access food.appetizer() here
            food.appetizer()
        }
    }
}

Importing from External Crates

If your project depends on other crates, import from them using the crate name:

# In Cargo.toml
[dependencies]
serde = "1.0"
import serde.json  // Import from the serde crate

fn main() {
    let data = json.parse("{\"key\": \"value\"}")
}

Import Styles

There are several valid import styles. Choose the one that makes sense for your code:

Style 1: Import Functions Directly

Use when you call the function many times:

import myLib.math.fibonacci

fn main() {
    println!("\(fibonacci(10))")
    println!("\(fibonacci(20))")
}

Style 2: Import the Module

Use when you need multiple items from the same module:

import myLib.math

fn main() {
    println!("\(math.fibonacci(10))")
    println!("\(math.add(5, 3))")
}

Style 3: Import with Alias

Use when dealing with naming conflicts:

import myLib.v1.process as processV1
import myLib.v2.process as processV2

fn main() {
    processV1.handle()
    processV2.handle()
}

Style 4: Full Paths

Use for rare or library items:

fn main() {
    let result = myLib.math.fibonacci(10)
}

Scoped Imports

Imports can be declared at any scope level, not just the top of a file:

fn processData() {
    import utils.math
    let result = math.fibonacci(10)
}

fn formatOutput() {
    import utils.formatting
    let text = formatting.indent("Text")
}

This can help keep imports close to where they're used, though top-level imports are more common.

Circular Imports

Oxide, like Rust, prevents circular dependencies at the crate level. However, within a crate, you can have modules that reference each other:

// src/lib.ox
module a {
    public fn callB() {
        b.doSomething()
    }
}

module b {
    public fn doSomething() {
        println!("B does something")
    }
}

This works because both modules are in the same compilation unit. The key is that you can't have circular dependencies between crates.

Re-exporting

When you import something in a public scope, it becomes available to your module's users:

// internal/utils.ox
public fn createConfig() { }

// src/lib.ox
import internal.utils.createConfig

// Users of this library can now do:
// import myLib.createConfig

To make this explicit, use public import:

public import internal.utils.createConfig

This makes it clear that createConfig is part of your public API.

Organizing Imports

A common convention is to organize imports into groups:

// Standard library imports (if any)
import io
import collections

// Internal imports
import utils.math
import utils.formatting

// Re-exports
public import math.fibonacci
public import formatting.indent

fn main() {
    let fib = fibonacci(10)
}

Comparison with Rust

In Rust:

  • Modules are brought into scope with use, not import
  • Paths use :: not .
  • Wildcard imports use use module::*;
  • pub use re-exports (same as Oxide)

Rust example:

#![allow(unused)]
fn main() {
use restaurant::food::appetizers::bruschetta;
use restaurant::food::*;
use restaurant::food::appetizers as starters;

pub use internal::utils::createMessage;
}

Oxide equivalent:

import restaurant.food.appetizers.bruschetta
import restaurant.food.*
import restaurant.food.appetizers as starters

public import internal.utils.createMessage

Summary

  • Basic imports bring items into scope to avoid writing full paths
  • Selective imports bring only what you need using braces
  • Module imports let you access items with dot notation
  • Wildcard imports bring everything into scope (use sparingly)
  • Renaming with as helps avoid conflicts and improve clarity
  • Nested imports import multiple items from the same parent
  • Relative paths work within modules without explicit imports
  • Scoped imports can be declared at any level
  • Re-exports with public import make items part of your public API

Now that you understand imports, let's explore paths and how to understand the full module hierarchy.