Módulos, Crates y Workspaces
mod, pub y use convierten un proyecto Rust en crecimiento en uno con una API interna deliberada, y los workspaces de Cargo extienden esa misma disciplina a varios crates.
Todo programa en Rust se organiza como un árbol de módulos que arranca desde una raíz implícita: el propio crate. La palabra clave mod añade un nodo a ese árbol — mod front_of_house { ... } declara un módulo llamado front_of_house, y todo lo que hay dentro de las llaves (o, a medida que el proyecto crece, dentro de un archivo aparte) le pertenece. Esto no es solo una agrupación cosmética tipo carpetas: el árbol de módulos es también la unidad sobre la que operan tanto las reglas de privacidad de Rust como la resolución de rutas, así que entender el árbol es un requisito previo para entender ambas cosas.
A diferencia de muchos lenguajes donde los elementos son accesibles por defecto y tú decides ocultarlos, Rust hace que cada elemento —funciones, structs, enums, e incluso los campos de un struct individualmente— sea privado por defecto, visible solo dentro del módulo que lo define y de los módulos descendientes de este. La visibilidad se activa explícitamente con pub. Esta inversión es deliberada: te obliga a decidir, en el momento en que escribes un elemento, si forma parte de la API pública del módulo o si es un detalle de implementación, en lugar de exponer internals por accidente y descubrir años después que medio código base depende de ellos. pub en un struct ni siquiera hace públicos sus campos por defecto: pub struct Rectangle { width: f64, height: f64 } es un tipo público y nombrable con dos campos que nadie fuera del módulo puede leer ni escribir directamente, que es exactamente cómo forzarías a quien lo use a pasar por un constructor que valide los datos.
use no cambia qué es visible; solo te ahorra escribir una ruta larga cada vez que quieres nombrar algo. La convención de Rust marca una distinción deliberada según qué estés importando: para funciones, normalmente traes al alcance el módulo padre y llamas hosting::add_to_waitlist(), de modo que el sitio de la llamada siga insinuando que la función vive en otro lugar; para structs, enums y traits, normalmente traes al alcance el tipo mismo, escribiendo use std::collections::HashMap; para después poder escribir simplemente HashMap::new(). Las rutas se pueden escribir desde la raíz del crate (una ruta absoluta) o de forma relativa al módulo actual usando self o super para subir un nivel — las mismas tres formas de referirte a cualquier elemento, ya sea dentro de un use o directamente en el código.
Un crate es un binario o una biblioteca, y la diferencia va más allá de si tiene o no un fn main. Un crate binario (src/main.rs) compila a un ejecutable y es lo que realmente se ejecuta; un crate de biblioteca (src/lib.rs) compila a algo pensado para enlazarse dentro de otros crates y expone su API a través de elementos pub, sin ningún main propio. Un mismo paquete puede contener ambos —una biblioteca más uno o varios binarios que dependen de ella— y este es el patrón al que convergen casi de inmediato los proyectos Rust con experiencia: poner la lógica real en lib.rs, donde es testeable por unidad y reutilizable, y dejar que main.rs sea una cáscara delgada que interpreta argumentos, conecta las piezas y llama a la biblioteca.
Cuando un solo archivo crece más allá de unos cientos de líneas, dividirlo en módulos mantiene junto el código relacionado y separado el que no lo está. mod garden; en main.rs, sin llaves, le dice al compilador que el contenido del módulo vive en un archivo aparte: ya sea src/garden.rs, o, si garden a su vez necesita submódulos, src/garden/mod.rs (la convención antigua) o un directorio src/garden/ junto a garden.rs (el esquema que usan hoy la mayoría de los proyectos). El detalle que conviene recordar: soltar un archivo .rs nuevo en src/ no hace nada por sí solo. Rust no escanea directorios buscando archivos fuente que compilar — todo módulo tiene que declararse con mod en algún punto alcanzable desde la raíz del crate, o el archivo simplemente nunca se compila, lo cual es un silencio confuso de depurar la primera vez que te topas con él.
Cuando un proyecto le queda chico a un solo crate —por ejemplo, una biblioteca principal, una CLI que depende de ella, y un crate de pruebas de integración que ejercita a ambas—, un workspace de Cargo te permite gestionarlos juntos en vez de como directorios sin relación. Un Cargo.toml de nivel superior con una sección [workspace] y una lista members une a los crates bajo un único Cargo.lock y un único directorio target/ compartido, de modo que una dependencia usada por dos crates del workspace se resuelve a una sola versión y se compila una vez en lugar de dos. Cada crate miembro sigue teniendo su propio Cargo.toml y se comporta como un crate normal por su cuenta; el workspace solo los coordina.
En conjunto, mod, pub y use son la forma en que un código base de Rust hace crecer una API interna en lugar de convertirse en un archivo gigantesco donde todo puede alcanzar a todo. Esa frontera la impone el compilador a costo cero en tiempo de ejecución, igual que los límites de trait y los lifetimes: Rust prefiere detectar una frontera de abstracción rota en tiempo de compilación antes que dejar que se convierta en un problema de depuración en producción. Con este vocabulario ya en su lugar, estás listo para la próxima lección: leer y escribir el tipo de código Rust del mundo real —organizado en módulos, exponiendo una superficie pública deliberada— en el que los smart pointers aparecen todo el tiempo.
mod front_of_house {pub mod hosting {pub fn add_to_waitlist() {println!("added to waitlist");}}}mod back_of_house {pub fn fix_incorrect_order() {// `super::` steps back up to the parent module — this works the// same way in every Rust edition.super::front_of_house::hosting::add_to_waitlist();}}fn main() {front_of_house::hosting::add_to_waitlist();back_of_house::fix_incorrect_order();}
A small module tree built with mod and pub, called both from the crate root and from a sibling module using super:: to step back up.
use std::collections::HashMap;fn count_words(text: &str) -> HashMap<&str, u32> {let mut counts = HashMap::new();for word in text.split_whitespace() {*counts.entry(word).or_insert(0) += 1;}counts}fn main() {let counts = count_words("the quick brown fox jumps over the lazy dog the fox runs");println!("{:?}", counts);}
use brings a standard-library path into scope so the rest of the function can write the short name instead of the full path every time.
mod shapes {// The struct is public, but its fields are not: outside code can// only build one through `new`, which can enforce invariants.pub struct Rectangle {width: f64,height: f64,}impl Rectangle {pub fn new(width: f64, height: f64) -> Rectangle {Rectangle { width, height }}pub fn area(&self) -> f64 {self.width * self.height}}}fn main() {let rect = shapes::Rectangle::new(3.0, 4.0);println!("area: {}", rect.area());// This line would fail to compile if uncommented: `width` is private.// println!("{}", rect.width);}
A public struct with private fields: outside code can only construct a Rectangle through the pub new constructor, which is how item-level pub and field-level privacy work together for encapsulation.
🧠 Comprueba tu comprensión
0/1 · 0/1 answered1. You add a new file src/tables.rs to a binary crate, expecting its public functions to become available elsewhere in the project. Nothing changes — the compiler doesn't even seem to notice the file. What's missing?