Módulos são a base da organização de código em Rust. Eles permitem agrupar funções, structs, enums, constantes e até outros módulos, criando uma hierarquia que reflete a estrutura lógica do seu projeto. Sem módulos, todo o código ficaria em um único arquivo, o que é inviável para projetos reais. Com eles, você pode dividir o código em partes menores, reutilizáveis e de fácil navegação.

Além de organizar, os módulos controlam a visibilidade: o que é público (acessível fora do módulo) e o que é privado (acessível apenas dentro do módulo). Isso é essencial para encapsulamento e para criar APIs limpas. Nesta aula, veremos como declarar módulos com mod, como expor itens com pub, como importar caminhos com use e como navegar entre módulos usando caminhos absolutos e relativos.

mod

A palavra-chave mod é usada para declarar um módulo. Um módulo pode ser definido inline no mesmo arquivo ou em um arquivo separado. Quando você escreve mod nome; dentro de um arquivo, o Rust procura por um arquivo nome.rs ou nome/mod.rs no mesmo diretório. Isso permite dividir o código em múltiplos arquivos.

Exemplo de módulo inline:

mod matematica {
    pub fn soma(a: i32, b: i32) -> i32 {
        a + b
    }
}

fn main() {
    println!("{}", matematica::soma(2, 3));
}

Neste exemplo, definimos um módulo chamado matematica com uma função pública soma. Para acessar a função, usamos o caminho matematica::soma. Sem o pub, a função não seria acessível fora do módulo, causando um erro de compilação.

Para módulos em arquivos separados, crie um arquivo matematica.rs com o mesmo conteúdo da função e, no arquivo principal, declare mod matematica;. O Rust automaticamente carrega o arquivo. Isso é fundamental para manter o código organizado em projetos maiores.

pub

A visibilidade em Rust é privada por padrão. Isso significa que itens (funções, structs, campos, etc.) definidos em um módulo só podem ser acessados dentro do próprio módulo e seus submódulos. Para tornar um item acessível fora do módulo, usamos a palavra-chave pub.

Exemplo:

mod mensagens {
    // Função privada - acessível apenas dentro de 'mensagens'
    fn formatar(texto: &str) -> String {
        format!("Mensagem: {}", texto)
    }

    // Função pública - acessível de fora
    pub fn exibir(texto: &str) {
        println!("{}", formatar(texto));
    }
}

fn main() {
    // mensagens::formatar("Olá"); // Erro: função privada
    mensagens::exibir("Olá"); // OK
}

No exemplo, formatar é privada, então não pode ser chamada de fora. exibir é pública e pode ser chamada. Esse controle é crucial para esconder detalhes de implementação e expor apenas o que é necessário.

Para structs, você precisa marcar cada campo como pub se quiser que eles sejam acessíveis externamente. Caso contrário, a struct pode ser construída, mas os campos não podem ser lidos ou alterados.

mod pessoa {
    pub struct Pessoa {
        pub nome: String,
        idade: u8, // privado
    }

    impl Pessoa {
        pub fn nova(nome: String, idade: u8) -> Self {
            Self { nome, idade }
        }

        pub fn idade(&self) -> u8 {
            self.idade
        }
    }
}

fn main() {
    let p = pessoa::Pessoa::nova("Ana".to_string(), 30);
    println!("Nome: {}", p.nome); // OK
    // println!("{}", p.idade); // Erro: campo privado
    println!("Idade: {}", p.idade()); // OK via método
}

Aqui, o campo nome é público, mas idade é privado. Para acessar a idade, fornecemos um método público idade(). Isso é uma prática comum para encapsulamento.

use

A palavra-chave use permite trazer itens para o escopo atual, evitando escrever caminhos completos toda vez. Isso melhora a legibilidade e reduz repetição.

Exemplo sem use:

mod matematica {
    pub fn soma(a: i32, b: i32) -> i32 { a + b }
}

fn main() {
    println!("{}", matematica::soma(1, 2));
    println!("{}", matematica::soma(3, 4));
}

Com use:

mod matematica {
    pub fn soma(a: i32, b: i32) -> i32 { a + b }
}

use matematica::soma;

fn main() {
    println!("{}", soma(1, 2));
    println!("{}", soma(3, 4));
}

Você também pode usar use para importar múltiplos itens ou usar as para renomear:

use matematica::{soma, multiplicacao};
use matematica::soma as adicionar;

Além disso, use pode ser usado com caminhos relativos e absolutos. Por padrão, use assume caminhos absolutos a partir da crate root, mas você pode usar self ou super para caminhos relativos.

Caminhos absolutos e relativos

Em Rust, existem duas formas de se referir a itens em módulos: caminhos absolutos e relativos. Caminhos absolutos começam com crate (para a raiz do crate atual) ou com o nome de uma crate externa. Caminhos relativos começam com self (módulo atual), super (módulo pai) ou o nome de um item no módulo atual.

Exemplo de estrutura:

mod a {
    pub fn funcao_a() {}
    pub mod b {
        pub fn funcao_b() {}
        pub mod c {
            pub fn funcao_c() {}
        }
    }
}

