mais ici, tu voudrais qu'il réponde « attention, tu as oublié de vérifier que le pop avait bien réussi ».
C'est l'idée.
Et c'est tout le problème : un compilateur, aussi évolué soit-il, ne peut pas deviner quelle était ton intention.
Est-ce que tu voudrais vraiment d'une écriture tab[index] qui te renvoie un Option<T> que tu es obligé de déconstruire à chaque utilisation ?
Pourquoi pas. Je suis sérieux, c'est là une question de cohérence. Soit on fait confiance au développeur et on considère que s'il demande une connerie, le programme fait une connerie, soit le langage est stricte et ne laisse pas passer des conneries. Là, on est dans un entre deux. Dans l'idée, ça signifierait que tout élément extrait d'un itérable est une Option. C'est générique, simple (même si lourd) et cohérent, donc oui, je préférerais.
Comme je le reconnais, la sûreté, ça se justifie. Laisser des contournements aussi immédiats me semble contradictoire.
Attention à ne pas croire non plus que, parce qu'il ne t'avertit pas qu'il pourrait y avoir un problème, on est dans le cas du C. Je ne connais pas suffisamment Rust pour en être sûr, mais étant donné ce que j'en ai vu, je suis à peu près convaincu que si tu exécutes un programme qui contient ce genre d'erreur, tu vas manger une exception. En C, pour peu que le tableau soit juxtaposé en mémoire avec des données qui appartiennent aussi à ton programme, il va silencieusement continuer à s'exécuter en accédant à ces données (au lieu de ton tableau) comme si de rien n'était… Et paf, ça fait des buffer overflows. Donc non, si tu demandes une connerie, le programme ne va pas « faire une connerie », du moins pas dans le sens où ça se passerait en C ou en C++. Il va s'arrêter en te disant « non, désolé, ça n'est pas valide ». Encore une fois, on aime bien que ce genre de vérification soit fait au maximum à la compilation : avoir la garantie qu'un programme qui compile est correct est un peu le Graal des systèmes de type. Mais c'est un sujet difficile, qui, en plus du fait qu'on veut accessoirement un système de types utilisable, flirte avec l'indécidabilité, ce qui n'arrange rien.
Donc, le type Option ici n'est pas là pour t'assurer que ton programme ne va pas lire des valeurs absurdes. Il est là pour t'assurer que tu as pris en compte, d'une façon ou d'une autre, la possibilité que le pop
ait échoué (parce que le tableau était vide). Finalement, c'est une question de cas d'usages : en règle générale, quand tu demandes à accéder à l'élément d'indice i
d'un tableau, tu t'attends à ce qu'il y ait bien une case i
dans le tableau. Si jamais ça ne marche pas, ça va lever une exception, que tu pourras éventuellement récupérer pour gérer ce cas. Mais on s'attend en règle générale à ce que ça marche. À l'inverse, dans le cas général où on travaille avec des piles, on peut raisonnablement supposer que le cas où la pile est vide est un cas normal d'utilisation, qui ne correspond pas à une erreur, y compris quand on essaye de faire un pop
. Par exemple, si tu te sers de ta pile pour faire un backtracking, ça indiquera qu'il n'y a plus de possibilité de revenir en arrière, et que le problème n'a pas de solution. Comme ce n'est pas à proprement parler un cas d'erreur exceptionnel mais un cas possible, qu'il faut donc traiter, on fait apparaître cette possibilité dans le type de la fonction. Le typeur s'assure ainsi que tu vas prévoir ce cas.
Voilà donc un élément de réponse pour expliquer la différence de comportement entre ces deux fonctions. C'est toujours une histoire de compromis : bien sûr, idéalement, on voudrait que le traitement correct des exceptions soit entièrement vérifié par le typeur. Mais le mieux est l'ennemi du bien : si ton système de type inclut dans le type de chaque fonction la possibilité qu'elle échoue avec un Out_of_memory
, tu n'es pas vraiment plus avancé. Encore une fois, par rapport au C, tu gagnes déjà un avantage énorme en faisant échouer un programme incorrect plutôt qu'en le faisant continuer à s'exécuter dans un état incohérent.
L'autre problème dont tu ne parles pas et qui m'a bien frustré, c'est la doc. La doc de pop dit
Removes the last element from a vector and returns it, or None if it is empty.
Je croyais en lisant ça qu'il retournant soit un élément du vecteur, soit None (comme fait python pour certaines fonctions, par exemple). Sauf qu'en fait, il retourne un truc qu'il faudra évaluer. J'ai complètement mésinterprété la doc, mais avec mon bagage, c'était couru d'avance.
Pour le coup, la faute est entièrement de ton côté, comme tu le remarques. Une documentation n'est ni un document formel (au sens mathématique) qui décrit de façon précise le comportement des fonctions, ni un tutoriel qui explique ce comportement en des termes accessibles à n'importe quel débutant. Il faut un minimum de bagage pour pouvoir la lire, et en l'occurrence, tu ne peux pas comprendre ce que fait cette fonction sans connaître le type Option
. Ce n'est pas le rôle de la documentation de pop
de te l'expliquer. Si tu veux continuer à découvrir le langage (ce que je t'encourage à faire, parce que la curiosité n'est jamais un défaut en programmation), je te conseille plutôt de prendre un document destiné à l'apprentissage, ou au moins à lire la documentation de façon honnête et approfondie : si une fonction renvoie un type que tu ne connais pas, va lire la documentation de ce type avant de te plaindre qu'il n'a pas le comportement que tu supposais sans aucune information. Rust n'a a priori aucune raison de se comporter comme Python, et c'est une mauvaise idée de supposer que c'est forcément le cas quand tu n'es pas allé vérifier.
C'est le but : offrir un langage de programmation système avec un système de types censé qui apporte une sûreté très largement absente du C++ ou du C. Il se trouve qu'effectivement, ces systèmes de types ont été à l'origine développés sur une base de langages fonctionnels.
Donc je fais pleinement partie du public cible du langage. J'ai dit le contraire plus haut, donc je vais un peu développer. Mon usage typique, c'est d'envoyer une simulation qui va tourner pendant 3 jours. Si ça plante, je dois tout recommencer. Le langage doit donc aider à la fiabilité tout en étant rapide. S'il aide à la parallélisation, c'est gagné, je prends. Sauf que. Sauf que mes profs/collègues trouvent que l'excellente doc de numpy est horriblement compliqué et qu'on ne trouve jamais ce qu'on cherche dedans. On m'a parlé de Rust comme d'un truc qui a pour but de remplacer le C++. Mais comment diables voulez-vous que je propose ça à mes collègues ? Théoriquement, c'est génial, mais la marche à franchir est dantesque. La doc, en ne mettant pas le système de vérification dans les exemples, le compilateur en sortant des messages long et pas super clairs n'aident en rien.
Je redis aussi ce que j'ai dit plus haut : avoir un programme sécurisé, ce n'est pas gratuit. Les gens ont souvent la fausse impression, en venant de langages non-sûrs comme Python ou Javascript, que les langages typés sont « casse-couilles » ou pénibles à utiliser, et que l'apport en sécurité serait en réalité quelque chose de pénible : « en Python, j'ai soit un int soit None, je n'ai pas besoin de récupérer l'entier depuis un type Option ». La réalité, c'est qu'un langage sûr, ce n'est pas un langage qui sécurise tes programmes tout seul, c'est un langage qui t'empêche d'écrire des programmes non-sûrs. Mais quand on est habitué à écrire, en Python, des programmes qui ne sont pas corrects parce que c'est facile et rapide et que le compilateur laisse faire, forcément, quand un langage typé refuse, c'est assez vexant. Si tes collègues ne sont pas prêts à reconnaître qu'un programme typé est sensiblement aussi complexe dans les deux langages, et que la seule différence est que les programmes simples et cassés ne seront pas acceptés en Rust, effectivement, ça aura du mal à passer.
Aparté : je ne suis pas informaticien. Mes collègues non plus. Ce n'est pas le cœur de métier, même si c'est massivement utilisé. Quand je dis que mes collègues sont mauvais en programmation, c'est un constat et non une critique : il va falloir faire avec ce niveau en informatique. Quand aux jeunes que j'ai côtoyer, c'est kif-kif.
Soyons honnêtes : s'il existe des études spécialisées dans l'informatique, ce n'est pas pour rien. Programmer n'est pas quelque chose de très compliqué, mais écrire des programmes complexes et corrects demande un travail d'apprentissage réel, qu'il s'agisse de méthodes pour ne pas écrire trop de bêtises dans des langages permissifs ou de l'utilisation pertinente du système de types expressif d'un langage plus strict. Si tes collègues ne sont pas prêts à sauter le pas, il n'existe pas de solution miracle pour que leurs programmes marchent.
Aparté bis : l'un des trois points mis en avant par Rust est le "concurency". S'il aide vraiment à la parallélisation, les opportunités d'un point de vue simulations sont vraiment importantes. D'où une plus grande frustration encore. J'hésite vraiment entre inutilisable pour moi et super utile. Et je n'exclue pas que ce soit les deux.
Comme dit plus haut, je ne connais pas suffisamment Rust pour me lancer sur la question. Je peux toutefois hasarder une réponse : il me semble que Rust utilise des types représentant des permissions pour une fonction d'utiliser une variable (mutable). Si la permission sur une variable est unique, ça garantit que deux fonctions ne peuvent pas simultanément accéder à la même variable mutable : on évite ainsi tous les problèmes de race condition, encore une fois grâce au système de types. Peut-être est-ce pour ça qu'on parle d'un langage adapté pour la concurrence. Est-ce que pour autant c'est une caractéristique facile à utiliser ? Je ne sais pas.