Ir para o conteúdo

Borrowing ​

Uma chamada não leva o que recebe. Todo parâmetro passa por borrow por padrão, salvo se a assinatura disser o contrário, e o call site não escreve nada: sem &, sem &mut, sem marcador na chamada.

talor
struct Bag { items: Array<i64> }

fn total(b: Bag): i64 { b.items.len() }

let bag = Bag { items: [1, 2] };
let n = total(bag);          // reading a parameter borrows it, so `bag` is still here
println(`${n} and ${bag.items.len()}`);

O que uma chamada faz com o valor ​

Três coisas, e o compilador lê qual delas no corpo da função chamada. A mesma regra vale em um pub fn de outro module e dentro de um module, e nada disso é escrito no call site.

  • Ler - o parâmetro é um borrow, e o valor continua com quem chamou.
  • Escrever - o argumento precisa ser um lugar que pode ser escrito, e o valor de quem chamou muda.
  • Ficar - a função guarda o valor, envolve o valor ou o captura em uma closure, então a variável de quem chamou é consumida. Um parâmetro que a função só lê e devolve inteiro passa por borrow em vez disso: veja borrows devolvidos.

Usar um valor que a função chamada ficou é recusado no ponto do uso, e a mensagem nomeia a chamada:

error: 'bag' was given to 'consume' on line 5, which keeps it; write 'bag.clone()' at the call if both uses are needed

Um parâmetro que o corpo escreve é a variável do chamador ​

Em C, uma função que deve alterar a variável do chamador recebe um pointer, o chamador passa &x, e a função escreve através de *p. O Talor tem o mesmo efeito sem nada escrito em nenhum dos dois lugares: quando o corpo escreve em um parâmetro, o compilador o passa como um pointer para a variável do chamador, e depois da chamada o chamador lê o valor que a função escreveu.

talor
struct Counter { n: i64 }

fn bump(c: Counter) { c.n = c.n + 1; }

fn fill(xs: Array<i64>, k: i64) {
    let i: i64 = 0;
    while i < k { xs.push(i); i += 1; }
}

fn main(): i32 {
    let c = Counter { n: 40 };
    bump(c);
    bump(c);
    let xs: Array<i64> = [];
    fill(xs, 3);
    (c.n + xs.len()) as i32      // 45: c and xs are the caller's, with the writes
}

O programa sai com 45. bump e fill recebem o endereço de c e de xs do chamador, e não uma cópia, e o contrato que o compilador derivou é o que talor surface registra:

fn bump(c: Counter edits): ()
fn fill(xs: Array<i64> edits, k: i64): ()

Três coisas que um pointer de C não promete, e isto promete:

  • Não existe null. O argumento é sempre uma variável do chamador, então não há nada a checar antes de escrever.

  • Dois argumentos escritos nunca são o mesmo lugar. Uma chamada que entrega a mesma variável a dois parâmetros que o corpo escreve é recusada, porque cada escrita alcançaria o que a outra está alterando:

    talor
    struct H { items: Array<i64> }
    
    fn swap_into(a: H, b: H) { a.items = [1]; b.items = [2]; }
    
    fn main(): i32 {
        let h = H { items: [] };
        swap_into(h, h);             // refused: both are written, and they are the same place
        0
    }
    error: argument 1 is edited and argument 2 is the same place 'h', so the write reaches what the other is read out of; pass two places
  • O que é lido do lugar que está sendo escrito é passado como cópia, então a escrita não o alcança. Aqui move_over lê k e escreve h, e a chamada entrega h.k, uma parte do próprio h:

    talor
    struct K { items: Array<i64> }
    struct H { k: K, total: i64 }
    
    // Reads `k`, writes `h`: `h.k.items` is emptied and replaced.
    fn move_over(k: K, h: H): i64 {
        h.k.items = [];
        h.k.items.push(99);
        let sum: i64 = 0;
        for x in k.items { sum += x; }     // reads the k it was handed
        h.total = sum;
        sum
    }
    
    fn main(): i32 {
        let h = H { k: K { items: [10, 20, 30] }, total: 0 };
        let s = move_over(h.k, h);         // h.k is read out of the place h the call writes
        println(`sum=${s} total=${h.total} items=${h.k.items.len()} first=${h.k.items[0]}`);
        0
    }
    sum=60 total=60 items=1 first=99

    k é o array como estava na chamada, porque o compilador passou uma cópia de h.k, e a escrita alcançou o h do chamador. Sem a cópia, k apontaria para dentro do array que a primeira linha do corpo libera. Compilado com --borrow strict, o compilador não faz nenhuma cópia por conta própria e recusa a chamada, e move_over(h.k.clone(), h) escrito à mão é o mesmo programa.

