Bem-vindo à aula sobre módulos e visibilidade em Rust! Este é um dos pilares da organização de código na linguagem. Em qualquer projeto que vá além de um arquivo simples, você precisará dividir o código em módulos para manter tudo legível, reutilizável e com fronteiras claras de responsabilidade. Nesta aula, vamos mergulhar no sistema de módulos do Rust, que é ao mesmo tempo poderoso e um pouco diferente do que você pode estar acostumado em outras linguagens.

O sistema de módulos do Rust é baseado em arquivos e diretórios, mas também permite declarar módulos inline. A visibilidade (quem pode acessar o quê) é controlada por palavras-chave como pub e crate. Vamos entender como tudo isso se encaixa, desde a declaração de um módulo até a importação de itens com use. Prepare-se para ver muitos exemplos práticos, porque a melhor forma de aprender isso é na prática.

mod

A palavra-chave mod é usada para declarar um módulo. Um módulo é uma coleção de itens: funções, structs, enums, traits, constantes, e até mesmo outros módulos. Eles ajudam a agrupar código relacionado e a controlar a visibilidade. Por padrão, tudo dentro de um módulo é privado, ou seja, só é acessível dentro do próprio módulo e seus submódulos.

Existem duas formas de declarar um módulo: inline, usando mod com um bloco, ou em um arquivo separado. Vamos ver a forma inline primeiro:

mod math {
    fn add(a: i32, b: i32) -> i32 {
        a + b
    }
}

fn main() {
    // Isso não compila, pois add é privado
    // println!("{}", math::add(2, 3));
}

Neste exemplo, criamos um módulo math com uma função add. Como não usamos pub, a função é privada e não pode ser acessada de fora do módulo. Para acessar, precisamos tornar a função pública com pub, o que veremos em seguida.

Quando o projeto cresce, é comum colocar cada módulo em um arquivo separado. Por exemplo, você pode ter um arquivo math.rs e declará-lo no main.rs com mod math;. O Rust procura o arquivo math.rs no mesmo diretório do arquivo que contém a declaração. Para módulos dentro de módulos, a estrutura de diretórios segue o mesmo padrão.

// main.rs
mod math;

fn main() {
    println!("{}", math::add(2, 3));
}
// math.rs
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

Nesse caso, a função add é pública (pub) e pode ser acessada de main. Note que o módulo math em si é privado por padrão, mas como estamos no mesmo crate, isso não impede o acesso. Se quiséssemos que o módulo math fosse acessível de fora do crate (por exemplo, se fosse uma biblioteca), precisaríamos declarar pub mod math;.

pub

A palavra-chave pub é usada para tornar um item público, ou seja, acessível fora do módulo onde ele está definido. Sem pub, tudo é privado. Isso é uma das grandes vantagens do Rust: o controle de visibilidade é explícito e rigoroso.

Vamos expandir o exemplo anterior e ver como pub funciona em diferentes níveis:

mod outer {
    pub fn public_fn() {
        println!("Chamando função pública");
    }

    fn private_fn() {
        println!("Chamando função privada");
    }

    pub mod inner {
        pub fn inner_public() {
            println!("Função pública interna");
        }

        fn inner_private() {
            println!("Função privada interna");
        }
    }
}

fn main() {
    outer::public_fn(); // OK
    // outer::private_fn(); // Erro: private_fn é privada
    outer::inner::inner_public(); // OK
    // outer::inner::inner_private(); // Erro: inner_private é privada
}

Além de pub, existem outras formas de visibilidade: pub(crate) torna o item público apenas dentro do crate, pub(super) torna visível para o módulo pai, e pub(in path) permite especificar um caminho exato. Isso é útil para APIs públicas de bibliotecas, onde você quer expor apenas o necessário.

Por exemplo, se você está criando uma biblioteca, pode usar pub(crate) para itens que são usados internamente, mas não devem ser expostos aos usuários da biblioteca. Isso ajuda a manter a API limpa e evita que usuários dependam de detalhes internos que podem mudar.

pub mod library {
    pub fn public_api() {}

    pub(crate) fn internal_helper() {}
}

Neste caso, internal_helper é acessível dentro do crate, mas não para quem usa a biblioteca externamente.

use

A palavra-chave use é usada para trazer caminhos para o escopo atual, facilitando a referência a itens sem precisar digitar o caminho completo toda vez. É como um atalho.

Por exemplo, em vez de escrever math::add toda vez, você pode usar use math::add; e depois chamar apenas add.

mod math {
    pub fn add(a: i32, b: i32) -> i32 {
        a + b
    }
}

use math::add;

fn main() {
    println!("{}", add(2, 3)); // agora podemos chamar add diretamente
}

Você também pode usar use com chaves para importar vários itens de um mesmo módulo:

mod math {
    pub fn add(a: i32, b: i32) -> i32 { a + b }
    pub fn sub(a: i32, b: i32) -> i32 { a - b }
}

use math::{add, sub};

fn main() {
    println!("{}", add(2, 3));
    println!("{}", sub(5, 2));
}

E ainda, pode usar self para incluir o próprio módulo, e * para importar tudo que é público (embora isso não seja recomendado, pois pode poluir o escopo e causar ambiguidades).

use math::*;

Além disso, use também pode ser usado para criar aliases com as. Isso é útil quando dois itens têm o mesmo nome ou quando você quer encurtar um caminho longo.

use std::collections::HashMap as Map;

