Programmation Fonctionnelle, TP4

Htmligure
Version & licenses
Creative Commons License

Programmation Fonctionnelle, TP4 : Script de recensement de fichiers sources

Guyslain Naves

OCaml étant un langage interprétable, on peut l'utiliser en remplacement de tout langage de script, par exemple à la place de Bash. Il existe même des libraires OCaml permettant de faire facilement les opérations liés au système de fichiers ou au système d'exploitation ( shcaml par exemple).

Pour ce TP, on reprend le script vu en cours, et on va lui apporter des fonctionnalités supplémentaires.

Commencez par créer un nouveau répertoire, mettez-y le script. Ajoutez en première ligne :

  1. #!PATH_TO_OCAML

en remplacement PATH_TO_OCAML par le résultat de which ocaml.

En ligne de commande, rendez le script executable :

  1. chmod +x script_source_size.ml

Puis testez le script, par exemple sur le répertoire courant.

  1. ./script_source_size.ml .

Il doit vous afficher un nombre d'octets.

Relisez bien tout le script et assurez-vous de bien comprendre comment il fonctionne, puisque vous allez ensuite devoir le modifier. Le script utilise la librairie Core. Créez donc un fichier .merlin pour référencer l'usage de Core et profiter de l'auto-complétion et des indications de types. La documentation pour Core 0.10 est ici, celle pour Core 0.9 est là. En particulier on utilise les modules suivantes (ouvrez-les dans différents onglets pour y avoir accès facilement pendant la durée du TP) :

  • List (opérations sur les listes),
  • Option (opérations sur les options),
  • Filename (manipulation des noms de fichiers),
  • Sys (interaction avec le système de fichiers),
  • Unix (interaction avec le système d'exploitation).

Une première modification.

Pour s'échauffer, on modifie le script de sorte à retourner, non pas le nombre d'octets des fichiers sources, mais le nombre de lignes de code. Pour cela il faut ouvrir le fichier en lecture, extraire les lignes, et fermer le fichier. On utilise ces trois fonctions :

  1. val In_channel.create : string -> in_channel
  2. val In_channel.input_lines : in_channel -> string list
  3. val In_channel.close : in_channel -> unit

Le type in_channel représente les sources de caractères : fichiers ouverts en lecture ou sortie d'un tube.

Écrivez une fonction nbr_of_lines_of_file : string -> int, qui au nom d'un fichier retourne le nombre de lignes du fichier (utilisez la fonction déjà définie pour trouver la longueur de la liste).

Puis, modifiez le script pour qu'il calcule le nombre total de lignes de code des fichiers d'un répertoire et de ses sous-répertoires.

Vérifiez votre modification en calculant le nombre de lignes de code du répertoire courant (qui devrait être le nombre de lignes du script).

Collecter plusieurs statistiques.

Pourquoi choisir entre nombre d'octets et nombre de lignes de codes ? Autant calculer les deux à la fois. On pourrait utiliser un couple pour encoder les deux informations, mais on voudra peut-être en ajouter d'autres par la suite. Donc nous allons utiliser un enregistrement.

On introduit le nouveau type :

  1. module Stats =
  2. struct

  3. type t = (* complete name is Stats.t *)
  4. { lines_of_code : int;
  5. bytes : int
  6. }

  7. end

Nous mettons ce nouveau type dans un module pour garder le code organisé. Idéalement, on en ferait un nouveau fichier, mais comme nous ne comptons pas le compiler, merlin n'en tiendra pas compte et ce ne sera pas pratique (on pourrait néanmoins le faire, il faudrait juste utiliser #use stats.ml au début du script).

Toujours pour organiser, déplacez les définitions de nbr_of_lines_of_file et size_of_file dans le module Stats.

Créez ensuite une fonction stats_of_file qui étant donné un nom de fichier, retourne ses statistiques. La syntaxe pour créer un enregistrement est :

  1. { lines_of_code = ???; (* replace [???] by an expression *)
  2. bytes = ???;
  3. }

Pour intégrer cela à la suite du script, il nous faut deux ingrédients. Le premier, c'est le fait qu'avec seulement un entier, il suffisait de faire des additions pour tenir compte des multiples fichiers. Du coup, il nous faut une fonction d'addition sur les statistiques, et une valeur zéro. Définissez les deux fonctions suivantes au module Stats :

  1. val zero : Stats.t
  2. val add : Stats.t -> Stats.t -> Stats.t

Un type muni d'un zéro et d'une fonction d'addition associative (et ayant zéro pour neutre) s'appelle un monoïde. On en trouve partout en informatique, c'est même une des structures fondamentales en programmation fonctionnelle. Quels autres monoïdes connaissez-vous ?

Ajoutez maintenant une fonction qui retourne les statistiques d'un fichier dont le nom est donné en argument. Puis une fonction donnant une représentation par chaîne de caractères d'une statistique en utilisant le module Printf (toujours dans le module Stats).

  1. val of_file : string -> Stats.t
  2. val to_string : Stats.t -> string

Modifiez maintenant le reste du script pour utiliser le type Stats.t. Les modifications devraient être mineures ! Testez le script modifié.

Collecter les statistiques par type de fichier.

Différentes sortes de fichiers.

Nous souhaitons maintenant enregistrer indépendamment les fichiers de chaque extension possible. On commence par créer un type pour distinguer les différentes sortes de fichiers. Un fichier a une extension et un langage associé, cela nous donne :

  1. module Kind =
  2. struct
  3. type t =
  4. { language : string;
  5. extension : string
  6. }

  7. end

Déplacez la liste des extensions dans ce module Kind. Modifiez la liste, de sorte que chaque extension soit associée à son langage : on aura donc une liste de paire de chaînes de caractères.

  1. val extensions : (string * string) list

Il nous faut maintenant une fonction qui a un nom de fichier associe sa sorte de type Kind.t. Pour cela, créez d'abord une fonction qui a une extension associe sa sorte. Bien sûr, il peut ne pas y avoir de sorte associée, donc la fonction retourne une option. Pour trouver la bonne extension dans la liste, utilisez List.Assoc.find. Il faudra ensuite transformer le contenu de l'option, quelle fonction du module Option utiliser ?

  1. val of_extension : string -> Kind.t option

Ensuite, ajoutez une fonction of_file utilisant Filename pour retrouver l'extension du nom de fichier. À nouveau, cette fonction peut retourner une option. Il vous faudra une autre fonction de Option, de quelle type doit-elle être ?

Enfin, ajoutez une fonction pour comparer les sortes. Pour l'instant, on compare les langages par ordre alphabétique (l'ordre naturel des string), en cas d'égalité, on compare les extensions.

  1. val compare_extension : Kind.t -> Kind.t -> int
  2. val compare : Kind.t -> Kind.t -> int
  3. val equal : Kind.t -> Kind.t -> bool

