Foco na propriedade

Nota tradutor: o registro é datado de 13 de maio de 2014, portanto, alguns detalhes, incluindo o código-fonte, podem não corresponder ao estado atual das coisas. A resposta à pergunta por que a tradução de um post tão longo é necessária será o valor de seu conteúdo para formar um entendimento de um dos conceitos fundamentais da linguagem Rust, como fluência.


Com o tempo, fiquei convencido de que seria melhor abandonar a distinção entre variáveis ​​locais mutáveis ​​e imutáveis ​​no Rust. Pelo menos muitas pessoas são céticas sobre esse assunto. Eu queria declarar minha posição em público. Darei vários motivos: filosóficos, técnicos e práticos, além de me voltar para a principal defesa do sistema atual. (Observação: eu vi isso como Rust RFC, mas decidi que o tom é melhor para uma postagem de blog e não tenho tempo para reescrevê-la agora.)


Explicação


Escrevi este artigo de maneira bastante decisiva e acredito que a linha que estou defendendo estará correta. No entanto, se não concluirmos o suporte ao sistema atual, isso não será um desastre ou algo assim. Tem suas vantagens e, no geral, acho bastante agradável. Eu apenas acho que podemos melhorá-lo.


Em uma palavra


Gostaria de remover a distinção entre variáveis ​​locais imutáveis ​​e mutáveis ​​e renomear &mut ponteiros para &my , &only ou &uniq (isso não &uniq diferença para mim). Se ao menos não houvesse palavra-chave mut .


Motivo filosófico


A principal razão pela qual desejo fazer isso é porque acredito que isso tornará a linguagem mais consistente e fácil de entender. Essencialmente, isso nos reorientará de falar sobre mutabilidade para falar sobre o uso de aliases (que chamarei de "compartilhamento", veja abaixo).


A variabilidade se torna uma conseqüência da singularidade: "Você sempre pode alterar tudo o que tem acesso exclusivo. Os dados compartilhados geralmente são imutáveis, mas se você precisar, pode alterá-los usando algum tipo de tipo de Cell ".


Em outras palavras, com o tempo, ficou claro para mim que problemas com a corrida de dados e a segurança da memória surgem quando você tem o uso de aliases e mutabilidade. Uma abordagem funcional para resolver esse problema é eliminar a mutabilidade. A abordagem de Rust seria remover o uso de aliases. Isso nos dá uma história que pode ser contada e que nos ajudará a descobrir.


Uma observação sobre terminologia: acho que devemos nos referir ao uso de aliases como separação ( nota do tradutor: daqui em diante, em todo lugar, em vez de "aliasing", é usado "sharing" no significado de "separação" ou "propriedade compartilhada", pois nenhum "uso de aliases", nenhuma "pseudonimização" dá uma compreensão do que está em jogo ). No passado, evitávamos isso devido às suas referências multiencadeadas. No entanto, se / quando implementarmos os planos de paralelização de dados que propus, essa conotação não será totalmente inapropriada. De fato, dada a estreita relação entre segurança de memória e corrida de dados, eu realmente quero promover essa conotação.


Motivo educacional


Eu acho que as regras atuais são mais difíceis de entender do que deveriam ser. Não é óbvio, por exemplo, que o &mut T não implica nenhuma propriedade compartilhada. Além disso, a designação &mut T implica que &T não implica mutabilidade, que não é totalmente exata, devido a tipos como Cell . E é impossível concordar sobre como chamá-los ("links mutáveis ​​/ imutáveis" é o mais comum, mas isso não está totalmente correto).


Por outro lado, um tipo como &my T ou &only T parece simplificar a explicação. Esse é um link exclusivo - naturalmente, você não pode forçar dois deles a apontar para o mesmo lugar. E a mutabilidade é uma coisa ortogonal: vem da singularidade, mas também vale para as células. E o tipo &T é exatamente o oposto, um link compartilhado . O RFC PR # 58 fornece vários argumentos semelhantes. Não vou repeti-los aqui.


Motivo prático


Atualmente, existe uma lacuna entre ponteiros emprestados, que podem ser compartilhados ou mutáveis ​​+ exclusivos e variáveis ​​locais que são sempre únicas, mas que podem ser mutáveis ​​ou imutáveis. O resultado final disso é que os usuários devem postar anúncios mut sobre coisas que não são diretamente editáveis.


Variáveis ​​locais não podem ser modeladas usando referências


Esse fenômeno ocorre porque os links não são tão expressivos quanto as variáveis ​​locais. Em geral, isso impede a abstração. Deixe-me dar alguns exemplos para explicar o que quero dizer. Imagine que eu tenho uma estrutura de ambiente que armazena um ponteiro para um contador de erros:


 struct Env { errors: &mut usize } 

Agora eu posso criar instâncias dessa estrutura (e usá-las):


 let mut errors = 0; let env = Env { errors: &mut errors }; ... if some_condition { *env.errors += 1; } 

