Funções, Expressões e Como Rust Pensa Diferente sobre Retorno de Valores

Se você vem de Python, JavaScript ou Java, já sabe o que é uma função. Mas Rust tem uma visão particular sobre funções que vai te surpreender — especialmente a distinção entre statements e expressions, e a forma como valores são retornados. É sutil no começo, mas muda profundamente a forma como você escreve código.


Declarando funções

Funções em Rust são declaradas com fn, seguido do nome, parâmetros e corpo entre chaves. Por convenção, nomes de funções usam snake_case:

fn saudacao() {
    println!("Olá, mundo do Rust!");
}

fn main() {
    saudacao();
}

Diferente de Python ou JavaScript, a ordem de declaração das funções não importa em Rust. Você pode chamar uma função antes de declará-la no arquivo — o compilador encontra tudo antes de gerar o código.


Parâmetros e tipos obrigatórios

Em Rust, toda função deve declarar explicitamente o tipo de cada parâmetro. Não há inferência aqui — essa é uma decisão deliberada. Funções são contratos, e contratos precisam ser explícitos:

fn somar(a: i32, b: i32) {
    println!("{a} + {b} = {}", a + b);
}

fn main() {
    somar(10, 32);
}

Múltiplos parâmetros são separados por vírgula, cada um com sua própria anotação de tipo. Não existe sintaxe como a, b: i32 para compartilhar o tipo — cada parâmetro é declarado individualmente. Isso torna a assinatura mais verbosa, mas infinitamente mais legível.


Statements vs. Expressions — a distinção que muda tudo

Aqui começa a parte que mais confunde quem vem de outras linguagens.

Em Rust, há uma diferença fundamental entre:

  • Statement — uma instrução que executa uma ação e não retorna valor
  • Expression — um trecho de código que produz um valor

Em linguagens como C ou Java, essa distinção existe mas é informal. Em Rust, ela é parte da gramática da linguagem.

Veja o exemplo:

fn main() {
    let x = 5;          // statement: não produz valor
    let y = {
        let a = 3;
        a + 2           // expression: produz 5 (sem ponto e vírgula!)
    };

    println!("y = {y}"); // y = 5
}

O bloco { let a = 3; a + 2 } é uma expression. Ele avalia para 5, que é atribuído a y. A linha a + 2 não tem ponto e vírgula — e isso é crucial. Adicionar ; ao final de uma expression a transforma em um statement, e ela passa a retornar () — o tipo unitário vazio de Rust.

Esse detalhe de ponto e vírgula é responsável por uma boa parte da confusão inicial de quem aprende Rust. Guarde bem: ausência de ponto e vírgula na última linha de um bloco significa retorno implícito daquele valor.


Funções com retorno de valor

Para declarar que uma função retorna um valor, use -> seguido do tipo após os parênteses:

fn quadrado(n: i32) -> i32 {
    n * n   // sem ponto e vírgula: retorno implícito
}

fn main() {
    let resultado = quadrado(7);
    println!("7² = {resultado}"); // 49
}

Não há palavra-chave return aqui. A última expressão do corpo da função é automaticamente o valor retornado. Esse estilo é idiomático em Rust e você vai vê-lo em praticamente todo código da linguagem.

O return explícito existe, mas é reservado para retornos antecipados — quando você quer sair da função antes de chegar ao final:

fn dividir(a: f64, b: f64) -> f64 {
    if b == 0.0 {
        return 0.0; // saída antecipada
    }
    a / b // retorno normal ao final
}

fn main() {
    println!("{}", dividir(10.0, 2.0)); // 5
    println!("{}", dividir(10.0, 0.0)); // 0
}

Funções que não retornam valor

Quando uma função não retorna nada significativo, seu tipo de retorno é () — pronunciado "unit". Você raramente escreve isso explicitamente, pois é implícito quando não há ->:

fn imprimir_linha(texto: &str) {
    println!("{texto}");
    // retorna () implicitamente
}

É equivalente a:

fn imprimir_linha(texto: &str) -> () {
    println!("{texto}");
}