Ceci termine le module Kind.

Collecter toutes les données.

Il nous faut maintenant un type capable de contenir toutes les informations pour les différentes sortes de fichiers. Idéalement il nous faut un dictionnaire associant à chaque sorte sa comptabilité. Nous apprendrons à utiliser les dictionnaires de la librairie standard dans un prochain cours (ils utilisent une fonctionnalité que nous introduirons plus tard). En attendant, nous utilisons le dictionnaire du pauvre : une liste associative, c'est-à-dire une liste de paires (clé,valeur). Les fonctions associées sont dans le module List.Assoc (qui manque cruellement de documentation, mais ça devrait être compréhensible néanmoins).

Créez un nouveau module Digest à la suite de Kind.

  1. module Digest =
  2. struct
  3. type digest = (Kind.t * Stats.t) list
  4. end

Nous voulons en faire un monoïde bien sûr. Ajoutez une valeur empty, qui sera le zéro.

L'addition sera la fonction merge fusionnant deux listes associatives. Avant de l'écrire, il nous faut une fonction pour ajouter un nouveau fichier dans une liste associative. Si la sorte du fichier est déjà référencée, il faut augmenter les statistiques associées. Sinon il faut simplement ajouter un nouvel élément. Écrivez donc la fonction :

  1. val add : Digest.t -> (Kind.t * Stats.t) -> Digest.t

Utilisez les fonctions mem,find,add de List.Assoc et les fonctions de Option.

Ensuite, en utilisant une seule fonction du module List, ajoutez :

  1. val merge : Digest.t -> Digest.t -> Digest.t

Il reste à écrire une fonction pour créer un Digest.t depuis un nom de fichier, puis des fonctions de conversions en chaînes de caractères (utilisez String.concat) :

  1. val of_file : string -> Digest.t
  2. val item_to_string : (Kind.t * Stats.t) -> string
  3. val to_string : Digest.t -> string

Modifiez alors le reste du script : de nouveau il n'y a pas grand chose à changer.

Améliorer l'affichage.

On se propose maintenant d'améliorer un peu la sortie, de sorte à afficher un joli tableau :

la sortie.

Pour cela, ajoutons encore un module, qui gère l'affichage de tableaux, donnés comme la liste des entête de colonnes, plus la liste des lignes (chaque ligne est une liste de chaînes de caractères).

  1. module ArrayDisplay =
  2. struct


  3. end

Ajoutez une fonction compute_widths_of_line qui calcule la largeur nécessaire à chaque colonne d'une ligne donnée. Puis il nous faut une fonction prenant deux listes de même longueur, et calculant la liste des maximums coordonnée par coordonnée (si le nombre de colonnes était déterminé, on pourrait ajouter un zéro et avoir un autre monoïde, mais ce n'est pas le cas). Ensuite, utilisez ces deux fonctions pour faire une fonction qui, étant donnée une liste de lignes, calcule pour chaque colonne la largeur nécessaire. Utilisez les fonctions de List pour cela.

  1. val compute_widths_of_line : string list -> int list
  2. val max_widths : int list -> int list -> int list
  3. val compute_widths : string list list -> int list

Il nous faut ensuite une fonction pad qui ajoute des espaces à une chaîne pour obtenir une chaîne de longueur voulue, une fonction format prenant la largeur de chaque colonne, le contenu de chaque colonne, et retourne la chaîne de caractère contenant toute la ligne.

  1. val pad : int -> string -> string (* use String.make *)
  2. val format : widths:(int list) -> string list -> string (* use String.concat *)

il reste à écrire la fonction principale, ce qui devrait être facile maintenant (utilisez Printf.printf "%s\n% pour afficher chaque ligne) :

  1. val display : header:string list -> body:string list list -> unit

Ceci termine le module ArrayDisplay.

Ajoutez maintenant aux modules Stats, Kind et Digest des fonctions header retournant les entêtes des informations du module, et to_strings retournant les lignes pour chaque valeur.

  1. val Stats.header : string list
  2. val Stats.to_strings : Stats.t -> string list

  3. val Kind.header : string list
  4. val kind.to_strings : Kind.t -> string list

  5. val Digest.header : string list
  6. val Digest.to_strings : Digest.t -> string list list

Puis modifiez la fonction principale show_dir_size pour terminer l'ajout de cette fonctionnalité.

À vous de jouer...

Quelques idées d'améliorations possibles :

  • Ajoutez des statistiques en plus, par exemple le nombre de fichier ou la longueur moyenne d'une ligne non-vide, des types de fichiers sources.
  • Ajoutez des options à la ligne de commande pour trier le tableau selon divers critères.
  • Ajoutez des options à la ligne de commande pour ne comptabiliser que sur un langage.