Aprendendo Rust do zero - Funções 🕺

Aprendendo Rust do zero - Funções 🕺

Como escrever e utilizar funções

Introdução 📝

Esse post faz parte de uma série que estou escrevendo chamada "Aprendendo Rust do zero" onde eu tento resumir o que acho ser importante baseado no livro oficial no site do Rust. Nesse aqui gostaria de abordar as funções. Atenção, escolhi manter alguns termos em inglês para facilitar a compreensão caso seja feito alguma busca na internet a partir desse post.

Sumário 📖

  1. Instalando Rust
  2. Hello, World!
  3. Hello, Cargo!
  4. Variáveis e Mutabilidade
  5. Tipos de dados
  6. Funções - você está aqui
  7. Comentários Simples
  8. Controle de Fluxo

Funções🕺

Funções são predominantes em códigos Rust. Você já viu a função mais importante do Rust: a função main, que é o ponto de entrada de muitos programas. Você também viu a palavra chave fn, que habilita você a criar novas funções.

Código escrito em Rust utiliza snake case como o estilo convencional para nomes de funções e variáveis. Ao utilizar snake case, todas as letras devem ser minúsculas e utilizar um underline para separar palavras. A seguir um exemplo de uma definição de função:

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

    another_function();
}

fn another_function() {
    println!("Another function.");
}

Definições de funções em Rust começam com fn e tem um par de parênteses depois do nome da função. As chaves indicam ao compilador onde o corpo da função começa e termina.

Nós podemos chamar qualquer função que nós tenhamos definido escrevendo seu nome seguido por um par de parênteses. Como another_function foi definida no programa, ela pode ser chamada dentro da função main. Note que nós definimos another_functiondepois da função main no código fonte. Nós poderiamos ter definido antes também. O Rust não se importa onde você define suas funções, importa somente que elas tenham sido definidas em algum lugar.

Vamos começar um novo projeto chamado functions para podermos explorar um pouco mais das funções. Copie e cole o exemplo acima no arquivo src/main.rs e execute. Você verá a seguinte saída no terminal:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished dev [unoptimized + debuginfo] target(s) in 0.28s
     Running `target/debug/functions`
Hello, world!
Another function.

As linhas executam na ordem em que elas aparecem dentro da função main. Primeiro a mensagem "Hello, world!" é impressa e então a função another_function é chamada e sua mensagem é impressa.

Parâmetros das funções

Funções podem ter parâmetros, que são variáveis especiais que são parte da assinatura da função. Quando uma função tem parâmetros você pode utilizá-los com valores concretos. Tecnicamente esses valores são chamados argumentos, mas em conversas casuais as pessoas tendem a usar as palavras parâmetros e argumentos para exemplificar as variáveis na assinatura da função como também os valores passados quando você chama uma função.

O seguinte exemplo mostra uma versão reescrita da função another_function utilizando parâmetros em Rust:

fn main() {
    another_function(5);
}

fn another_function(x: i32) {
    println!("The value of x is: {}", x);
}

Se você tentar executar esse código, vai obter a seguinte saída:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished dev [unoptimized + debuginfo] target(s) in 1.21s
     Running `target/debug/functions`
The value of x is: 5

A declaração de another_function tem um parâmetro chamado x. O tipo de x é especificado como i32. Quando 5 é passado para another_function, o println! coloca 5 onde as chaves estão posicionadas na string formatada.

Em assinaturas de funções, você deve declarar o tipo de cada parâmetro. Isso é uma decisão de design do Rust. Obrigar a declaração de tipos na definição da função significa que o compilador quase nunca vai precisar que você declare o tipo da função em outro lugar do código para descobrir qual é o tipo daquele parâmetro.

Quando você quiser que uma função tenha multiplos parâmetros, separe a declaração dos parâmetros com vírgulas, como a seguir:

fn main() {
    print_labeled_measurement(5, 'h');
}

fn print_labeled_measurement(value: i32, unit_label: char) {
    println!("The measurement is: {}{}", value, unit_label);
}

Esse exemplo cria uma função chamada print_labeled_measurementcom dois parâmetros. O primeiro parâmetro é chamado valuee é do tipo i32. O segundo é chamado unit_label e é do tipo char. A função então imprime um texto contendo ambos o value e unit_label.

Vamos tentar executar esse código. Troque o programa atual no arquivo src/main.rs do seu projeto functions pelo exemplo logo acima e o execute com cargo run:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/functions`
The measurement is: 5h

Como nós chamamos a função com 5 como o valor para o parâmetro value e hcomo o valor para o parâmetro unit_label, a saída do programa contém esses valores.

O corpo de funções contém Statements e Expressions

O corpo de uma função é composto de uma série de statements que opicionalmente terminam com uma expression. Até aqui nós vimos apenas funções que não finalizavam com expressions, mas você viu uma expression como parte de um statement. Como Rust é uma linguagem de programação baseada em expressions, isso é uma distinção importante a se entender. Como outras linguagens de programação não tem as mesmas distinções, vamos então olhar o que são statements e expressions e como eles afetam de forma diferente o corpo das funções.

Nós já utilizamos statements e expressions. Statements são instruções que fazem alguma ação e não retornam um valor. Expressions resultam em um valor final. Veremos ver alguns exemplos a seguir.

Criar uma variável e atribuir um valor a ela com a palavra chave leté um statement. No exemplo a seguir, let y = 6; é um statement:

fn main() {
    let y = 6;
}

Declaração de funções são também statements, o exemplo anterior é um statement.

Statements não retornam valores. Ou seja, você não pode atribuir um statement let para outra variável, como o exemplo a seguir tenta fazer. Você obterá um erro:

fn main() {
    let x = (let y = 6);
}

Se você executar esse código, o erro se parecerá com:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
error[E0658]: `let` expressions in this position are experimental
 --> src/main.rs:2:14
  |
2 |     let x = (let y = 6);
  |              ^^^^^^^^^
  |
  = note: see issue #53667 <https://github.com/rust-lang/rust/issues/53667> for more information
  = help: you can write `matches!(<expr>, <pattern>)` instead of `let <pattern> = <expr>`

error: expected expression, found statement (`let`)
 --> src/main.rs:2:14
  |
2 |     let x = (let y = 6);
  |              ^^^^^^^^^
  |
  = note: variable declaration using `let` is a statement

warning: unnecessary parentheses around assigned value
 --> src/main.rs:2:13
  |
2 |     let x = (let y = 6);
  |             ^^^^^^^^^^^ help: remove these parentheses
  |
  = note: `#[warn(unused_parens)]` on by default

