Capítulo 03 9 min de leitura

Funções e controle de fluxo

Um programa é funções chamando funções. Este capítulo trata dessa superfície: como defini-las, como ramificar e iterar, e um recurso que surpreende quem vem do Python — a maioria das coisas em Glide são expressões, incluindo blocos.

Funções

A forma geral:

fn add(a: i32, b: i32) -> i32 {
    return a + b;
}

Leia da esquerda para a direita:

  • fn — declara uma função.
  • add — seu nome.
  • (a: i32, b: i32) — seus parâmetros. Todo parâmetro tem um tipo explícito. Não há *args nem **kwargs — o que você declara é o que os chamadores devem passar.
  • -> i32 — o tipo de retorno. Uma função que não retorna nada omite a seta:
fn shout(msg: string) {
    println!(msg.to_upper());
}
  • { ... } — o corpo, um bloco. Instruções terminam com ;.
  • return a + b; — retorno explícito. Isso não é negociável em Glide; não inferimos valores de retorno a partir de expressões finais como o Rust faz.

Chamando uma função

Exatamente como você esperaria:

fn main() -> i32 {
    let s: i32 = add(3, 4);
    println!(s);    // 7
    return 0;
}

Passando valores grandes

Escalares (i32, bool, f64...) são passados por valor — o receptor recebe uma cópia. Objetos na heap (*Vector<T>, *HashMap<V>, ...) são passados por ponteiro — tanto o chamador quanto o receptor enxergam o mesmo valor. Passar um ponteiro é barato (é apenas um endereço).

Se você quiser que o receptor leia um valor da heap sem assumir a posse dele, você passa um empréstimo:

fn print_first(v: &*Vector<i32>) {
    println!(v.get(0));
}

fn main() -> i32 {
    let v: *Vector<i32> = Vector::new();
    v.push_all!(1, 2, 3);
    print_first(&v);
    println!(v.len());   // ainda é nosso — empréstimos não consomem
    return 0;
}

O & significa "empréstimo" — como emprestar um livro. As regras de empréstimo têm um capítulo inteiro (Modelo de memória); por ora, observe apenas que & é "dar acesso de leitura" e &mut é "dar acesso de escrita".

if é uma expressão

A forma familiar funciona:

if x > 0 {
    println!("positive");
} else if x < 0 {
    println!("negative");
} else {
    println!("zero");
}

Mas — e este é o truque principal — todo if é também um valor. Você pode colocá-lo no lado direito de um =:

let kind: string = if x > 0 { "positive" }
                   else if x < 0 { "negative" }
                   else { "zero" };

Toda a cadeia if/else é avaliada como uma das três strings, que fica associada a kind. Na posição de expressão, cada ramo é um valor único sem `return` e sem `;` no final — o valor simples é o resultado do ramo. (else é obrigatório aqui: uma expressão deve sempre produzir algo.)

match

match é um switch sobre a forma de um valor — uma variante de enum, ou o some/none de um ?T. Cada braço tem a forma padrão => resultado. match é onde os enums brilham:

enum HttpMethod {
    Get,
    Post,
    Put,
    Delete,
}

fn allow_body(m: HttpMethod) -> bool {
    return match m {
        HttpMethod::Get    => false,
        HttpMethod::Post   => true,
        HttpMethod::Put    => true,
        HttpMethod::Delete => false,
    };
}
  • Cada braço tem a forma Padrão => valor, (separados por vírgula, valor simples — mesma regra de expressão que o if). Se um braço precisar de várias instruções, use um bloco que termina em return: HttpMethod::Get => { do_thing(); return false; }.
  • _ é o curinga — captura qualquer coisa que os braços anteriores não tenham tratado.
  • O compilador verifica se você cobriu todas as variantes do enum. Se você adicionar um HttpMethod::Patch, receberá um erro non-exhaustive-match até tratá-lo — o que captura uma classe real de bugs.

Laços

Três formas de laço.

`while` executa enquanto a condição for verdadeira:

let mut i: i32 = 0;
while i < 10 {
    println!(i);
    i = i + 1;
}

`for` é o clássico laço de três partes no estilo C (init; condição; passo):

for let j: i32 = 0; j < 10; j = j + 1 {
    println!(j);
}

`for ... in` percorre qualquer iterável (Vector, intervalo, HashMap, qualquer coisa que implemente o trait Iterator):

let v: *Vector<i32> = Vector::new();
v.push_all!(10, 20, 30);
for x in v {
    println!(x);
}

break sai do laço mais próximo; continue pula para a próxima iteração.

Intervalos

Uma expressão de intervalo é inicio..fim (meio-aberto, fim excluído) ou inicio..=fim (fechado, fim incluído):

for i in 0..5      { println!(i); }   // 0, 1, 2, 3, 4
for i in 0..=5     { println!(i); }   // 0, 1, 2, 3, 4, 5
for i in 1..=10    { println!(i * i); }

Intervalos são apenas valores do tipo Range<i32> (ou Range<i64>). Você pode coletá-los, filtrá-los — tudo que existe em stdlib::iter funciona com eles.

Blocos como valores

Um bloco simples { ... } executa suas instruções e produz um valor via return:

let result: i32 = {
    let a: i32 = expensive();
    let b: i32 = also_expensive();
    return a + b;
};

É a mesma mecânica que if e match usam. O ponto de um bloco simples é delimitar bindings temporários: a e b são descartados na chave de fechamento, mesmo que sua soma continue viva em result. Você recorreria a isso quando um cálculo precisa de variáveis intermediárias que nenhuma outra parte da função deve ver.

Um primeiro olhar em ? para propagação de erros

Quando uma função retorna !T (lembre: "um T ou um erro"), você geralmente quer encerrar no caso de erros. O operador ? faz isso em um único caractere:

fn parse_int(s: string) -> !i32 {
    if s.eq("") { return err("empty"); }
    return ok(42);   // fingindo que parseamos
}

fn double(s: string) -> !i32 {
    let n: i32 = parse_int(s)?;   // se err → retorna err; se ok → desempacota para n
    return ok(n * 2);
}

? é a abreviação de "se isso retornou um erro, retorne-o desta função também; caso contrário, extraia o valor". Funciona apenas dentro de uma função que ela própria retorna !T ou ?T. Dedicaremos um capítulo inteiro a isso — veja Erros como valores.

Um exemplo completo

Vamos combinar alguns desses conceitos. Aqui está uma função que retorna a primeira vogal de uma string, ou none se não houver nenhuma:

first_vowel.glide
fn is_vowel(c: i32) -> bool {
    return c == 97 || c == 101 || c == 105 || c == 111 || c == 117
        || c == 65 || c == 69  || c == 73  || c == 79  || c == 85;
}

fn first_vowel(s: string) -> ?string {
    for let i: i32 = 0; i < s.len(); i = i + 1 {
        if is_vowel(s.at(i).to_int()) {
            return some(s.substring(i, i + 1));
        }
    }
    return none();
}

fn main() -> i32 {
    let r: ?string = first_vowel("rhythm and blues");
    if r.has {
        println!("got:", r.val);
    } else {
        println!("no vowel");
    }
    return 0;
}

Três coisas a observar:

  • s.at(i) retorna um caractere; .to_int() o amplia para um i32 para que possamos compará-lo a códigos ASCII.
  • is_vowel retorna um bool e é chamada como qualquer outra função.
  • first_vowel retorna ?string — às vezes tem um valor, às vezes é none. .has e .val são como você lê esse resultado.

Para onde ir agora

Você já tem funções, ramificações, laços e uma prévia de ?. O próximo capítulo — Erros como valores — é o grande: como Glide lida com falhas sem exceções.