Ir para o conteúdo

Lifetimes ​

Um borrow é um olhar para um valor que vive em outro lugar. O Talor não escreve a região nem um nome para ela: não existe 'a, nem &, nem nada no call site. O compilador infere do fluxo de controle quanto tempo um borrow vive, e o borrow termina no último uso dele, não no fim do scope que o contém.

Uma linguagem que faz dos lifetimes parte do tipo chama de elision o ato de deixá-los de fora: a anotação é omitida onde uma regra fixa sobre a assinatura a determina. O Talor só tem a forma elidida - nada é escrito em posição nenhuma - e a regra não é sobre a assinatura: a origem de uma view retornada é lida do corpo, então duas funções com a mesma assinatura podem devolver views de lugares diferentes.

talor
fn main(): i32 {
    let xs: Array<Array<i64>> = [[1, 2], [3]];
    let r = xs[0];              // `r` reads the element where it lies
    println(`${r.len()}`);
    xs = [];                    // accepted: `r` was used on the line above
    println(`${xs.len()}`);
    0
}

Views ​

Uma view é um olhar compartilhado e somente leitura para um lugar. Ela é uma palavra de máquina, é Copy, não possui nada e não libera nada.

Nenhuma palavra escreve uma. Um lugar entregue a um parâmetro que a função só lê já é um borrow desse lugar, uma leitura de um lugar que esta função não possui mantém o borrow, e um valor que um corpo devolve de uma leitura deriva a origem dele do corpo. O compilador faz uma view sempre que uma leitura pode ficar onde está, e o programa não diz nada para isso. view é o nome do conceito, não uma palavra da linguagem: um programa pode usá-la como nome.

talor
struct Point { x: i64, y: i64 }

fn norm2(p: Point): i64 { p.x * p.x + p.y * p.y }

let p = Point { x: 3, y: 4 };
println(`${norm2(p)}`);     // the parameter is only read: nothing is written at the call site

Uma view de um T responde o que um lugar de tipo T responde: um campo é lido através dela (r.x), uma chamada de método a toma como receiver (r.len()), e um índice é lido através dela (r[i]).

A leitura de um escalar Copy é uma cópia, porque a cópia de um escalar é mais barata que um pointer para ele. Para um valor que possui algo, leia um campo ou chame um método através da view: *r em uma view de um agregado é recusado, porque levaria o valor inteiro de um lugar que a função não possui.

As regras ​

  1. Uma view não pode viver mais que o lugar para o qual aponta.
  2. Uma view não pode viver em um campo.
  3. Uma view não pode viver em um container nem em uma tupla.
  4. Enquanto uma view de x está viva, x não pode ser atribuído, passar por move, nem ser passado aonde seria alterado. Viva termina no último uso da view.
  5. Uma view precisa de um lugar: uma chamada ou um literal não tem onde viver.
  6. Uma view é somente leitura; a alteração passa pelo dono.
  7. Uma função retorna uma view quando toda view retornada vem de seus parâmetros, e o call site prende o resultado a todo argumento de onde ele pode vir.

Cada regra tem uma mensagem, e a mensagem diz o que escrever em vez disso:

error: 'xs' is viewed on line 3 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()'
error: a view is read-only: write through 'x', which owns the place, or take an owned copy with '.clone()'

A única forma da mesma ideia com escrita permitida é Locked<T>, que só um lock faz e só pelo tempo de uma chamada: uma view com a escrita liberada, e uma regra de região mais estrita que estas sete.

Ler um lugar que esta função não possui é um olhar ​

Um elemento de um array, um campo de um parâmetro que é um borrow, a variável de um for e um binding de padrão sobre um valor que está sob borrow são lugares de onde nada pode passar por move, então ler um deles mantém o borrow em vez de copiar o valor:

talor
struct Row { cells: Array<i64> }

fn main(): i32 {
    let xs: Array<Row> = [Row { cells: [1, 2] }, Row { cells: [3] }];
    let r = xs[0];              // `r` reads the element
    let n = r.cells[1];         // and a view is indexed like the place it views
    println(`${n}`);
    for x in xs { println(`${x.cells.len()}`); }   // the loop variable is a borrow too
    0
}

Todo o resto continua igual: um valor Copy é copiado, porque a cópia de um escalar é mais barata que um pointer para ele; um lugar que esta função possui também é lido onde está, e passa por move quando o novo nome é entregue adiante ou escrito; um temporário não tem de onde ser lido. Guardar um desses olhares, ou passá-lo a um parâmetro com que a função fica, é um move para fora de um armazenamento que a função não possui, e é recusado. Retornar um deles faz a função retornar um borrow, como mostrado abaixo.

Um valor retornado é um borrow quando o corpo lê no lugar ​

Um tipo de retorno é escrito como o tipo que ele é - Row - e o corpo decide se o que volta é um olhar ou um valor:

talor
struct Row { cells: Array<i64> }
struct Table { rows: Array<Row> }

fn row(t: Table, i: i64): Row { t.rows[i] }   // a look at `t`, not a copy

let t = Table { rows: [Row { cells: [1, 2] }] };
let r = row(t, 0);
println(`${r.cells.len()}`);

Um olhar retornado pode vir de mais de um parâmetro, e então o resultado fica preso a todo argumento de onde ele pode vir: nenhum deles pode ser substituído enquanto o resultado estiver vivo. Borrows devolvidos mostra cada caso.

O lugar de onde um olhar é tirado precisa viver mais que a instrução que o usa, então uma chamada cuja origem é um temporário é recusada:

error: a view returned by this call points into a temporary, and a temporary dies at the end of the statement; bind it to a name first

Um olhar guardado onde o valor tem dono - colocado em um container, ou atribuído a um binding que vive mais que o argumento - viveria mais que aquilo para o qual aponta, então o compilador o copia nesse ponto. Compilado com --borrow strict, ele é recusado, e .clone() é como um programa pede um valor próprio.

Um corpo que constrói um valor retorna um valor com dono, e o mesmo vale para um corpo que escreve em um parâmetro e depois o devolve. Um corpo que retorna um olhar em um caminho e um valor que ele construiu em outro é recusado, porque o chamador não saberia se cabe a ele liberar o resultado, e o mesmo vale para um olhar envolvido em um valor que o corpo constrói, como Some(t.rows[i]). Escrever .clone() na parte faz dela um valor próprio da função:

error: 'pick' reads a place it does not own on line 4 and returns a value of its own on line 4, so the caller cannot tell whether the result is its to release; return a value of its own on both, with '.clone()' on line 4

Um borrow em uma closure ​

Uma closure pode capturar uma view, porque uma view é Copy e somente leitura: o lugar para o qual ela aponta fica sob borrow enquanto a closure viver, então esse lugar não pode passar por move nem ser alterado enquanto a closure existir.

talor
struct Row { cells: Array<i64> }
struct Table { rows: Array<Row> }

fn row(t: Table, i: i64): Row { t.rows[i] }

let t = Table { rows: [Row { cells: [1, 2] }] };
let r = row(t, 0);
let f = () => r.cells.len();
println(`${f()}`);

Essa closure não pode sair da função em que foi criada: um valor que carrega uma view não é retornado nem passado aonde a função chamada fica com ele, porque qualquer um dos dois viveria mais que a região de onde a view foi tirada, e fn(A): R não tem onde dizer qual região é essa.

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