For more information about this error, try `rustc --explain E0658`.
warning: `functions` (bin "functions") generated 1 warning
error: could not compile `functions` due to 2 previous errors; 1 warning emitted

O statement let y = 6 não retorna um valor, então não há nada para atribuir a x. Isso é diferente do que acontece em outras linguagens de programação, como C e Ruby, onde a atribuição retorna o valor que foi atribuído. Nessas linguagens é possível escrever x = y = 6 e ter ambos x e y com valor igual a 6, que não é o caso do Rust.

Expressions resolvem a um valor e representam a maior parte do restante de código que você vai escrever em Rust. Considere uma operação matemática, como 5 + 6, que é uma expression que resolve para 11. Expressions podem ser parte de statements: no exemplo acima o 6 no statement let y = 6, é um expression que resolve para o valor 6. Chamar uma função é um expression. O bloco que nós usamos para criar novos escopos, { }, é uma expression, como por exemplo:

fn main() {
    let y = {
        let x = 3;
        x + 1
    };

    println!("The value of y is: {}", y);
}

Essa expression:

{
    let x = 3;
    x + 1
}

é um bloco que, nesse caso, resolve para 4. Esse valor é atribuido para y como parte do statement let. Note a linha que tem x + 1 sem um ponto e vírgula no final, que é diferente da maioria das linhas que você viu até agora. Expressions não incluem um ponto e vírgula no final da linha. Se você adicionar ponto e vírgula ao fim de uma expression, você torna ela em um statement, o faz com que ele não retorne um valor. Tenha isso em mente nas próximas vezes quando for analisar valores retornados por funções e expressions.

Funções que retornam valores

Funções podem retornar valores para o código que as chamam. Nós não nomeamos valores de retorno, mas nós declaramos o seu tipo depois de uma seta (->). Em Rust o valor retornado de uma função é sinônimo do valor da expression final no bloco do corpo de uma função. Você pode retornar antes do fim de uma função utilizando a palavra chave return e especificar um valor, mas a maioria das funções retorna a última expression implicitamente. Veja a seguir um exemplo de uma função que retorna um valor:

fn five() -> i32 {
    5
}

fn main() {
    let x = five();

    println!("The value of x is: {}", x);
}

Não há chamada de funções, macros ou mesmo statements let na função five, somente o número 5. Essa é uma função perfeitamente válida em Rust. Note que o tipo de retorno da função é especificado também, como -> i32. Tente executar esse código e a saída deverá parecer com isso:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished dev [unoptimized + debuginfo] target(s) in 0.30s
     Running `target/debug/functions`
The value of x is: 5

O 5 em five é o valor retornado da função, que é o motivo de o tipo de retorno ser i32. Vamos examinar isso em mais detalhes. Há duas partes importantes aqui: primeiro, a linha let x = five(); mostra que nós estamos usando o valor de retorno de uma função para inicializar a variável. Como a função five retorna 5, essa linha é o mesmo que:

let x = 5;

Segundo, a função five não tem nenhum parâmetro e define o tipo de retorno, mas o corpo da função é um 5 sozinho sem nenhum ponto e vírgula pois é um expression que queremos retornar o valor.

Vamos ver outro exemplo:

fn main() {
    let x = plus_one(5);

    println!("The value of x is: {}", x);
}

fn plus_one(x: i32) -> i32 {
    x + 1
}

Executar esse código irá imprimir The value of x is: 6. Porém se nós colocarmos um ponto e vírgula no final da linha que contém x + 1, mudando ela de uma expression para um statement, nós obteremos um erro.

fn main() {
    let x = plus_one(5);

    println!("The value of x is: {}", x);
}

fn plus_one(x: i32) -> i32 {
    x + 1;
}

Compilar esse código produz um erro, como podemos ver a seguir:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
error[E0308]: mismatched types
 --> src/main.rs:7:24
  |
7 | fn plus_one(x: i32) -> i32 {
  |    --------            ^^^ expected `i32`, found `()`
  |    |
  |    implicitly returns `()` as its body has no tail or `return` expression
8 |     x + 1;
  |          - help: consider removing this semicolon

For more information about this error, try `rustc --explain E0308`.
error: could not compile `functions` due to previous error

A mensagem do erro, mismatched types, mostra o erro principal desse código. A definição da função plus_one diz que ela vai retornar um valor do tipo i32, que é expresso como (), o unit_type, lembra dele? Ou seja, nada é retornado, o que contradiz a definição da função e resulta em um erro. Nessa saída, o Rust provê uma mensagem que pode ajudar a resolver esse erro: ele sugere remover o ponto e vírgula, o que resolveria de fato o erro.

No próximo post vamos aprender como escrever comentários no nosso código.

Referência 📚

Para escrever esse post e os possíveis próximos que virão, estou utilizando o livro online recomendado no site oficial do Rust, que você pode encontrar em doc.rust-lang.org/book/title-page.html.