Módulos do Rust

Descomplicando a organização do seu código

·

10 min read

Quando você começa a programar com Rust e precisa dividir o código em vários arquivos e subpastas, você logo se depara com o conceito de módulos da linguagem.

Não importa qual tecnologia ou linguagem a gente usa, sempre vamos ter a tendência de organizar nossas ideias e conceitos em pastas e arquivos diferentes. Isso ajuda a manter a organização, deixar partes lógicas separadas, e facilitar quando o projeto começa a crescer.

Ao criar essas pastas em um projeto Rust, a gente logo percebe que as coisas funcionam um pouco diferente: alguém que está acostumado a programar em C, por exemplo, provavelmente vai ficar tentando importar os arquivos diretamente para trazer os tipos e namespaces. Depois de dar uma "fuçada" na ajuda da IDE ou procurar na internet, a gente descobre que Rust raciocina em módulos, não arquivos.

Considerando que os crates são sobre compartilhamento de código entre projetos,**módulos são sobre organização de código dentro de um projeto. - Livro Programação em Rust (novatec).

O Rust inteiro é baseado em módulos que fazem parte de uma árvore de módulos. Não existe esse negócio de simplesmente importar um arquivo.

Módulos em Rust

Alguns conceitos importantes do sistema de módulos do Rust são: packages, crates, módulos e paths. Nesse artigo, vamos focar em módulos e paths, especialmente sobre como usá-los em vários arquivos e deixar tudo funcionando junto.

Explicações:

  • package: Um pacote Rust é um jeito de agrupar crates relacionados. É como se fosse uma "caixa" maior onde você guarda os seus projetos.

  • crate: É a unidade de compilação do Rust. Pode ser pensado como uma biblioteca ou um executável.

  • modules: São blocos de código organizados dentro do seu projeto. Servem para separar funcionalidades e deixar o código mais lógico.

  • paths: São como os "endereços" dos diferentes módulos e itens dentro do seu projeto. É como o Rust sabe onde encontrar cada coisa.


Criando o projeto base

O jeito mais fácil de começar um novo projeto em Rust é usar um template na sua IDE preferida (ou até pelo terminal mesmo). Um projeto simples provavelmente vai ter uma estrutura parecida com essa:

A parte interessante, pensando em módulos, fica dentro da pasta src. Olhando a estrutura de arquivos dentro dela, vamos encontrar:

Por enquanto, não tem muito segredo: é só o arquivo main.rs. Esse arquivo é o ponto de entrada do nosso projeto, contendo a função main que costuma começar assim:

fn main() {
    println!("Hello, world!");
}

Os arquivossrc/main.rs(pra executáveis) ou src/lib.rs(pra bibliotecas) são chamadas de "raízes do crate" (crate roots).

Basicamente, o main.rs ou o lib.rs são os arquivos que "ponto de entrada" (entry point) para o compilador (rustc) entrar no seu pacote Rust.

A gente consegue, claro, compilar e rodar o projeto vazio sem problema – como era de se esperar. É aqui que a gente arma nossa "base" pra começar a escalada e falar sobre como lidar com os módulos.


O Plano

A ideia é que nosso projeto tenha dois módulos que vão ser importados a partir do src/main.rs. Esses módulos vão ter funções públicas (que podem ser usadas fora do módulo) e também partes privadas (só visíveis dentro do próprio módulo). O projeto também vai servir pra mostrar como módulos podem usar as funcionalidades de outros módulos.

Os dois módulos são: uma central elétrica gerando energia, e uma torre que consome essa energia.

Vamos pensar na crate root (src/main.rs) como uma rua. A gente quer construir dois prédios lado a lado: o prédio da torre, e o prédio da central elétrica. Esses prédios vão ser nossos dois módulos. O nosso projeto tem dependências: a rua e os prédios dependem uns dos outros, o prédio da central precisa gerar energia, e o prédio da torre precisa consumir essa energia pra funcionar.

Adicionando arquivos e pastas ao projeto

Quando criamos um módulo que queremos dividir em várias pastas e arquivos, precisamos criar uma pasta e um arquivo lado a lado com a nossa raiz do crate (ou seja, junto de main.rs ou lib.rs). O nome da pasta e do arquivo precisam ser idênticos, e vão ser o nome do nosso módulo. Por exemplo, se criamos o módulo example_module, a estrutura vai ficar assim:

No nosso caso, como vamos criar dois prédios, ou seja, dois módulos, precisamos criar duas pastas e dois arquivos. A pasta vai ficar mais ou menos assim:

A estrutura de pastas no diretório "src" do projeto