OK, agora imagine que eu quero separar o código que modifica env.errors em uma função separada. Eu poderia pensar que, como a variável env não é declarada como mutável, posso usar o imutável & link:


 let mut errors = 0; let env = Env { errors: &mut errors }; helper(&env); fn helper(env: &Env) { ... if some_condition { *env.errors += 1; //  } } 

Mas isso não é verdade. O problema é que &Env é um tipo de propriedade compartilhada ( nota do tradutor: como você sabe, mais de uma referência imutável de objeto pode existir por vez ) e, portanto, env.errors aparece em um espaço que permite a propriedade separada do objeto env . Para que esse código funcione, devo declarar env como mutável e usar o link &mut ( nota do tradutor: &mut ) para informar ao compilador que env é único em propriedade, pois apenas uma referência a objeto mutável pode existir por vez e a corrida de dados é excluída, mas mut porque você não pode criar uma referência mutável para um objeto imutável ):


 let mut errors = 0; let mut env = Env { errors: &mut errors }; helper(&mut env); 

Esse problema surge porque sabemos que as variáveis ​​locais são únicas, mas não podemos colocar esse conhecimento em uma referência emprestada sem torná-lo mutável.


Esse problema ocorre em vários outros lugares. Até agora, escrevemos sobre isso de maneiras diferentes, mas continuo sendo assombrado pelo sentimento de que estamos falando de uma pausa, o que simplesmente não deveria ser.


Verificação de tipo para fechamentos


Tivemos que contornar essa limitação no caso de fechamentos. Os fechamentos são abertos principalmente em estruturas como Env , mas não completamente. Isso ocorre porque eu não quero exigir que variáveis ​​locais sejam declaradas mut se forem usadas via &mut em um fechamento. Em outras palavras, pegue algum código, por exemplo:


 fn foo(errors: &mut usize) { do_something(|| *errors += 1) } 

Uma expressão que descreve o fechamento criará realmente uma instância da estrutura Env :


 struct ClosureEnv<'a, 'b> { errors: &uniq &mut usize } 

Confira o link &uniq . Isso não é algo que o usuário final possa inserir. Significa um ponteiro "único, mas não necessariamente mutável". Isso é necessário para passar na verificação de tipo. Se o usuário tentasse escrever essa estrutura manualmente, ele teria que escrever &mut &mut usize , o que exigiria que o parâmetro de errors fosse declarado como mut errors: &mut usize .


Fechamentos e procedimentos não embalados


Prevejo que essa restrição é um problema para fechamentos descompactados. Deixe-me elaborar o design que eu estava considerando. Basicamente, a ideia era que a expressão || é equivalente a algum novo tipo estrutural que implementa uma das características Fn :


 trait Fn<A, R> { fn call(&self, ...); } trait FnMut<A, R> { fn call(&mut self, ...); } trait FnOnce<A, R> { fn call(self, ...); } 

O tipo exato será selecionado de acordo com o tipo esperado, a partir de hoje. Nesse caso, os consumidores de fechamentos podem escrever uma de duas coisas:


 fn foo(&self, closure: FnMut<usize, usize>) { ... } fn foo<T: FnMut<usize, usize>>(&self, closure: T) { ... } 

Nós ... provavelmente queremos corrigir a sintaxe, talvez adicione açúcar como FnMut(usize) -> usize ou salve | usize | -> usize etc. Não é tão importante, é importante que aprovemos o fechamento por valor . Observe que, de acordo com as regras atuais do DST (tipos de tamanho dinâmico), é permitido passar um tipo por valor como argumento para a FnMut<usize, usize> , portanto, o argumento FnMut<usize, usize> é um DST válido e não é um problema.


Além : este projeto não está completo e descreverei todos os detalhes em uma mensagem separada.


O problema é que um link &mut é necessário para encerrar. Como o fechamento é passado por valor, os usuários novamente terão que escrever mut onde parecer fora de lugar:


 fn foo(&self, mut closure: FnMut<usize, usize>) { let x = closure.call(3); } 

Esse é o mesmo problema do exemplo Env acima: o que realmente acontece aqui é que a FnMut do FnMut deseja apenas um link exclusivo , mas como não faz parte do sistema de tipos, solicita um link mutável .


Agora talvez possamos contornar isso de maneiras diferentes. Uma opção que poderíamos fazer é || a sintaxe não se expandiria para um "certo tipo estrutural", mas para um "tipo estrutural ou um ponteiro para um tipo estrutural, conforme ditado pela inferência de tipo". Nesse caso, o chamador pode escrever:


 fn foo(&self, closure: &mut FnMut<usize, usize>) { let x = closure.call(3); } 

Eu não quero dizer que este é o fim do mundo. Mas este é outro passo adiante nas crescentes distorções pelas quais devemos passar para manter essa lacuna entre variáveis ​​e referências locais.


Outras peças da API


Não fiz um estudo exaustivo, mas, é claro, essa diferença aparece em outro lugar. Por exemplo, para ler no Socket , preciso de um ponteiro exclusivo, portanto, devo declarar que é mutável. Portanto, às vezes isso não funciona:


 let socket = Socket::new(); socket.read() // :    

Naturalmente, de acordo com minha sugestão, esse código funcionaria bem. Você ainda receberia uma mensagem de erro se tentasse ler do &Socket , mas seria exibido algo como "é impossível criar um link exclusivo para um link compartilhado", que eu pessoalmente considero mais compreensível.


Mas não precisamos de mut por segurança?


Não, não mesmo. Os programas de ferrugem seriam igualmente bons se você declarasse todas as ligações como mut . O compilador é perfeitamente capaz de rastrear quais variáveis ​​locais estão sendo alteradas a qualquer momento - precisamente porque elas são locais para a função atual. O que o sistema de tipos realmente se importa é a singularidade.


O significado que vejo nas atuais regras de aplicação do mut , e não negarei que tenha valor, é principalmente o fato de ajudarem a declarar a intenção. Ou seja, quando leio o código, sei quais variáveis ​​podem ser reatribuídas. Por outro lado, também passo muito tempo lendo o código C ++ e, francamente, nunca percebi que esse é um grande obstáculo. (O mesmo vale para o tempo que passei lendo código em Java, JavaScript, Python ou Ruby.)


Também é verdade que às vezes encontro bugs porque declarei a variável como mut e esqueci de alterá-la. Penso que poderíamos obter benefícios semelhantes com outras verificações mais agressivas (por exemplo, nenhuma das variáveis ​​usadas na condição do loop muda no corpo do loop). Pessoalmente, não me lembro de me deparar com a situação oposta: ou seja, se o compilador diz que algo deve ser mutável, basicamente significa sempre que eu esqueci a palavra-chave mut algum lugar. (Pense: quando foi a última vez que você respondeu a um erro do compilador sobre uma alteração inválida, fazendo algo diferente de reestruturar o código para validar a alteração?)


Alternativas


Eu vejo três alternativas ao sistema atual:


  1. O que eu apresentei onde você simplesmente joga fora a “mutabilidade” e rastreia apenas a singularidade.
  2. Um em que você tem três tipos de referência: & , &uniq e &mut . (Como escrevi, esse é realmente o sistema de tipos que temos hoje, pelo menos do ponto de vista de um verificador de empréstimos).
  3. Uma opção mais rigorosa, na qual variáveis ​​não mut são sempre consideradas separadas. Isso significaria que você teria que escrever:


     let mut errors = 0; let mut p = &mut errors; // ,  `p`   ,  `mut`. *p += 1; 

    Você precisa declarar p como mut , porque, caso contrário, a variável seria considerada separada, mesmo que seja uma variável local e, portanto, a alteração *p não *p permitida. O que é estranho nesse esquema é que a variável local NÃO permite propriedade separada e sabemos com certeza, porque quando você tenta criar seu alias, ele se move, o destruidor inicia nela, etc. Ou seja, ainda temos o conceito de "propriedade", que é diferente de "não permite propriedade separada".


    Por outro lado, se descrevemos esse sistema, dizendo que a mutabilidade é herdada por meio de ponteiros &mut , sem sequer gaguejar sobre propriedade compartilhada, isso pode fazer sentido.



Destes três, eu definitivamente prefiro o número 1. É o mais simples, e agora estou mais interessado em como podemos simplificar o Rust preservando seu caráter. Caso contrário, dou preferência à que temos agora.


Conclusão


Basicamente, acho que as regras atuais sobre mutabilidade têm algum valor, mas são caras. Eles são uma espécie de abstração fluida: isto é, eles contam uma história simples, que de fato se mostra incompleta. Isso leva à confusão quando as pessoas passam de um entendimento inicial, no qual o &mut reflete como a mutabilidade funciona, para um entendimento completo: algumas vezes, mut necessário apenas para garantir a exclusividade e outras, sem a palavra-chave mut .


Além disso, devemos agir com cautela para manter a ficção, que mut denota mutabilidade, não singularidade. Adicionamos casos especiais para o mutuário verificar os fechamentos. Devemos tornar as regras sobre mutabilidade mutável mais complexas em geral. Devemos adicionar mut aos fechamentos para poder chamá-los ou abrir a sintaxe dos fechamentos de uma maneira menos óbvia. E assim por diante


No final, tudo se transforma em uma linguagem mais complexa como um todo. Em vez de apenas pensar em propriedade compartilhada e singularidade, o usuário deve pensar em propriedade compartilhada e mutabilidade, e os dois estão de alguma forma confusos.


Eu não acho que vale a pena.

Source: https://habr.com/ru/post/pt418735/


All Articles