Programmation Rust, suite
Article mis en ligne le 15 mai 2021
dernière modification le 16 mai 2021

par Laurent Bloch

Cet article est la suite d’un précédent.

 Configurer les séquences pour l’alignement

Après avoir ouvert et lu des fichiers, il faut maintenant lire un fichier au format FASTA, en se limitant pour l’instant à un fichier qui ne contient qu’une seule séquence, séparer la ligne initiale de commentaire (qui contient les identifiants de la séquence) des lignes suivantes (qui contiennent le texte proprement dit de la séquence), puis remettre bout à bout les lignes de texte de la séquence pour avoir une seule chaîne de caractères (le format FASTA ne tolère que des lignes de 120 caractères, ce qui est insuffisant pour des séquences réelles, et impose donc des sauts de lignes nuisibles à un alignement correct). On trouvera deux fichiers d’exemples de séquences au format FASTA à la fin de l’article précédent. Rien de bien difficile a priori, mais nous allons rencontrer les idiosyncrasies originales de Rust.

Le seul module du programme précédent qu’il faut modifier est fasta_read_seq. Comme vu à l’article précédent, la fonction fasta_read_seq reçoit en argument une structure de type Config qui contient, sous forme de chaînes de caractères, les noms des deux fichiers de séquences (nous n’envisageons pas pour l’instant l’alignement multiple, qui considère plusieurs séquences). Il faut passer chacun de ces noms de fichiers à une fonction les_lignes qui va effectuer le travail énoncé au paragraphe précédent et renvoyer, pour chaque fichier un tuple de deux éléments, ident qui contiendra le texte de la ligne initiale de commentaire et sequence qui contiendra le texte de la séquence dans une seule chaîne de caractères.

 Rencontre avec un nouveau modèle de mémoire

Je veux imprimer, séparément, ident et sequence renvoyés par les_lignes. Commençons avec ident :

println!("{}\n", les_lignes(f1).0);

Cette instruction donne bien le résultat attendu, les_lignes(f1) renvoie le tuple (ident, sequence), et les_lignes(f1).0 en extrait le premier élément, numéroté 0, que println!("{}\n", les_lignes(f1).0); se fait un plaisir d’imprimer.

Essayons avec ident et sequence, il ne semble pas y avoir de différence de procédé :

println!("{}\n{}\n", les_lignes(f1).0, les_lignes(f1).1);

Cela ne marche pas. les_lignes(f1).0 a emprunté la variable f1 à fasta_read_seq, qui ne peut plus l’utiliser lors de la seconde tentative d’invocation ! Voici le message d’erreur du compilateur :

  1. error[E0382]: use of moved value: `f1`
  2.   --> src/fasta_files_mgt/fasta_read_seq.rs:15:52
  3.    |
  4. 12 |         let f1 = config.filename1;
  5.    |             -- move occurs because `f1` has type `std::string::String`, which does not implement the `Copy` trait
  6. ...
  7. 15 |     println!("{}\n{}\n", les_lignes(f1).0, les_lignes(f1).1);
  8.    |                                     --                ^^ value used here after move
  9.    |                                     |
  10.    |                                     value moved here

Télécharger

Pour que cela marche, il faut passer les arguments par référence, ainsi :

println!("{}\n{}\n", les_lignes(&f1).0, les_lignes(&f1).1);

et la fonction les_lignes, qui renvoie un tuple de type (String, String), s’écrit ainsi :

  1.     fn les_lignes(f: &String) -> (String, String) {
  2.         let mut sequence = String::new();
  3.         let mut ident = String::new();
  4.         if let Ok(lines) = read_lines(f) {
  5.             for line in lines {
  6.                 if let Ok(texte) = line {
  7.                     if &texte[0..1] == ">" {
  8.                         ident.push_str(&texte);
  9.                     } else {
  10.                         sequence.push_str(&texte);
  11.                     }
  12.                 }
  13.             }
  14.         }
  15.         (ident, sequence)
  16.     }

Télécharger

On remarque la méthode push_str qui concatène son argument à la suite de la chaîne de l’instance qui l’invoque. La fonction read_lines renvoie un itérateur lines sur la lecture du fichier, ce qui évite de le charger en mémoire, et permet d’examiner les lignes une à une. Ces fonctions assez complexes sont très inspirées de la documentation officielle de Rust.

Sur le conseil d’un lecteur j’ai remplacé l’usage d’une tranche (slice) pour repérer les lignes dont le premier caractère est ">" par le recours à la méthode texte.starts_with(">") :

  1. if texte.starts_with(">") {
  2.     ident.push_str(&texte);
  3. } else {
  4.     sequence.push_str(&texte);
  5. }

Télécharger

Le module complet est désormais :

  1. // src/fasta_files_mgt/fasta_read_seq.rs :
  2.  
  3. pub mod fasta_read_seq {
  4.  
  5.     use std::fs::File;
  6.     use std::io::{self, BufRead};
  7.     use std::path::Path;
  8.  
  9.     use crate::fasta_files_mgt::fasta_open_read::fasta_open_read;
  10.  
  11.     pub fn fasta_read_seq(config: fasta_open_read::Config) {
  12.         let f1 = config.filename1;
  13.         let f2 = config.filename2;
  14.  
  15.         println!("{}\n{}\n", les_lignes(&f1).0, les_lignes(&f1).1);
  16.         println!("{}\n{}\n", les_lignes(&f2).0, les_lignes(&f2).1);
  17.     }
  18.  
  19.     fn les_lignes(f: &String) -> (String, String) {
  20.         let mut sequence = String::new();
  21.         let mut ident = String::new();
  22.         if let Ok(lines) = read_lines(f) {
  23.             for line in lines {
  24.                 if let Ok(texte) = line {
  25.                     if texte.starts_with(">") {
  26.                         ident.push_str(&texte);
  27.                     } else {
  28.                         sequence.push_str(&texte);
  29.                     }
  30.                 }
  31.             }
  32.         }
  33.         (ident, sequence)
  34.     }
  35.  
  36.     fn read_lines<P>(filename: P) -> io::Result<io::Lines<io::BufReader<File>>>
  37.     where P: AsRef<Path>, {
  38.         let file = File::open(filename)?;
  39.         Ok(io::BufReader::new(file).lines())
  40.     }
  41. }

Télécharger

Il y a encore des choses inexpliquées dans ce programme. Ce qui est bien avec Cargo et le compilateur Rust, c’est qu’ils émettent des diagnostics perspicaces qui donnent soit directement la réponse au problème, soit des pistes de recherche judicieuses qui, avec l’aide d’un manuel et de la documentation en ligne, aboutissent au résultat. Comme avec Ada, la rigueur du modèle de mémoire et du typage donne du mal pour obtenir un code qui compile, mais une fois que cela compile, la réussite de l’exécution n’est pas loin.