Ir para o conteúdo

Borrows devolvidos ​

Uma função que devolve um de seus parâmetros, ou uma parte de um deles, retorna um borrow do argumento do chamador. Nada na assinatura diz isso, e nada precisa dizer: o compilador lê o corpo, descobre para quais argumentos o resultado pode apontar e cobra isso do chamador.

Um parâmetro devolvido ​

Um corpo que só lê um parâmetro e o devolve empresta esse parâmetro. O chamador continua com o valor, e o resultado é um olhar para ele:

talor
fn id(xs: Array<i64>): Array<i64> { xs }

fn main(): i32 {
    let xs: Array<i64> = [1, 2];
    let r = id(xs);
    (r.len() + xs.len()) as i32      // 4: xs is still the caller's
}

Uma parte de um parâmetro passa por borrow do mesmo jeito. first devolve o elemento onde ele está, e nada é copiado:

talor
fn first(xs: Array<Array<i64>>): Array<i64> { xs[0] }

fn main(): i32 {
    let xs: Array<Array<i64>> = [[1, 2], [3]];
    let r = first(xs);
    r.len() as i32                   // 2
}

Um resultado que pode vir de vários argumentos ​

Quando o corpo pode devolver um entre vários parâmetros, o resultado fica preso a todo argumento de onde ele pode vir:

talor
fn longer(x: Array<i64>, y: Array<i64>): Array<i64> {
    if x.len() > y.len() { x } else { y }
}

fn main(): i32 {
    let a = [1, 2, 3];
    let b = [4];
    let r = longer(a, b);            // a look at a or at b
    (r.len() + a.len() + b.len()) as i32
}

O chamador não sabe qual dos dois recebeu, então enquanto r estiver vivo nem a nem b podem ser substituídos, passar por move ou ser escritos:

talor
fn longer(x: Array<i64>, y: Array<i64>): Array<i64> {
    if x.len() > y.len() { x } else { y }
}

fn main(): i32 {
    let a = [1, 2, 3];
    let b = [4];
    let r = longer(a, b);
    b = [];                          // refused: r may be a look at b
    (r.len() + b.len()) as i32
}
error: 'b' is viewed on line 8 and the view is still live here, so this assignment would leave it pointing at something else; end the view first, or take a copy with '.clone()'

Um borrow dura até o seu último uso, e não até o fim do bloco. Depois que r é lido pela última vez, b fica livre de novo.

O contrato é derivado, e fica visível ​

A assinatura de longer é a que foi escrita. O que o compilador derivou do corpo é registrado por talor surface, que escreve o contrato do module e faz o build falhar quando ele muda:

fn longer(x: Array<i64>, y: Array<i64>): Array<i64> from param 0 from param 1

from param 0 from param 1 é o contrato cobrado de quem chama. Uma mudança no corpo que muda esse contrato - um resultado que agora pode vir de um terceiro argumento, ou que deixa de ser um borrow - muda essa linha, então a mudança aparece onde a função é definida, e não em uma chamada distante.

Argumentos que terminam na chamada passam por move ​

Quando todo argumento de onde o resultado pode vir é um temporário, ou um local que não é usado de novo, a chamada faz o move deles. O chamador fica dono do resultado, o argumento que não foi devolvido é liberado na chamada, e nada é copiado:

talor
fn longer(x: Array<i64>, y: Array<i64>): Array<i64> {
    if x.len() > y.len() { x } else { y }
}

fn build(n: i64): Array<i64> {
    let a: Array<i64> = [1, 2, 3, 4];
    let b: Array<i64> = [n];
    longer(a, b)                     // a and b end here: the result is build's own
}

fn main(): i32 {
    let kept = build(7);
    kept.len() as i32                // 4
}

Onde um borrow viveria mais que o argumento, o compilador copia ​

Um borrow não pode viver mais que aquilo para o qual aponta. Quando um resultado que é um borrow é guardado em um lugar que é dono do valor - um binding com o tipo escrito, uma atribuição, um retorno, um campo, um elemento, um argumento que o callee guarda - o compilador o copia nesse ponto, e em nenhum outro:

talor
fn longer(x: Array<i64>, y: Array<i64>): Array<i64> {
    if x.len() > y.len() { x } else { y }
}

fn main(): i32 {
    let a: Array<i64> = [1, 2, 3];
    let result: Array<i64> = [];
    {
        let b: Array<i64> = [4];
        result = longer(a, b);       // copied here: result outlives b
    }
    (result.len() * 10 + a.len()) as i32   // 33
}

A cópia é a mesma que o programa teria de escrever à mão como .clone(), no mesmo lugar. Uma chamada cujo resultado fica dentro da região dos seus argumentos, que é o caso comum, não copia nada.

A mesma regra vale para um resultado que é um borrow guardado em um container:

talor
fn first(xs: Array<Array<i64>>): Array<i64> { xs[0] }

fn main(): i32 {
    let xs: Array<Array<i64>> = [[1, 2], [3]];
    let ys: Array<Array<i64>> = [];
    ys.push(first(xs));              // copied: ys keeps what it is given
    (ys.len() + xs.len()) as i32
}

--borrow strict: nenhuma cópia que o programa não escreveu ​

Compilado com --borrow strict, o compilador não insere nenhuma cópia por conta própria. Um resultado que é um borrow guardado além dos seus argumentos é recusado, e a mensagem diz de qual argumento ele é emprestado:

talor build --borrow strict
error: this value is borrowed from 'a', and a borrowed value cannot be kept here; write '.clone()' to keep a copy

A mesma flag desliga a cópia que o compilador faz quando um argumento de uma chamada é escrito e outro é lido do mesmo lugar, como em g(h.k, h): essa chamada é recusada, e a mensagem diz onde escrever .clone().

O que continua sendo um move ​

Dois formatos não passam por borrow, e o chamador entrega o valor:

  • Um parâmetro que o corpo escreve e depois devolve. Um borrow é somente leitura, então um corpo que altera o parâmetro entrega o valor alterado:

    talor
    fn add(x: Array<i64>, v: i64): Array<i64> {
        x.push(v);
        x
    }
    
    fn main(): i32 {
        let a: Array<i64> = [1];
        let r = add(a, 2);             // a is given to add
        r.len() as i32
    }
  • Um parâmetro devolvido em um caminho, e um valor novo em outro. O chamador não saberia se é dono do resultado, então o parâmetro é entregue em todos os caminhos:

    talor
    fn or_new(x: Array<i64>, k: i64): Array<i64> {
        if k > 0 { x } else { [9, 9] }
    }
    
    fn main(): i32 {
        let a: Array<i64> = [1, 2, 3];
        let r = or_new(a, 1);          // a is given to or_new
        r.len() as i32
    }

Onde um valor com dono e um borrow se encontram ​

Um if ou um match cujos ramos discordam - um produz o borrow que uma chamada devolve, outro um valor próprio - se junta em um único valor que pertence ao programa, então o compilador copia o lado emprestado no join:

talor
fn first(xs: Array<Array<i64>>): Array<i64> { xs[0] }

fn main(): i32 {
    let xs = [[1, 2], [3]];
    let k = 1;
    let r = if k > 0 { first(xs) } else { [7] };   // the borrowed side is copied here
    xs[0].push(9);                                 // so xs may change while r is live
    println(`${r.len()} ${xs[0].len()}`);
    0
}

Com --borrow strict o join é recusado em vez disso, e .clone() no ramo do borrow é a cópia escrita à mão:

error: this value crosses the join and is kept there: it is borrowed from 'xs', and '.clone()' is what keeps a copy

As garantias ​

  • Um borrow devolvido nunca vive mais que aquilo para o qual aponta: ou os argumentos vivem mais que ele, ou o compilador o copia no ponto em que isso não aconteceria, ou, com --borrow strict, o programa é recusado ali.
  • Enquanto um resultado está vivo, todo argumento de onde ele pode vir fica protegido: nenhum deles pode ser substituído, passar por move ou ser escrito.
  • Um borrow custa o que um pointer custa. Uma cópia só acontece onde o programa guarda um resultado que é um borrow além dos seus argumentos, e ela é a cópia que o programa teria de escrever.

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