Remarque traducteur: l'enregistrement est daté du 13 mai 2014, donc certains détails, y compris le code source, peuvent ne pas correspondre à l'état actuel des choses. La réponse à la question de savoir pourquoi la traduction d'un article aussi long est nécessaire sera la valeur de son contenu pour former une compréhension de l'un des concepts fondamentaux de la langue Rust, comme la maîtrise de la langue.
Au fil du temps, je suis devenu convaincu qu'il valait mieux abandonner la distinction entre variables locales mutables et immuables dans Rust. Au moins, beaucoup de gens sont sceptiques à ce sujet. Je voulais exprimer ma position en public. Je donnerai divers motifs: philosophiques, techniques et pratiques, ainsi que la défense principale du système actuel. (Remarque: j'ai vu cela comme Rust RFC, mais j'ai décidé que le ton était meilleur pour un article de blog et je n'ai pas le temps de le réécrire maintenant.)
Explication
J'ai écrit cet article de manière assez décisive et je crois que la ligne que je défends sera correcte. Cependant, si nous ne finissons pas de soutenir le système actuel, ce ne sera pas un désastre ou quelque chose comme ça. Il a ses avantages, et dans l'ensemble je le trouve assez agréable. Je pense simplement que nous pouvons l'améliorer.
En un mot
Je voudrais supprimer la distinction entre les variables locales immuables et mutables et renommer les pointeurs &mut
en &my
, &only
ou &uniq
(cela ne &uniq
aucune différence pour moi). Si seulement il n'y avait pas de mot-clé mut
.
Motif philosophique
La principale raison pour laquelle je veux le faire est parce que je pense que cela rendra le langage plus cohérent et plus facile à comprendre. Essentiellement, cela nous réorientera de parler de mutabilité pour parler d' utiliser des alias (que j'appellerai «partage», voir ci-dessous).
La variabilité devient une conséquence de l'unicité: "Vous pouvez toujours changer tout ce à quoi vous avez un accès unique. Les données partagées sont généralement immuables, mais si vous en avez besoin, vous pouvez les changer en utilisant une sorte de type de Cell
."
En d'autres termes, au fil du temps, il est devenu clair pour moi que des problèmes avec la course aux données et la sécurité de la mémoire surviennent lorsque vous avez à la fois l'utilisation d'alias et la mutabilité. Une approche fonctionnelle pour résoudre ce problème consiste à éliminer la mutabilité. L'approche de Rust serait de supprimer l'utilisation d'alias. Cela nous donne une histoire qui peut être racontée et qui nous aidera à la comprendre.
Une note sur la terminologie: je pense que nous devrions nous référer à l' utilisation des alias comme séparation ni la «pseudonymisation» ne permet de comprendre les enjeux ). Dans le passé, nous avons évité cela en raison de ses références multithread. Cependant, si / lorsque nous mettons en œuvre les plans de parallélisation des données que j'ai proposés, cette connotation n'est pas entièrement inappropriée. En fait, étant donné la relation étroite entre la sécurité de la mémoire et la course aux données, je veux vraiment promouvoir cette connotation.
Motif éducatif
Je pense que les règles actuelles sont plus difficiles à comprendre qu'elles ne devraient l'être. Il n'est pas évident, par exemple, que &mut T
n'implique aucune propriété partagée. De plus, la désignation &mut T
implique que &T
n'implique aucune mutabilité, qui n'est pas entièrement exacte, en raison de types tels que Cell
. Et il est impossible de s'entendre sur comment les appeler (les «liens mutables / immuables» sont les plus courants, mais ce n'est pas tout à fait correct).
En revanche, un type comme &my T
ou &only T
semble simplifier l'explication. Il s'agit d'un lien unique - naturellement, vous ne pouvez pas forcer deux d'entre eux à pointer vers le même endroit. Et la mutabilité est une chose orthogonale: elle vient de l'unicité, mais elle vaut également pour les cellules. Et le type &T
est juste son contraire, un lien partagé . RFC PR # 58 fournit un certain nombre d'arguments similaires. Je ne les répéterai pas ici.
Motif pratique
Actuellement, il existe un écart entre les pointeurs empruntés, qui peuvent être partagés ou mutables + uniques, et les variables locales qui sont toujours uniques, mais peuvent être mutables ou immuables. Le résultat final est que les utilisateurs doivent publier des annonces mut
sur des choses qui ne sont pas directement modifiables.
Les variables locales ne peuvent pas être modélisées à l'aide de références
Ce phénomène se produit parce que les liens ne sont pas aussi expressifs que les variables locales. En général, cela empêche l'abstraction. Permettez-moi de vous donner quelques exemples pour expliquer ce que je veux dire. Imaginez que j'ai une structure d'environnement qui stocke un pointeur sur un compteur d'erreurs:
struct Env { errors: &mut usize }
Maintenant, je peux créer des instances de cette structure (et les utiliser):
let mut errors = 0; let env = Env { errors: &mut errors }; ... if some_condition { *env.errors += 1; }
OK, imaginez maintenant que je veux séparer le code qui modifie les env.errors
une fonction distincte. Je pourrais penser que puisque la variable env
n'est pas déclarée comme mutable, je peux utiliser le lien immuable &
:
let mut errors = 0; let env = Env { errors: &mut errors }; helper(&env); fn helper(env: &Env) { ... if some_condition { *env.errors += 1;
Mais ce n'est pas le cas. Le problème est que &Env
est un type de propriété partagée ( note du traducteur: comme vous le savez, plus d'une référence d'objet immuable peut exister à la fois ), et donc les env.errors
dans un espace qui permet une propriété distincte de l'objet env
. Pour que ce code fonctionne, je dois déclarer env
comme mutable et utiliser le lien &mut
( note du traducteur: &mut
) pour dire au compilateur que env
est unique en propriété, car une seule référence d'objet mutable peut exister à la fois et la course aux données est exclue, mais mut
parce que vous ne pouvez pas créer une référence mutable à un objet immuable ):
let mut errors = 0; let mut env = Env { errors: &mut errors }; helper(&mut env);
Ce problème se pose parce que nous savons que les variables locales sont uniques, mais nous ne pouvons pas mettre ces connaissances dans une référence empruntée sans les rendre mutables.
Ce problème se produit dans un certain nombre d'autres endroits. Jusqu'à présent, nous avons écrit à ce sujet de différentes manières, mais je continue d'être hanté par le sentiment que nous parlons d'une pause, qui ne devrait tout simplement pas l'être.
Vérification de type pour les fermetures
Nous avons dû contourner cette limitation en cas de fermeture. Les fermetures sont généralement ouvertes dans des structures telles que Env
, mais pas tout à fait. C'est parce que je ne veux pas exiger que les variables locales soient déclarées mut
si elles sont utilisées via &mut
dans une fermeture. En d'autres termes, prenez du code, par exemple:
fn foo(errors: &mut usize) { do_something(|| *errors += 1) }
Une expression décrivant la fermeture créera en fait une instance de la structure Env
:
struct ClosureEnv<'a, 'b> { errors: &uniq &mut usize }
Consultez le lien &uniq
. Ce n'est pas quelque chose que l'utilisateur final peut saisir. Cela signifie un pointeur "unique mais pas nécessairement modifiable". Cela est nécessaire pour passer la vérification de type. Si l'utilisateur tentait d'écrire cette structure manuellement, il devrait écrire &mut &mut usize
, ce qui nécessiterait à son tour que le paramètre d' errors
soit déclaré comme mut errors: &mut usize
.
Fermetures et procédures déballées
Je prédis que cette restriction est un problème pour les fermetures non emballées. Permettez-moi d'élaborer sur la conception que j'envisageais. Fondamentalement, l'idée était que l'expression ||
est équivalent à un nouveau type structurel qui implémente l'un des traits Fn
:
trait Fn<A, R> { fn call(&self, ...); } trait FnMut<A, R> { fn call(&mut self, ...); } trait FnOnce<A, R> { fn call(self, ...); }
Le type exact sera sélectionné en fonction du type attendu, à compter d'aujourd'hui. Dans ce cas, les consommateurs de fermetures peuvent écrire deux choses:
fn foo(&self, closure: FnMut<usize, usize>) { ... } fn foo<T: FnMut<usize, usize>>(&self, closure: T) { ... }
Nous ... voulons probablement corriger la syntaxe, peut-être ajouter du sucre comme FnMut(usize) -> usize
, ou enregistrer | usize | -> utiliser, etc. Ce n'est pas si important, il est important que nous passions la fermeture en valeur . Veuillez noter que conformément aux règles actuelles de DST (Dynamically-Sized Types), il est permis de passer un type par valeur comme argument au FnMut<usize, usize>
, donc l'argument FnMut<usize, usize>
est un DST valide et n'est pas un problème.
A part : ce projet n'est pas terminé, et je décrirai tous les détails dans un message séparé.
Le problème est qu'un lien &mut
est nécessaire pour appeler une fermeture. Étant donné que la fermeture est transmise par valeur, les utilisateurs devront à nouveau écrire mut
où il semble hors de propos:
fn foo(&self, mut closure: FnMut<usize, usize>) { let x = closure.call(3); }
C'est le même problème que dans l'exemple Env
ci-dessus: ce qui se passe réellement ici, c'est que le FnMut
veut juste un lien unique , mais comme il ne fait pas partie du système de type, il demande un lien mutable .
Maintenant, nous pouvons peut-être contourner cela de différentes manières. Une option que nous pourrions faire est de ||
la syntaxe ne se développerait pas dans un «certain type structurel», mais plutôt dans un «type structurel ou un pointeur vers un type structurel, comme dicté par l'inférence de type». Dans ce cas, l'appelant pourrait écrire:
fn foo(&self, closure: &mut FnMut<usize, usize>) { let x = closure.call(3); }
Je ne veux pas dire que c'est la fin du monde. Mais c'est un autre pas en avant dans les distorsions croissantes que nous devons traverser pour maintenir cet écart entre les variables locales et les références.
Autres pièces API
Je n'ai pas fait d'étude approfondie, mais, bien sûr, cette différence se glisse ailleurs. Par exemple, pour lire à partir de Socket
, j'ai besoin d'un pointeur unique, donc je dois le déclarer mutable. Par conséquent, parfois cela ne fonctionne pas:
let socket = Socket::new(); socket.read()
Naturellement, selon ma suggestion, un tel code fonctionnerait bien. Vous recevrez toujours un message d'erreur si vous essayez de lire à partir de &Socket
, mais il lira alors quelque chose comme "il est impossible de créer un lien unique vers un lien partagé", que je considère personnellement plus compréhensible.
Mais n'avons-nous pas besoin de mut
pour la sécurité?
Non, pas du tout. Les programmes Rust seraient également bons si vous déclariez toutes les liaisons comme mut
. Le compilateur est parfaitement capable de suivre les variables locales qui changent à un moment donné - précisément parce qu'elles sont locales à la fonction actuelle. Ce qui importe vraiment au système de caractères, c'est l'unicité.
Le sens que je vois dans les règles d'application actuelles de mut
, et je ne nierai pas qu'il a de la valeur, c'est avant tout qu'elles aident à déclarer l'intention. Autrement dit, lorsque je lis le code, je sais quelles variables peuvent être réaffectées. D'un autre côté, je passe également beaucoup de temps à lire du code C ++ et, franchement, je n'ai jamais remarqué qu'il s'agit d'une pierre d'achoppement majeure. (Il en va de même pour le temps que j'ai passé à lire du code en Java, JavaScript, Python ou Ruby.)
Il est également vrai que je trouve parfois des bogues parce que j'ai déclaré la variable comme mut
et oublié de la changer. Je pense que nous pourrions obtenir des avantages similaires avec d'autres contrôles plus agressifs (par exemple, aucune des variables utilisées dans la condition de boucle ne change dans le corps de la boucle). Personnellement, je ne me souviens pas d'être confronté à la situation inverse: c'est-à-dire que si le compilateur dit que quelque chose doit être mutable, cela signifie toujours que j'ai oublié le mot-clé mut
quelque part. (Réfléchissez: à quand remonte la dernière fois que vous avez répondu à une erreur du compilateur concernant une modification non valide en faisant autre chose que de restructurer le code pour rendre la modification valide?)
Alternatives
Je vois trois alternatives au système actuel:
- Celui que j'ai présenté où vous jetez simplement la «mutabilité» et ne suivez que l'unicité.
- Celui où vous avez trois types de référence:
&
, &uniq
et &mut
. (Comme je l'ai écrit, c'est en fait le système de type que nous avons aujourd'hui, du moins du point de vue d'un vérificateur d'emprunt.) Une option plus rigoureuse, dans laquelle les variables non mutantes sont toujours considérées comme séparées. Cela signifierait que vous auriez à écrire:
let mut errors = 0; let mut p = &mut errors;
Vous devez déclarer p
comme mut
, car sinon la variable serait considérée comme distincte, même s'il s'agit d'une variable locale, et donc la modification de *p
pas autorisée. Ce qui est étrange dans ce schéma, c'est que la variable locale N'AUTORISE PAS la propriété séparée, et nous le savons avec certitude, car lorsque vous essayez de créer son alias, elle se déplacera, le destructeur démarrera dessus, etc. Autrement dit, nous avons toujours le concept de «propriété», qui est différent de «ne permet pas la propriété séparée».
D'un autre côté, si nous décrivions ce système, en disant que la mutabilité est héritée via des pointeurs &mut
, sans même bégayer sur la propriété partagée, cela pourrait avoir du sens.
De ces trois, je préfère définitivement le n ° 1. C'est le plus simple, et maintenant je suis le plus intéressé par la façon dont nous pouvons simplifier la rouille en préservant son caractère. Sinon, je donne la préférence à celle que nous avons en ce moment.
Conclusion
Fondamentalement, je trouve que les règles actuelles concernant la mutabilité ont une certaine valeur, mais elles sont coûteuses. Ils sont une sorte d'abstraction qui coule: c'est-à-dire qu'ils racontent une histoire simple, qui s'avère en fait incomplète. Cela conduit à la confusion lorsque les gens passent d'une compréhension initiale, dans laquelle &mut
reflète le fonctionnement de la mutabilité, à une compréhension complète: parfois mut
nécessaire que pour garantir l'unicité, et parfois la mutabilité est obtenue sans le mot-clé mut
.
De plus, nous devons agir avec prudence afin de maintenir la fiction, qui mut
désigne la mutabilité et non l'unicité. Nous avons ajouté des cas spéciaux pour que l'emprunteur vérifie les fermetures. Nous devons rendre les règles concernant la mutabilité &mut
mutabilité plus complexes en général. Nous devons soit ajouter mut
aux fermetures pour pouvoir les appeler, soit rendre la syntaxe des fermetures ouverte de manière moins évidente. Et ainsi de suite.
Au final, tout se transforme en une langue plus complexe dans son ensemble. Au lieu de simplement penser à la propriété partagée et à l'unicité, l'utilisateur devrait penser à la propriété partagée et à la mutabilité, et les deux sont en quelque sorte foirés.
Je ne pense pas que cela en vaille la peine.