A cola que junta tudo

Claro que a gente ainda precisa botar um pouco de código pra unir essas pastas e arquivos. Um módulo, que é representado por uma pasta, pode ter vários arquivos dentro. Esses arquivos podem definir novos types ou symbols no Rust. A única coisa que falta é como a gente vai unir todas essas partes. Como a gente conecta os prédios à rua?

Rust usa uma "árvore de módulos" (module tree) para entender onde tá cada parte do código na hora de compilar.

Como a gente viu antes, a raiz dessa árvore é o nosso arquivo src/main.rs (ou src/lib.rs, pra bibliotecas). Então pra adicionar outra parte à essa "árvore", a gente precisa dizer a partir dessa "raiz" que tem outro "galho" ali. E por isso a gente criou aquele arquivo com o mesmo nome da pasta do módulo.

O jeito de fazer o Rust "enxergar" o módulo example_module é assim:

Veja os arquivos
-------------

main.rs     --> examplary_module.rs   --> a.rs
                                      --> b.rs

Vistos como módulos
---------------

crate_root  --> mod exemplary_module  -->       mod a {}
                                      --> pub   mod b {}
  • crate_root: Refere-se ao arquivo principal do seu programa (main.rs). Esse é o ponto de partida do compilador.

  • mod exemplary_module O arquivo (exemplary_module.rs) e a pasta (exemplary_module/) representam o módulo que estamos criando.

  • mod a {} Define o submódulo a dentro do módulo exemplary_module.

  • pub mod b {} Define o submódulo b dentro do módulo exemplary_module, mas usando pub para torná-lo público (acessível de fora do módulo).

Vamos de código

A estrutura da "árvore de módulos" é definida pela organização de pastas, o arquivo contendo o módulo ao lado, e pelo código que vai nos arquivos. O código, começando em src/auxiliary_building/auxiliary.rs, passando por src/auxiliary_building.rs, e terminando em src/main.rs seria mais ou menos isso:

// --- auxiliary.rs ---
// Assim como o módulo que está dentro de auxiliary_building.rs, 
// esta função é privada por padrão. 
// O uso de "pub" torna ela visível de fora do módulo.
pub fn generate_energy() {
}
// --- auxiliary_building.rs ---
// A função generate_energy, chamada no código em main.rs está 
// definida dentro do arquivo auxiliary.rs 
// (como vamos ver no próximo bloco). 
// Para cada arquivo que a gente adiciona, precisamos botar 
// a expressão mod <file>.
// O que o pub faz? O pub torna o módulo visível para qualquer 
// um que for usar esse módulo 
// (que, no nosso caso, é o crate principal, ou seja, o arquivo main.rs). 
// Se a gente não tivesse colocado o pub na frente do mod <file>, 
// esse código só ficaria visível dentro do próprio módulo.
pub mod auxiliary;
// --- main.rs ---
// trazendo o módulo auxiliary_building para este escopo
mod auxiliary_building;
// em vez de usar o caminho completo do módulo, 
// agora podemos usar o alias "aux"
use auxiliary_building::auxiliary as aux;

fn main() {
    println!("Ligando as máquinas da torre!");
    // uma função dentro do módulo auxiliary_building
    aux::generate_energy();
}
  • mod auxiliary_building; Traz o módulo para o escopo, significando que você consegue utilizá-lo neste arquivo.

  • use auxiliary_building::auxiliary as aux; Cria um atalho ("alias") para uma parte do caminho do módulo, facilitando escrever o código sem precisar do nome completo toda hora.

Mas o que é essa parada de :: ?

Os dois pontos (::) são usados para separar as partes de um path . É como se fosse um endereço para chegar num recurso do projeto, separando tudo direitinho. A gente vai falar mais sobre isso depois.

A mesma coisa vale pro nosso segundo módulo, ou seja, o segundo prédio da rua lá, o prédio da torre.

O panorama geral

A "raiz do crate" (src/main.rs) vai acessar o módulo tower_building usando as palavras mod e use

. O módulo tower_building define tudo que tem dentro dele em dois arquivos: src/tower_building.rs e src/tower_building/tower.rs. Aí, os symbols definidos no arquivo src/tower_building/tower.rs podem ser usados dentro do src/main.rs fechando o ciclo.

Olha o código como um todo:

pub fn generate_energy() {}
// --- auxiliary.rs ---
pub mod auxiliary;
// --- auxiliary_building.rs ---
mod auxiliary_building;
mod tower_building;

use auxiliary_building::auxiliary as aux;
use tower_building::tower as tower;