A segunda forma existe mas é considerada redundante. Evite-a.


Funções como expressões

Como tudo em Rust tende a ser uma expression, chamadas de função também produzem valores e podem ser usadas diretamente:

fn celsius_para_fahrenheit(c: f64) -> f64 {
    c * 1.8 + 32.0
}

fn main() {
    let temperaturas = [0.0, 20.0, 37.0, 100.0];

    for temp in temperaturas {
        println!("{temp}°C = {:.1}°F", celsius_para_fahrenheit(temp));
    }
}

Saída:

0°C = 32.0°F
20°C = 68.0°F
37°C = 98.6°F
100°C = 212.0°F

Múltiplos valores de retorno com tuplas

Rust não suporta múltiplos valores de retorno nativamente — mas tuplas resolvem isso com elegância:

fn minmax(lista: &[i32]) -> (i32, i32) {
    let mut min = lista[0];
    let mut max = lista[0];

    for &valor in lista {
        if valor < min { min = valor; }
        if valor > max { max = valor; }
    }

    (min, max) // retorna a tupla
}

fn main() {
    let numeros = [3, 7, 1, 9, 4, 6, 2, 8, 5];
    let (minimo, maximo) = minmax(&numeros);
    println!("Mínimo: {minimo}, Máximo: {maximo}");
}

Saída:

Mínimo: 1, Máximo: 9

Note o &[i32] — isso é um slice, uma referência a uma sequência de inteiros. Não se preocupe com os detalhes agora; vamos explorar referências e slices em profundidade nos próximos artigos.


Funções aninhadas

Em Rust, você pode declarar funções dentro de outras funções. Isso é útil para organizar lógica auxiliar que só faz sentido localmente:

fn calcular_imc(peso: f64, altura: f64) -> f64 {
    fn imc(p: f64, h: f64) -> f64 {
        p / (h * h)
    }

    fn classificar(imc: f64) -> &'static str {
        if imc < 18.5 { "Abaixo do peso" }
        else if imc < 25.0 { "Peso normal" }
        else if imc < 30.0 { "Sobrepeso" }
        else { "Obesidade" }
    }

    let valor = imc(peso, altura);
    println!("IMC: {valor:.2} — {}", classificar(valor));
    valor
}

fn main() {
    calcular_imc(70.0, 1.75);
    calcular_imc(50.0, 1.75);
    calcular_imc(100.0, 1.75);
}

Saída:

IMC: 22.86 — Peso normal
IMC: 16.33 — Abaixo do peso
IMC: 32.65 — Obesidade

Um padrão que você vai ver muito

Em Rust idiomático, é muito comum que funções terminem com uma expressão que resume seu propósito. Aprenda a reconhecer esse padrão:

fn area_retangulo(largura: f64, altura: f64) -> f64 {
    largura * altura
}

fn perimetro_retangulo(largura: f64, altura: f64) -> f64 {
    2.0 * (largura + altura)
}

fn descrever_retangulo(largura: f64, altura: f64) {
    println!(
        "Retângulo {}×{}: área={:.2}, perímetro={:.2}",
        largura,
        altura,
        area_retangulo(largura, altura),
        perimetro_retangulo(largura, altura)
    );
}

fn main() {
    descrever_retangulo(5.0, 3.0);
    descrever_retangulo(10.0, 2.5);
}

Saída:

Retângulo 5×3: área=15.00, perímetro=16.00
Retângulo 10×2.5: área=25.00, perímetro=25.00

O que vem a seguir

Hoje entendemos a anatomia de funções em Rust e a distinção crucial entre statements e expressions. Essa distinção vai aparecer de novo quando estudarmos estruturas de controle — e vai fazer muito mais sentido agora.

No próximo artigo, vamos explorar controle de fluxo: if, loop, while e for. E você vai descobrir que em Rust, um if não é apenas uma instrução — é uma expression que produz valor. O que significa que você pode escrever coisas como let x = if condicao { 1 } else { 2 }; — elegante, seguro e completamente idiomático.


Fontes e leituras recomendadas