fn main() {
    let mut map = Map::new();
    map.insert("chave", 42);
}

Outra característica importante do use é que ele é lexical, ou seja, só vale no escopo onde foi declarado. Se você declarar um use dentro de uma função, ele só é válido dentro daquela função.

Caminhos absolutos e relativos

Para referenciar itens em módulos, usamos caminhos. Existem dois tipos: absolutos e relativos. Caminhos absolutos começam com crate (a raiz do crate atual) ou o nome de uma crate externa. Caminhos relativos começam com self, super ou um identificador.

No Rust 2018 e posteriores, os caminhos absolutos geralmente começam com crate para o crate atual, enquanto que antes usavam ::. Por exemplo:

crate::math::add(2, 3)

Isso significa: a partir da raiz do crate, vá para o módulo math e chame add. Caminhos relativos usam self para o módulo atual e super para o módulo pai. Por exemplo:

mod math {
    pub fn add(a: i32, b: i32) -> i32 {
        a + b
    }

    pub fn add_twice(a: i32, b: i32) -> i32 {
        // Caminho relativo: self::add
        self::add(a, b) + self::add(a, b)
    }
}

Também podemos usar super para acessar o módulo pai. Por exemplo, dentro de um módulo aninhado, podemos chamar funções do módulo pai com super::.

mod outer {
    pub fn foo() { println!("foo"); }

    pub mod inner {
        pub fn bar() {
            // chama a função do módulo pai
            super::foo();
        }
    }
}

fn main() {
    outer::inner::bar();
}

Na prática, você vai usar mais caminhos absolutos com crate quando estiver dentro do crate, e caminhos relativos quando estiver em módulos aninhados e quiser referenciar o pai ou o atual. A escolha entre absoluto e relativo depende do contexto e da legibilidade.

Uma dica: em projetos grandes, é comum usar use com caminhos absolutos no início dos arquivos, para evitar repetição e facilitar a leitura. Por exemplo, em vez de escrever crate::utils::helpers::format várias vezes, você pode fazer use crate::utils::helpers::format; no topo do arquivo.

Boas práticas e observações finais

Organizar o código em módulos é uma arte. Aqui vão algumas boas práticas:

  • Mantenha módulos pequenos e focados: cada módulo deve ter uma responsabilidade clara.
  • Use pub apenas para o que é realmente necessário expor. Isso reduz a superfície de API e evita que usuários dependam de detalhes internos.
  • Prefira caminhos absolutos com crate para evitar ambiguidades, especialmente em módulos aninhados.
  • Use use para encurtar caminhos longos, mas evite use * em módulos grandes, pois pode causar conflitos de nomes.
  • Coloque as declarações use no topo do arquivo, em ordem alfabética ou agrupadas, para facilitar a leitura.

Lembre-se de que o sistema de módulos do Rust é estático e resolvido em tempo de compilação, o que significa que erros de caminho são detectados cedo. Aproveite isso para refatorar sem medo.

Referências

Exercícios

  1. Crie um módulo chamado greetings com uma função pública hello que imprime "Olá, mundo!". Depois, chame essa função a partir do main usando caminho absoluto com crate.

✓ Resposta:
mod greetings {
    pub fn hello() {
        println!("Olá, mundo!");
    }
}

fn main() {
    crate::greetings::hello();
}
  • No módulo greetings do exercício anterior, adicione uma função privada internal_helper que imprime "Ajuda interna". Tente chamá-la de fora e veja o erro. Depois, torne-a pública e chame-a.
  • ✓ Resposta:
    mod greetings {
        pub fn hello() {
            println!("Olá, mundo!");
        }
    
        // privada
        fn internal_helper() {
            println!("Ajuda interna");
        }
    }
    
    fn main() {
        crate::greetings::hello();
        // crate::greetings::internal_helper(); // erro: private function
    }
    

    Para torná-la pública, basta adicionar pub:

    pub fn internal_helper() {
        println!("Ajuda interna");
    }
    
  • Use a palavra-chave use para importar a função hello do módulo greetings e chamá-la sem o prefixo greetings::. Faça isso dentro da função main.
  • ✓ Resposta:
    mod greetings {
        pub fn hello() {
            println!("Olá, mundo!");
        }
    }
    
    fn main() {
        use crate::greetings::hello;
        hello();
    }
    
  • Crie dois módulos: outer e outer::inner. No módulo inner, defina uma função pública print_message que imprime "Mensagem do inner". No módulo outer, defina uma função pública call_inner que chama inner::print_message usando caminho relativo com self.
  • ✓ Resposta:
    mod outer {
        pub mod inner {
            pub fn print_message() {
                println!("Mensagem do inner");
            }
        }
    
        pub fn call_inner() {
            self::inner::print_message();
        }
    }
    
    fn main() {
        outer::call_inner();
    }
    
  • Explique a diferença entre caminhos absolutos e relativos. Dê um exemplo de cada um dentro de um módulo aninhado.
  • ✓ Resposta: Caminhos absolutos começam a partir da raiz do crate (ou de uma crate externa), usando crate ou o nome da crate. Por exemplo, crate::outer::inner::print_message(). Caminhos relativos começam a partir do módulo atual, usando self (módulo atual) ou super (módulo pai). Por exemplo, dentro de outer, podemos usar self::inner::print_message() ou super::outer::inner::print_message() se estivermos em um módulo mais profundo. A diferença é que absolutos são independentes da posição atual, enquanto relativos dependem do contexto.