fn main() {
    println!("Startup of Tower");

    aux::generate_energy();
    tower::start_consumption();
}
// --- main.rs ---
pub fn start_consumption() {}
// --- tower.rs ---
pub mod tower;
// --- tower_building.rs ---

E os submódulos?

Se quisermos adicionar uma pasta dentro de auxiliary_building, como por exemplo, um módulo chamado plug, fazemos exatamente a mesma coisa de antes: criamos uma pasta e o arquivo do módulo ao lado. Ficaria assim:

Tendo criado as pastas e arquivos no sistema, a gente precisa adicionar a "cola" para o sistema de módulos. Como o submódulo plug está dentro do módulo auxiliary_building, temos que ajustar o arquivo src/auxiliary_building.rs pra essa mudança. A gente adiciona o módulo plug assim:

pub mod auxiliary;
// Adicionamos o submódulo plug. 
// O sistema de módulos vai procurar por um arquivo plug.rs.
pub mod plug;
// --- auxiliary_building.rs ---

Beleza, depois de fazer isso, a gente pode usar qualquer coisa que esteja criada dentro da pasta src/auxiliary_building/plug direto no nosso arquivo src/main.rs. Pra ter uma ideia, eu fiz uma função no device.rs e outra no other_device.rs, e agora dá pra chamar as duas lá do main.rs:

mod auxiliary_building;
mod tower_building;

use auxiliary_building::auxiliary as aux;

fn main() {
    println!("Startup of Tower");
    aux::generate_energy();

    auxiliary_building::plug::device::do_it();
    auxiliary_building::plug::other_device::do_it_with_other();
}
// --- main.rs ---

E se o arquivo auxiliary.rs ali dentro de src/auxiliary_building fosse ficando maior? Adivinha só: a gente pode criar uma pasta nova chamada auxiliary, botar pedaços de código em arquivos diferentes dentro dessa pasta e declarar funções e symbols públicos no arquivo auxiliary.rs. Aí, se tiver algum módulo que já usa esse código, é só ajustar os caminhos (paths) certinho.


Paths, ou como acessar um módulo de outro módulo

A gente já usou paths: Os paths do Rust já estavam fazendo o trabalho deles quando a gente chamou a função do nosso módulo lá a partir do "raiz do crate" (src/main.rs).

Existem dois tipos de paths: absoluto e relativo.

  • Path absoluto: Por exemplo, essa chamada auxiliary_building::auxiliary::generate_energy() usa um path absoluto. Eles sempre começam do ponto de vista do "raiz do crate" (src/main.rs é a raiz, é onde a árvore de módulos começa). Se quisermos usar um caminho absoluto em qualquer lugar do nosso projeto, dá pra usar o literal crate.

  • Path relativo: Funciona a partir do ponto de vista do módulo atual, ou seja, o módulo onde você tá escrevendo o caminho. Pode começar com os literais self ou super.

    • O self começa a partir do ponto atual da "árvore de módulos".

    • O super "sobe um nível" na árvore, é como voltar pra pasta anterior no sistema de arquivos.

Pra resumir, temos três literais pra começar um path: crate (absoluto), self e super (relativo).

Olha um exemplo demonstrando o uso de cada um:

// path absoluto, começando da "raiz do crate"
use crate::auxiliary_building::plug::device as from_crate;

// path relativo, subindo na árvore de módulos
use super::super::auxiliary_building::plug::device as with_super;

// Não criamos outros símbolos no nosso exemplo pequeno, 
// mas a gente consegue acessar os outros símbolos que 
// já criamos com os comandos acima. 
// Aqui, damos o nome "with_self" e podemos começar 
// a usar esse caminho também, que começa no módulo atual.
use self::with_super as with_self;

pub fn start_consumption() {     
    // path absoluto
    from_crate::do_it(); 
    // paths relativos    
    with_super::do_it();    
    with_self::do_it();
}
// --- tower.rs ---

Conclusão

É, o jeito do Rust lidar com código, trazendo tudo junto, é bem diferente do que a gente tá acostumado... Mas no fim das contas, não é tão difícil de se acostumar.

Nesse exemplo, a gente viu como os arquivos se conectam com os módulos no Rust, e como o Rust “entende” essa árvore de módulos definidos em pastas e arquivos separados. Também falamos sobre os caminhos (ou “paths”), que ajudam a resolver dependências entre módulos, funções, etc.

https://github.com/tiagonevestia/rust-mod

Referências ✍

Explaining Rust’s Modules

The Rust Reference

Did you find this article valuable?

Support Tiago Neves by becoming a sponsor. Any amount is appreciated!