A clause: edits e takes ​

Uma clause depois da assinatura declara o efeito na própria assinatura, para quem prefere isso escrito a inferido:

talor
pub fn append(dst: Array<i64>, src: Array<i64>) edits dst { dst.push(1); }
pub fn consume(b: Bag): i64 takes b { b.items.len() }

append(xs, extra);           // the same call either way
let n = consume(bag);        // `bag` is gone after this line
  • edits x - a função altera o valor de quem chamou, então o argumento precisa ser um lugar que pode ser escrito.
  • takes x - a função fica com o valor, e usá-lo depois é um erro que nomeia a chamada.
  • Cada verbo nomeia todos os parâmetros a que se aplica, self é um dos nomes, e a clause vem depois do tipo de retorno.

A clause é opcional em toda parte, pub fn incluído, e ela nunca chega a quem chama: o compilador calcula o modo de cada parâmetro a partir do corpo e aplica as mesmas regras dos dois jeitos. Uma clause escrita é conferida contra o corpo, então ela não pode virar um comentário que mente:

error: 'size_of' edits 'h', and the body never writes it; drop the clause

Uma clause que o corpo já derivaria é informação em vez de erro, e o compilador pode reportá-la - como aviso, desligado por padrão. O único caso em que a palavra escrita é a única fonte do fato é um parâmetro cujo tipo menciona um parâmetro de tipo: esse valor viaja por cópia qualquer que seja a instância, então o corpo não decide nada e a clause diz se a função fica com ele.

Alcançar um container é alterar ​

h.items.push(x) escreve no valor de quem chamou exatamente como h.n = x, e os dois são uma regra só:

talor
struct Holder { items: Array<i64> }

fn add(h: Holder, x: i64) edits h { h.items.push(x); }

let h = Holder { items: [1] };
add(h, 2);
println(`${h.items.len()}`);   // 2

Com a clause escrita ou inferida, o argumento precisa ser um lugar que pode ser escrito. Ler um parâmetro não é alterar, e não precisa de clause.

Uma escrita que alcança o que outro argumento lê ​

Dois argumentos de uma chamada, em que a escrita alcança aquilo de que o outro é lido, seriam um lugar que a função escreve enquanto lê. O que só é lido é passado como sua cópia, que o compilador escreve sozinho:

talor
struct H { items: Array<Array<i64>> }

fn g(dst: H, src: H): i64 {
    let r = src.items[0];        // read out of `src`
    dst.items = [];              // the write, through the parameter the clause edits
    r.len()
}

g(h, h);        // accepted: `src` is a copy of `h`, taken before the call
g(h, k);        // accepted: the write reaches neither, and nothing is copied

A cópia custa um clone e acontece só em uma chamada que a escrita alcança. O receiver de um método é um argumento como qualquer outro, nas duas direções: p.show(p.items), em que show escreve self.items, copia o argumento, e h.look(h.k), em que look escreve o seu parâmetro e só lê self, copia o receiver.

Três casos são recusados em vez disso, porque nenhuma cópia os resolve:

  • dois lugares escritos que se sobrepõem, sejam dois argumentos ou um argumento e o receiver, já que a cópia de qualquer um dos dois levaria a escrita embora de quem chamou;
  • um valor lido que não aceita clone, um que contém uma closure ou um recurso;
  • um lugar que um lock entrega por borrow, e que nunca é copiado.

O que um borrow não é ​

  • Uma view é read-only, e nenhuma palavra a escreve: o compilador a infere de como um valor é usado. Veja lifetimes.
  • Um valor Copy é passado por valor, e um agregado lido não é copiado: ele viaja como um pointer para o valor de quem chamou.
  • O tipo de um valor de função não carrega clause, então uma função que escreve, fica com ou devolve um parâmetro não pode ser usada como valor.
  • O parâmetro de uma closure nunca é tomado, faça o corpo o que fizer.

Quanto tempo um borrow dura ​

Até o último uso dele, que é o assunto de lifetimes.

Talor v0.1.0 - Publicado sob a licença MIT OR Apache-2.0.