Para acessar funcao_c de dentro de main (raiz), podemos usar caminho absoluto: crate::a::b::c::funcao_c() ou caminho relativo: a::b::c::funcao_c() (a partir da raiz, ambos são equivalentes).

Dentro de um módulo, você pode usar super para se referir ao módulo pai. Por exemplo, dentro de c, para acessar funcao_a, você pode usar super::super::funcao_a (subindo dois níveis).

Exemplo prático:

mod a {
    pub fn funcao_a() { println!("a"); }
    pub mod b {
        pub fn funcao_b() { println!("b"); }
        pub mod c {
            pub fn funcao_c() {
                // Caminho absoluto
                crate::a::funcao_a();
                // Caminho relativo
                super::super::funcao_a();
                super::funcao_b();
            }
        }
    }
}

fn main() {
    // Caminhos a partir da raiz
    a::b::c::funcao_c();
    // Caminho absoluto
    crate::a::b::c::funcao_c();
}

Entender a diferença entre caminhos absolutos e relativos é importante para escrever código que seja robusto a refatorações. Caminhos absolutos são mais explícitos e menos propensos a quebrar se a hierarquia mudar, mas são mais longos. Caminhos relativos são mais curtos, mas podem quebrar se você mover o módulo.

Boas práticas

Ao trabalhar com módulos, siga estas boas práticas:

  • Use módulos para agrupar funcionalidades relacionadas, não para criar hierarquias profundas desnecessárias.
  • Exponha apenas o necessário com pub; mantenha detalhes internos privados.
  • Prefira caminhos absolutos (crate::...) em código de nível superior para evitar ambiguidades.
  • Use use para encurtar caminhos repetidos, mas evite importar itens que você não usa.
  • Organize módulos em arquivos separados para projetos maiores, seguindo a convenção de diretórios.

Referências

Exercícios

  1. Crie um módulo chamado geometria que contenha uma função pública area_retangulo(largura: f64, altura: f64) -> f64 e uma função privada dobro(x: f64) -> f64. No main, chame a função pública e tente chamar a privada (comente a chamada que causa erro).

    ✓ Resposta:
    mod geometria {
        pub fn area_retangulo(largura: f64, altura: f64) -> f64 {
            largura * altura
        }
    
        fn dobro(x: f64) -> f64 {
            x * 2.0
        }
    }
    
    fn main() {
        println!("Área: {}", geometria::area_retangulo(3.0, 4.0));
        // println!("{}", geometria::dobro(2.0)); // Erro: função privada
    }
  2. Defina um módulo saudacao com uma struct pública Pessoa que tenha um campo público nome e um campo privado idade. Implemente um método público new(nome: String, idade: u8) -> Pessoa e um método público idade(&self) -> u8. No main, crie uma instância e acesse o nome e a idade via método.

    ✓ Resposta:
    mod saudacao {
        pub struct Pessoa {
            pub nome: String,
            idade: u8,
        }
    
        impl Pessoa {
            pub fn new(nome: String, idade: u8) -> Pessoa {
                Pessoa { nome, idade }
            }
    
            pub fn idade(&self) -> u8 {
                self.idade
            }
        }
    }
    
    fn main() {
        let p = saudacao::Pessoa::new("João".to_string(), 25);
        println!("Nome: {}", p.nome);
        println!("Idade: {}", p.idade());
    }
  3. Usando use, importe duas funções de um módulo operacoes (por exemplo, soma e multiplicacao) e chame-as diretamente no main. Depois, importe uma delas com um alias usando as.

    ✓ Resposta:
    mod operacoes {
        pub fn soma(a: i32, b: i32) -> i32 { a + b }
        pub fn multiplicacao(a: i32, b: i32) -> i32 { a * b }
    }
    
    use operacoes::{soma, multiplicacao};
    use operacoes::soma as adicionar;
    
    fn main() {
        println!("{}", soma(2, 3));
        println!("{}", multiplicacao(2, 3));
        println!("{}", adicionar(4, 5));
    }
  4. Crie uma hierarquia de módulos: a contém b, que contém c. Dentro de c, crie uma função que chame uma função definida em a usando um caminho absoluto e outra usando um caminho relativo com super.

    ✓ Resposta:
    mod a {
        pub fn funcao_a() { println!("a"); }
        pub mod b {
            pub mod c {
                pub fn funcao_c() {
                    // Caminho absoluto
                    crate::a::funcao_a();
                    // Caminho relativo
                    super::super::funcao_a();
                }
            }
        }
    }
    
    fn main() {
        a::b::c::funcao_c();
    }
  5. Escreva um programa que declare um módulo inline config com uma constante pública VERSAO: &str = "1.0.0" e uma função pública exibir() que imprime a versão. No main, importe a constante e a função com use e chame-as.

    ✓ Resposta:
    mod config {
        pub const VERSAO: &str = "1.0.0";
    
        pub fn exibir() {
            println!("Versão: {}", VERSAO);
        }
    }
    
    use config::{VERSAO, exibir};
    
    fn main() {
        println!("Constante: {}", VERSAO);
        exibir();
    }