Skip to content
Snippets Groups Projects
Commit d2e37cbc authored by COQUERY EMMANUEL's avatar COQUERY EMMANUEL
Browse files

TP7

parent ed727c56
No related branches found
No related tags found
No related merge requests found
# TP7 - Applications OCaml
# TP7 - Application et configuration par fonctions
> **Attention sur les machines de TP** les projets `dune` ne fonctionne pas bien
> dans les répertoires utilisateurs sur les machines de TP. Il faut donc
> travailler dans un répertoire local de la machine. Utilisez `/tmp/p123456789`
> comme répertoire de travail, où `p123456789` est votre login étudiant.
>
> Il est préférable de travailler avec un projet forge (voir [rappels gitlab](../gitlab.md))
On va implémenter une petite application simple en OCaml.
Même si l'application tiendra en un seul fichier, elle comprendra plusieurs parties regroupées en _modules_.
**Remarque** `dune` créée un répertoire `_build` qu'il ne faut pas versionner. Pour éviter de le versionner créer à la racine du répertoire cloné un fichier `.gitignore` (attention au `.`) avec la ligne suivante (ajouter la ligne au fichier s'il existe déjà):
## 1. Application et modules
```gitignore
_build
```
On souhaite implémenter une application de gestion de la fabrication des jouets de Noël par les lutins du Père-Noël.
Bien que l'application soit modeste on souhaite pouvoir la faire grossir selon les besoins.
On va donc dès le départ la diviser en différents modules, le code de l'application contenant simplement des appels aux bonnes fonctions des autres modules.
## 0. Compléments API OCaml
Les différentes parties de l'application intiale sont les suivantes:
### 0.1. Entrées sorties en OCaml
- Un module `Association` de gestion des associations clé-valeur basé d'abord sur des listes, puis sur des arbres binaires de recherche. On veut pouvoir, à terme (mais pas dans ce TP), remplacer les usages de ce module par un module de la bibliothèque standard OCaml.
- Un module `Usine` contenant les types liés au métier de l'application: lutins, jouets, etc. Ce module contiendra également les différentes fonctions utilisées pour gérer l'usine.
- Un module `LutinsApp` qui va contenir le code de gestion des arguments en ligne de commande
- Enfin le code hors de ces modules lancera simplement l'application.
En OCaml, il existe deux types permettant de lire et d'écrire dans des fichiers, sur les entrées/sorties standard, sur une socket réseau, etc:
Tous ces modules seront placés dans le fichier `usinelutins.ml`.
On peut résumer les dépendances simples des modules de cette application via le diagramme suivant (la flèche signifie "est utilisé par"):
- `in_channel`: ce type représente des objets (des canaux) à partir desquels on peut lire des octets
- `out_channel`: ce type représente des objets (des canaux) dans lesquels on peut écrire des octets
```mermaid
graph TD
Association --> Usine
Usine --> LutinsApp
LutinsApp --> code directement dans usinelutins.ml
```
Comme dans beaucoup de langages, les entrées / sorties standard et erreur sont prédéfinies dans le module `Stdlib` (qui est ouvert automatiquement dans chaque fichier):
### 2. Premiers modules
- `stdin: in_channel`: entrée standard
- `stdout: out_channel`: sortie standard
- `stderr: out_channel`: sortie erreur
Créer un fichier `usinelutins.ml`. À titre d'exemple de module on donne le code suivant, à placer au début du fichier `usinelutins.ml`:
Pour lire dans un fichier, on utilise `open_in: string -> in_channel` qui fourni un `in_channel` qui lira les octets dans le fichier. Pour écrire, on utilisera `open_out: string -> out_channel`.
```ocaml
module Association = struct
(* Type pour représenter des dictionnaires (ou map ou association) dont la clé est une string *)
type 'a t = (string * 'a) list
(* Fonction pour chercher dans un dictionnaire. get d k renvoie None si k n'est pas une clé de d,
Some x si x est la valeur associée à k dans d. *)
let get : 'a t -> string -> 'a option = fun d k -> List.assoc_opt k d
(* Ajoute un couple clé/valeur dans un dictionnaire, si la clé est déjà dans le dictionnaire,
change en v la valeur qui sera renvoyée par get *)
let put : 'a t -> string -> 'a -> 'a t = fun d k v -> (k, v) :: d
(* Le dictionnaire vide *)
let empty : 'a t = []
(* Supprime les valeurs associées à la clé k dans le dictionnaire. *)
let rec delete : 'a t -> string -> 'a t =
fun d k ->
match d with
| [] -> []
| (k', v') :: rest ->
if k = k' then delete rest k else (k', v') :: delete rest k
end
```
Pour lire dans un canal d'entrée, on peut utiliser `input_line: in_channel -> string` pour lire une ligne.
Remarque: si la fin du fichier est atteinte OCaml génère une exception.
Cette notion n'ayant pas été vue en cours, les fonctions de lecture du contenu intégral d'un fichier seront fournies si le besoin s'en fait sentir.
Créer un module `Usine` à la suite du module `Association`. Dans ce module, créer:
Pour écrire concrètement dans un canal de sortie, on peut par exemple utiliser `output_string: out_channel -> string -> unit`. Comme cette fonction ne calcule pas de valeur, elle renvoie une valeur spéciale `()` de type `unit`. `()` est la seule valeur de ce type. Ce type représente les actions en OCaml. C'est un peu l'équivalent du type `void` de C.
- un type `jour` pour représenter les jours de la semaine (`Lundi`, `Mardi`, etc)
- une fonction `string_of_jour: jour -> string`
- une fonction `jour_opt_of_string: string -> jour option`
Enfin une fois les traitements terminés sur un fichier, il faut le fermer via `close_in: in_channel -> unit` ou `close_out: out_channel -> unit`.
> Ajouter des `assert` pour tester `string_of_jour` et `jour_opt_of_string`.
**[Documentation](https://v2.ocaml.org/releases/4.14/api/Stdlib.html#1_Inputoutput)** des entrées/sorties de base en OCaml.
### 3. Utilisation du module `Usine`
## 1. Application usine de jouets
On souhaite maintenant utiliser ce module. Pour cela il faut:
### 1.1. Reprise de l'usine de jouets
1. Écrire le code qui l'utilise après la définition du module
2. Faire précéder le nom d'une fonction, d'un type ou d'un constructeur par le nom du module, par exemple on écrira `Usine.Lundi`.
> Terminer la partie 2. du [TP5](tp5.md) sur l'usine de lutins.
Ajouter le code suivant à la fin de `usinelutins.ml`:
### 1.2. Gestion de données JSON
```ocaml
let usage () = print_endline "\nUsage: ocaml usinelutins.ml jour"
Dans le fichier `lib/dune`, ajouter la bibliothèque `yojson`, en ajoutant au besoin une section `libraries` après `name`:
let run args =
match args with
| _pgm :: jour_s :: _ -> print_endline "Bonjour, nous sommes un certain jour"
| _ -> usage ()
```ocaml
(library
(name lutins)
(libraries yojson))
let _ = run (Array.to_list Sys.argv)
```
La bibliothèque `yojson` permet de lire des fichiers au format [JSON](https://fr.wikipedia.org/wiki/JavaScript_Object_Notation).
Ci-dessous un exemple de fichier JSON:
```json
{
"lutins": [
"Harcèlemoutons",
"Ravineur",
"Courtaud",
"Lèchecuillère",
"Grattepot",
"Lèchebol",
"Claqueporte",
"Gobeyahourt",
"Chipesaucisse",
"Zieutefenêtre",
"Renifleporte",
"Crocheviande",
"Volebougie"
],
"jouets": [
"toupie",
"osselets",
"ballon",
"nounours",
"robot",
"poupée",
"cubes"
],
"jours": {
"lundi": { "attribution": "longueur_nom", "nombre": "toujours_42" },
"mardi": { "attribution": "premiere_lettre", "nombre": "carre_nom" },
"mercredi": { "attribution": "longueur_nom", "nombre": "diff_nom" },
"jeudi": { "attribution": "premiere_lettre", "nombre": "toujours_42" },
"vendredi": { "attribution": "longueur_nom", "nombre": "carre_nom" },
"samedi": { "attribution": "premiere_lettre", "nombre": "diff_nom" },
"dimanche": { "attribution": "longueur_nom", "nombre": "toujours_42" }
}
}
```
> Modifier ce code afin afficher en plus `Nous sommes ` suivi d'un jour de la semaine traduit en string via un appel à `string_of_jour`.
Les fichiers JSON permettent de représenter des structures imbriquées arbitrairement.
Une fois lues depuis le fichier cette structure est représentée par un type inductif, [`Yojson.Basic.t`](https://ocaml-community.github.io/yojson/yojson/Yojson/Basic/index.html):
> Vérifier le bon fonctionnement du programme en lançant `ocaml usinelutins.ml`. Modifier le code de façon à ce que le jour choisi soit obtenu à partir de `jour_s`. Gérer par un affichage approprié le cas ou `jour_s` n'est pas un jour.
```ocaml
type t =
| `Null
| `Bool of bool
| `Int of int
| `Float of float
| `String of string
| `Assoc of (string * t) list
| `List of t list
```
### 4. Enrichir le module `Usine`
Attention à la syntaxe avec les quotes inversées `` ` `` devant les constructueurs.
`Yojson` utilise des constructeurs particuliers qui sont distingués via cette syntaxe.
En pratique et dans le cadre de ce programme, on pourra les utiliser comme des constructeurs habituels, mais les messages d'erreurs seront un peu différents.
Il sera ici particulièrement important de **bien spécifier les types des fonctions** qui vont manipuler ces structures.
L'exemple de fichier ci-dessus produira uniquement des structures utilisant les 3 constructeurs suivants:
- `` `String`` pour les valeurs textuelles comme par exemple `"Harcèlemoutons"` qui sera représenté par la structure `` `String "Harcèlemoutons"``
- `` `List`` pour les listes de valeurs comme `[ "toupie", "osselets"]` qui sera représentée par la structure: `` `List [ `String "toupie"; `String "osselets"]``
- `` `Assoc`` pour les dictionnaires clé-valeur comme `{ "attribution": "longueur_nom", "nombre": "toujours_42" }` qui sera représentée par la structure: `` `Assoc [("attribution", `String "longueur_nom"); ("nombre", `String "toujours_42")]``.
Une `` `List`` ou une `` `Assoc`` peuvent contenir des éléments plus complexe que des `` `String``.
Par exemple la version simplifiée du fichier précédent:
```json
{
"lutins": ["Harcèlemoutons", "Ravineur"],
"jouets": ["toupie", "osselets"],
"jours": {
"lundi": { "attribution": "longueur_nom", "nombre": "toujours_42" },
"mardi": { "attribution": "premiere_lettre", "nombre": "carre_nom" }
}
}
```
On veut maintenant définir au sein de l'usine de fabrication de jouets une notion de _configuration_ de la journée. La configuration d'une journée permet de connaître deux choses: pour chaque lutin, le jouet qu'il va fabriquer et pour chaque lutin et chaque jouet la quantité qu'il peut fabriquer. Les jouets et les lutins étant représentés par des chaînes de caractères, il se peut qu'un lutin ou un jouet passé en argument n'existe pas. On utilise donc une `option` pour gérer cette situation.
On va maintenant créer dans le module `Usine` un type `configuration` qui sera simplement une paire de fonctions. La première aura le type `string -> string option`, la seconde aura le type `string -> string -> int option`.
sera représentée par la structure suivante:
Définir ce type le module `Usine`:
```ocaml
`Assoc [
("lutins", `List [ `String "Harcèlemoutons"; `String "Ravineur" ]);
("jouets", `List [ `String "toupie"; `String "osselets"]);
("jours", `Assoc [
("lundi", `Assoc [("attribution", `String "longueur_nom");
("nombre", `String "toujours_42")]);
("mardi", `Assoc [("attribution", `String "premiere_lettre");
("nombre", `String "carre_nom")])
])
]
type configuration = (string -> string option) * (string -> string -> int option);;
```
Dans le module `Usine` ajouter une fonction `get_lutins: Yojson.Basic.t -> string list` qui renverra la liste des lutins à partir d'une structure issue d'un fichier similaire au fichier exemple ci-dessus. Il faudra donc aller chercher la liste dans l'entrée "lutins" et traduire la liste de `` `String``.
Ajouter les fonctions suivantes dans le module `Usine`:
Faire de même pour les jouets avec une fonction `get_jouets: Yojson.Basic.t -> string list`. Essayez de mutualiser du code entre ces deux fonctions en créant une ou plusieurs fonctions intermédiaires.
- `mk_configuration: (string -> string option) -> (string -> string -> int option) -> configuration` cette fonction va fabriquer une paire avec ses deux arguments.
- `get_jouet: configuration -> (string -> string option)` cette fonction va extraire le premier élément de la paire
- `get_nb_jouets: configuration -> (string -> string -> int option)` cette fonction va extraire le deuxième élément de la paire
Tester ces deux fonctions via `assert` en utilisant l'exemple de structure ci-dessus.
Tester ces fonctions avec des `assert` en utilisant une fonction qui renvoie toujours "toupie" pour le choix du jouet et toujours `42` pour le nombre de jouets.
### 1.3. Configuration de l'usine à partir du fichier json
### 5. Codage de l'application de gestion des jouets - début
On souhaite maintenant créer l'association contenant la configuration par jour à partir du contenu d'un fichier JSON structuré comme le fichier exemple précédent.
Dans le fichier exemple, chaque jour de la semaine se voit associé deux chaînes de caractères:
On dispose à présent des éléments de langage pour pouvoir coder le début de l'application de gestion de l'usine de jouets.
- la chaîne correspondant à la clé `attribution` va permettre de désigner la fonction à utiliser pour choisir le jouet qui sera fabriqué par un lutin;
- la chaîne correspondant à la clé `nombre` va permettre de désigner la fonction à utiliser pour, étant donné un lutin et un jouet, savoir combien d'exemplaires du jouet le lutin va fabriquer.
Dans le module `Usine`, créer une configuration basée sur des `assoc` pour créer une variable globale pour y stocker, pour chaque jour, une configuration. Essayer de faire varier les fonctions de la configuration selon le jour.
Commencer par créer un module `FonctionsUsine` contenant les fonctions suivantes:
Créer également une variable globale contenant la liste des noms des lutins.
- `longueur_nom (jouets: string list) (lutin: string): string option` qui renverra le `i`<sup>ème</sup> jouet de la liste `jouets` si la longueur du (nom du) lutin est `i` (c.f. [String.length](https://v2.ocaml.org/releases/4.14/api/String.html#VALlength)). On travaillera modulo la longueur de la liste de jouets pour gérer les noms de lutins trop longs.
- `premiere_lettre` fonctionne comme la précédente, mais en utilisant pour `i` le code ASCII de la premère lettre du nom du lutin (c.f. [Char.code](https://v2.ocaml.org/releases/4.14/api/Char.html#VALcode) et [String.get](https://v2.ocaml.org/releases/4.14/api/String.html#VALget)).
- `toujours_42: string -> string -> int option` renverra toujours `Some 42`.
- `carre_nom: string -> string -> int option` reverra la somme des carrés des longueurs des chaînes passées en argument.
- `diff_nom: string -> string -> int option` renverra la taille de la différence symétrique des ensembles de lettres de chaînes passées en argument. Autrement dit, elle renvoie le nombre de caractères apparaissant dans la première chaîne mais pas dans la deuxième additionné au nombre de caractères apparaissant dans la deuxième mais pas dans la première.
Créer une fonction `calcule_jouets_config: configuration -> (string,int) list` qui indique pour chaque jouet combien d'exemplaires ont été fabriqué, en excluant les jouets fabriqués zéro fois.
Dans `usine.ml`, créer une fonction `fonctions_choix_jouets (jouets: string list): (string * (string -> string option)) list` qui prend en argument une liste de jouets et créer la liste d'association contenant les paires `("premiere_lettre", FonctionsUsine.premiere_lettre jouets)` et `("longueur_nom", FonctionsUsine.longueur_nom jouets)`.
On remarque l'application partielle des fonctions `FonctionsUsine.premiere_lettre` et `FonctionsUsine.longueur_nom` pour obtenir des fonctions de type `string -> string option` que l'on pourra placer dans une `configuration`.
Enfin créer un dernier module `LutinsApp` qui contiendra:
Réaliser un travail similaire pour créer une liste d'association pour obtenir les fonctions permettant d'obtenir le nombre de jouets fabriqués par un lutin pour un modèle de jouet.
- une fonction `affiche_jouets: (string,int) list -> string` qui calculera une chaîne d'affichage de la sortie de la fonction `calcule_jouets_config`;
Créer enfin une fonction `configurations_of_json: Yojson.Basic.t -> ((jour, configuration) Association.assoc_t) option` qui prendra une structure JSON similaire au fichier exemple et construira une structure similaire à la variable globale contenant les configurations de chaque jour (normalement de type `(jour, configuration) Association.assoc_t`). La fonction reverra en fait une `option` qui vaudra `None` si la structure JSON contient des valeurs inconnues pour le nom des fonctions ou s'il y manque des informations (par exemple si elle ne contient pas de jouets).
Modifier la fonction `run: string list -> unit` pour affichera (via `print_endline`) les jouets produits durant le jour passé en argument.
Pour terminer, dans le module `LutinsApp`, modifier le code de façon à:
### 6. Optimisation du module Association
- prendre un argument supplémentaire en ligne de commande (le nom du fichier JSON à lire);
- lire le contenu du fichier via la fonction `Yojson.Basic.from_channel: in_channel -> Yojson.Basic.t` (le `in_channel` étant lui-même obtenu via `open_in` comme expliqué au début du TP);
- utiliser la structure obtenue pour fabriquer les listes de lutins et de jouets, créer la configurations des jours et enfin calculer la quantité produite pour chaque jouet en fonction du jour indiqué comme autre argument en ligne de commande.
Changer le type et le code du module `Association` de façon à utiliser des arbres binaires de recherche à la place de listes de paires. Le code du reste de l'application ne devrait pas changer.
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment