Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • programmation-fonctionnelle/lifpf
  • p2208151/lifpf
  • p2402635/lifpf
  • p2103541/lifpf
  • p2105194/lifpf
  • p2210488/lifpf
  • p2212652/lifpf
  • p2019985/lifpf
  • p2208660/lifpf
  • p2102825/lifpf
  • p2002599/lifpf
  • p2106670/lifpf
  • p2103270/lifpf
  • p2210897/lifpf-demo
  • p2110686/lifpf-aysoy
  • p2110638/lifpf
  • p2202556/lifpf
17 results
Show changes
# LIFPF TP4: Structures et fonctions génériques et mutuellement récursives
Lancez régulièrement tous vos tests via `ocaml tp4.ml` depuis le bash ou via `#use "tp4.ml";;` depuis l'interpréteur interactif.
## 1. Arbres n-aires
Un arbre n-aire est un arbre dans lequel chaque noeud peut avoir un _nombre quelconque de fils_.
Dans le cadre de cet exercice, on va considérer des arbres dont les données sont stockées **uniquement** dans les feuilles.
Une forêt est une liste d'arbres.
Un noeud contiendra donc simplement une forêt.
### 1.1. Type arbre n-aire
> Définir un type `'a arbre_n` pour représenter les arbres n-aires contenant des éléments de type `'a`. Un arbre est soit _une feuille contenant un élément_, soit un _noeud contenant une forêt_.
>
> Définir quelques arbres pour pouvoir tester.
>
> Donner deux exemples d'arbres ne contenant **aucun** élément.
>
> Quel est le type inféré par OCaml pour ces arbres ? Pourquoi ?
### 1.2. Hauteur d'un arbre
On souhaite maintenant pouvoir calculer la hauteur d'un arbre.
Comme un arbre peut contenir une forêt, il faut deux fonctions mutuellement récursives: une fonction qui donne la hauteur d'un arbre et une autre qui donne la hauteur maximale des arbres dans une forêt.
> Coder `hauteur_arbre` et `hauteur_foret`, deux fonctions mutuellement récursives qui calculent respectivement la hauteur d'un arbre et la hauteur maximale des arbres d'une forêt.
> Tester avec `assert`.
**Appeler votre chargé de TP pour faire valider votre progression**
### 1.3. Éléments d'un arbre
On souhaite maintenant pouvoir construire la liste des éléments d'un arbre.
Comme un arbre peut contenir une forêt, il faut **deux fonctions mutuellement récursives** :
- une fonction qui donne les éléments _d'un arbre_;
- une autre qui donne les éléments _d'une forêt_.
Par ailleurs on veut éviter de faire des concaténations qui vont s'avérer coûteuses, on va donc écrire ces fonctions en **s'appuyant sur un accumulateur**.
Les fonctions vont ainsi ajouter les éléments de l'arbre/de la forêt à l'accumulateur.
> Coder deux fonctions mutuellement récursives `list_of_arbre_aux` et `list_of_foret` qui vont prendre un arbre / une forêt en argument, ainsi qu'une liste d'éléments `acc` qui vont ajouter en tête de `acc` les éléments de l'arbre / de la forêt.
> Vous pouvez aussi commencer par écrire une version **sans** accumulateur et la modifier ensuite.
>
> Tester avec `assert`.
>
> Placer la définition des deux fonctions à l'intérieur d'une fonction `list_of_arbre` (via un `let ... in ...`).
> Le code de `list_of_arbre` fera ensuite simplement appel à `list_of_arbre_aux` avec un accumulateur initial vide.
>
> Modifier les tests précédents pour tester `list_of_arbre` et pas `list_of_arbre_aux` (qui est maintenant masquée).
### 1.4. Minimum d'un arbre
On veut maintenant extraire _l'élément minimal d'un arbre_.
Attention, si l'arbre est vide, le minimal n'est **pas** défini, le résultat sera donc un `'a option`.
Il faudra prendre en compte cette spécificité lors du traitement des appels récursifs, typiquement en faisant un pattern matching sur le résultat de l'appel récursif.
**Remarque :** la fonction `min` est prédéfinie en OCaml. Son type est `'a -> 'a -> 'a`.
On l'utilisera pour obtenir le minimum de deux éléments de l'arbre.
On évitera cependant de l'utiliser directement avec des `option` car elle ne donnera pas le résultat voulu.
Par exemple, `min None (Some 3)` vaut `None`.
> Définir deux fonctions mutuellement récursives `minimum` et `minimum_foret` qui donne le minimum d'un arbre / d'une forêt.
> Tester avec `assert`.
**Faire valider votre progression par votre chargé de TP**
### 1.5. Reduce
On peut remarquer que dans le code de `minimum` et `minimum_foret`, la fonction `min` est utilisée pour combiner deux résultats.
On pourrait donc généraliser ce code pour qu'il fonctionne avec **n'importe quelle fonction de combinaison des résultats**.
La fonction généralisée est souvent appelée `reduce`.
On veut donc coder les fonctions `reduce` et `reduce_foret` qui généralisent `minimum`.
Elles prendront un argument supplémentaire `f` qui est la fonction de combinaison des résultats.
Dans le code de ces fonctions, `f` sera utilisée à la place de `min`.
Si l'arbre / la forêt contient des éléments de type `'a`, alors le type de `f` sera `'a -> 'a -> 'a`.
> Coder `reduce` et `reduce_foret`.
> Tester en reprenant les cas de test utilisés pour `minimum`, en utilisant `min` comme valeur pour `f`.
Si ce n'est pas déjà fait, définir quelques exemples arbres dont le contenu sera de type `int`.
> Tester `reduce` et `reduce_foret` avec un fonction qui fait l'addition (en `int`) de ses deux arguments, ou le produit des éléments, le maximum etc.
**Remarque** : le type de l'addition est `int -> int -> int`.
Cela fonctionne car le type de `reduce` est `('a -> 'a -> 'a) -> 'a arbre_n -> 'a option`, donc `reduce` a aussi le type `(int -> int -> int) -> int arbre_n -> int option`.
**Faire valider votre progression par votre chargé de TP**
## 2. FIFOs basées sur des listes
On souhaite implémenter une structure de file (en anglais **FIFO**: _First In First Out_).
Dans ce type de structure, on veut pouvoir ajouter et retirer des éléments de manière à ce que les éléments soient retirés _dans l'ordre où ils ont été ajoutés_.
Une implémentation naïve de FIFO basée sur une liste pose des problèmes de performance :
- soit on ajoute les nouveaux éléments _en tête de liste_, mais il faut alors les retirer en fin de liste ce qui force à récrire toute la liste à chaque supression;
- soit on ajoute _en fin de liste_, mais ici c'est le coût à l'insertion qui est prohibitif puisqu'on reconstruit la liste au moment de l'ajout.
Une autre technique consiste à utiliser **deux** listes.
La première (à gauche) est destinée à recevoir les nouveaux éléments, alors que la seconde à droite sera utilisée pour stocker les éléments à retirer :
- dans la liste de gauche, les éléments sont stockés dans l'ordre **inverse** de leur insertion : le dernier élément inséré est en tête de liste.
- dans la liste de droite, les éléments sont stockés dans l'ordre où ils doivent être récupérés, c'est-à-dire **le plus ancien en tête de liste**.
Reste à transférer des éléments entre la liste de gauche et celle de droite.
C'est là que cette technique trouve son efficacité : on transfère de la gauche à la droite lorsque l'on essaie de retirer un élément de la liste de droite alors qu'elle est vide.
On profite de ce transfert pour renverser l'ordre des éléments (comme dans `renverse_ajoute` du TP2).
Chaque élément est donc placé exactement _une fois dans chacune des listes_.
Donc remplir et vider la file s'effectue donc avec un nombre d'opérations linéaire dans le nombre d'éléments ajoutés et retirés, contrairement aux versions naïves pour lesquelles le nombre d'opérations est quadratique.
### 2.1. Exemple
###### 1. On suppose qu'on a la situation de départ suivante :
Gauche
```mermaid
flowchart LR
4 --> 3 --> vide
```
Droite
```mermaid
flowchart LR
1 --> 2 --> vide
```
###### 2. On retire le premier élément (1) :
Gauche
```mermaid
flowchart LR
4 --> 3 --> vide
```
Droite
```mermaid
flowchart LR
2 --> vide
```
###### 3. On ajoute un élément (42) :
Gauche
```mermaid
flowchart LR
42 --> 4 --> 3 --> vide
```
Droite
```mermaid
flowchart LR
2 --> vide
```
###### 4. On retire le premier élément (2) :
Gauche
```mermaid
flowchart LR
42 --> 4 --> 3 --> vide
```
Droite
```mermaid
flowchart LR
vide
```
###### 5. On retire le premier élément (3) :
On effectue d'abord le transfert des éléments de gauche à droite, puis on retire l'élément comme avant
**Transfert :**
Gauche
```mermaid
flowchart LR
vide
```
Droite
```mermaid
flowchart LR
3 --> 4 --> 42 --> vide
```
**Retirer 3 :**
Gauche
```mermaid
flowchart LR
vide
```
Droite
```mermaid
flowchart LR
4 --> 42 --> vide
```
### 2.2 Type FIFO et ajout d'éléments
> Définir un type `'a fifo` avec un seul constructeur contenant deux listes dont les éléments sont de type `'a`.
> Définir quelques exemples d'éléments de ce type.
<!-- pour séparer les citations -->
> Définir une fonction `push_fifo` qui prend en argument un élément `e` et une fifo `f` et renvoie la fifo contenant les éléments de `f` puis `e` en insérant `e` en tête de la liste de gauche.
> Tester avec `assert` et les fifos exemples créées précédement.
<!-- pour séparer les citations -->
> Définir une fonction `push_list_fifo` qui prend une liste d'éléments `l` et une fifo `f` et renvoie la fifo contenant tous les éléments de `f` suivis de tous les éléments de `l` insérés dans l'ordre de `l` (i.e. l'élément en tête de `l` avant les autres).
> Tester avec `assert` et les fifos exemples créées précédement.
**Faire valider votre progression par votre chargé de TP**
### 2.3 Récupération des éléments et transfert
On va tout d'abord coder la fonction `transfert_fifo` qui déplace les éléments de la liste de gauche dans celle de droite en inversant leur ordre. On pourra s'inspirer de `renverse_ajoute` du TP2 pour cette fonction.
> Coder la fonction `transfert_fifo` et la tester avec `assert` et les fifos exemples créées précédement.
On peut maintenant coder la fonction `pop_fifo` qui va récupérer un élément dans la fifo.
Dans cette fonction, on distinguera le cas d'une fifo avec au moins un élément à droite de celle avec la liste droite vide. Dans le deuxième cas, on utilisera `transfert_fifo` pour basculer les éléments de gauche à droite avant de prendre la tête de liste à droite.
Le résultat de `pop_fifo` sera une paire contenant d'une part la fifo sans l'élément retiré et d'autre part l'élément retiré. Comme une fifo peut être vide, il n'y a pas forcément d'élément à retirer. La valeur de ce dernier sera donc une option, et considérera par convention que retirer un élément d'une fifo vide produit une fifo vide (et `None` pour la valeur de l'élément retiré). Le type de `pop_fifo` sera donc `'a fifo -> ('a fifo * 'a option)`.
> Coder la fonction `pop_fifo` et la tester avec `assert` et les fifos exemples créées précédement.
<!-- Séparateur de citation -->
> Coder la fonction `pop_all_fifo` qui prend une fifo `f` et renvoie la liste contenant tous les éléments dans l'ordre de `f`, c'est à dire qu'on veut le premier élément retiré de `f` en tête de la liste résultat. Tester avec `assert` et les fifos exemples créées précédement.
**Faire valider votre progression par votre chargé de TP**
(**********************************************************************)
(* Arbres n-aires *)
(**********************************************************************)
(** Arbre avec un nombre quelconque de fils *)
type 'a arbre_n = Feuille of 'a | Noeud of 'a arbre_n list
let a1 = Feuille 1
let a2 = Feuille 2
let a3 = Noeud []
let a4 = Noeud [ a1 ]
let a5 = Noeud [ a1; a2 ]
let a6 = Noeud [ a1; a2; a3; a4; a5 ]
let a_vide_1 = Noeud []
let a_vide_2 = Noeud [ Noeud [] ]
(* Le type de ces arbres vide est 'a arbre_n. En effet, comme ces arbres ne
contiennent pas d'éléments ils peuvent être vus comme des arbresavec ce qu'on
veut comme type d'élément. *)
let rec hauteur (a : 'a arbre_n) : int =
match a with Feuille _ -> 1 | Noeud l -> hauteur_foret l + 1
and hauteur_foret (l : 'arbre_n list) : int =
match l with
| [] -> 0
| a :: l' -> max (hauteur a) (hauteur_foret l')
;;
assert (hauteur a1 = 1);;
assert (hauteur a3 = 1);;
assert (hauteur a4 = 2);;
assert (hauteur a5 = 2);;
assert (hauteur a6 = 3)
(**
Renvoie une liste contenant tous les éléments de l'arbre
@param a: l'arbre
@return la liste de ses éléments
*)
let list_of_arbre (a : 'a arbre_n) : 'a list =
let rec list_of_arbre_aux (a : 'a arbre_n) (acc : 'a list) : 'a list
=
match a with
| Feuille x -> x :: acc
| Noeud f -> list_of_foret f acc
and list_of_foret (f : 'a arbre_n list) (acc : 'a list) : 'a list =
match f with
| [] -> acc
| a :: f' -> list_of_arbre_aux a (list_of_foret f' acc)
in
list_of_arbre_aux a []
;;
assert (list_of_arbre a1 = [ 1 ]);;
assert (list_of_arbre a4 = [ 1 ]);;
assert (list_of_arbre a5 = [ 1; 2 ]);;
assert (list_of_arbre a6 = [ 1; 2; 1; 1; 2 ])
(**
[minimum arbre] est le plus grand élément de arbre si arbre en contient au moins 1.
@param arbre l'arbre dans lequel on cherche le minimum
@return None si l'arbre ne contient pas d'élément, ou sinon Some m avec m le plus petit élément de l'arbre
*)
let rec minimum (arbre : 'a arbre_n) : 'a option =
match arbre with
| Feuille x -> Some x
| Noeud la -> minimum_foret la
(**
[minimum_foret l] donne l'élément minimal que l'on peut trouver dans une forêt
@param l la forêt
@return None si la forêt ne contient pas d'élément ou sinon Some m où m est le plus petit élément de la forêt
*)
and minimum_foret (la : 'a arbre_n list) : 'a option =
match la with
| [] -> None
| a :: la' -> (
match minimum_foret la' with
| None -> minimum a
| Some n -> (
match minimum a with
| None -> Some n
| Some n' -> Some (min n n')))
;;
assert (minimum a1 = Some 1);;
assert (minimum a3 = None);;
assert (minimum a4 = Some 1);;
assert (minimum a5 = Some 1);;
assert (minimum a6 = Some 1)
(**
[reduce f a] renvoie:
- None si a ne contient aucun élément
- Some x si a contient un seul élément x
- Some x où x est le résultat de la combinaison des éléments de a en utilisant f
@param f la fonction de combinaison des éléments
@param a l'arbre qui contient les éléments
*)
let rec reduce (f : 'a -> 'a -> 'a) (arbre : 'a arbre_n) : 'a option =
match arbre with Feuille x -> Some x | Noeud l -> reduce_foret f l
(**
[reduce_foret f l] renvoie:
- None si l (en tant que forêt) ne contient aucun élément
- Some x si l contient un seul élément x
- Some x où x est le résultat de la combinaison des éléments de l en utilisant f
@param f la fonction de combinaison des éléments
@param la forêt qui contient les éléments
*)
and reduce_foret (f : 'a -> 'a -> 'a) (la : 'a arbre_n list) :
'a option =
match la with
| [] -> None
| a :: la' -> (
match reduce_foret f la' with
| None -> reduce f a
| Some n -> (
match reduce f a with
| None -> Some n
| Some n' -> Some (f n n')))
;;
assert (reduce min a1 = Some 1);;
assert (reduce min a3 = None);;
assert (reduce min a4 = Some 1);;
assert (reduce min a5 = Some 1);;
assert (reduce min a6 = Some 1);;
assert (reduce ( + ) a1 = Some 1);;
assert (reduce ( + ) a3 = None);;
assert (reduce ( + ) a5 = Some 3);;
assert (reduce ( + ) a6 = Some 7)
(**********************************************************************)
(* Files (FIFO) implémentées avec deux listes *)
(**********************************************************************)
type 'a fifo = Fifo of ('a list * 'a list)
(** File vide *)
let empty_fifo : 'a fifo = Fifo ([], [])
(**
[push_fifo e f] Ajoute e dans f
@param e l'élément a ajouter
@param f la fifo dans laquelle on veut ajouter l'élément
@return la fifo contenant les éléments de f puis e
*)
let push_fifo (e : 'a) (f : 'a fifo) : 'a fifo =
match f with Fifo (l1, l2) -> Fifo (e :: l1, l2)
let f1 = push_fifo 1 empty_fifo
let f2 = push_fifo 2 f1
let f3 = push_fifo 3 f2
let f4 = push_fifo 4 f3;;
assert (f1 = Fifo ([ 1 ], []));;
assert (f2 = Fifo ([ 2; 1 ], []));;
assert (f3 = Fifo ([ 3; 2; 1 ], []));;
assert (f4 = Fifo ([ 4; 3; 2; 1 ], []))
(**
[push_list_fifo l f] ajoute les éléments de l à la file f
@param l les éléments à ajouter
@param f la file dans laquelle ajouter les éléments
@return la file contenant les éléments de f puis les éléments de l
*)
let rec push_list_fifo (l : 'a list) (f : 'a fifo) : 'a fifo =
match l with
| [] -> f
| x :: l' -> push_list_fifo l' (push_fifo x f)
;;
assert (push_list_fifo [] empty_fifo = empty_fifo);;
assert (push_list_fifo [] f2 = f2);;
assert (push_list_fifo [ 1 ] empty_fifo = f1);;
assert (push_list_fifo [ 3; 4 ] f2 = f4);;
assert (push_list_fifo [ 1; 2; 3; 4 ] empty_fifo = f4)
(**
Fonction utilitaire transférant tous les éléments de la liste de gauche dans
celle de droite en en renversant l'ordre au passage.
*)
let rec transfert_fifo (f : 'a fifo) : 'a fifo =
match f with
| Fifo ([], l2) -> Fifo ([], l2)
| Fifo (x :: l1, l2) -> transfert_fifo (Fifo (l1, x :: l2))
;;
assert (transfert_fifo f4 = Fifo ([], [ 1; 2; 3; 4 ]));;
assert (transfert_fifo f1 = Fifo ([], [ 1 ]))
(**
[pop_fifo f] renvoie le premier élément de f s'il y en a un, ainsi que la file contenant le reste des éléments de f.
@param f la file dans laquelle on veut prendre un élément
@return (f',r) où
- f' est la file contenant les éléments de f sauf le premier
- r est Some x si f a pour premier élément x ou bien None si f est vide
*)
let pop_fifo (f : 'a fifo) : 'a fifo * 'a option =
match f with
| Fifo (l1, []) -> (
match transfert_fifo f with
| Fifo (_, []) -> (Fifo ([], []), None)
| Fifo (_, x :: l2') -> (Fifo ([], l2'), Some x))
| Fifo (l1, x :: l2') -> (Fifo (l1, l2'), Some x)
;;
assert (pop_fifo empty_fifo = (empty_fifo, None));;
assert (pop_fifo f1 = (empty_fifo, Some 1));;
assert (pop_fifo f2 = (Fifo ([], [ 2 ]), Some 1));;
assert (pop_fifo (fst (pop_fifo f2)) = (empty_fifo, Some 2))
(**
Renvoie tous les éléments de la file dans l'ordre de celle-ci
@param f la file dont on veut les éléments
@return une liste contenant les éléments de f dans l'ordre
*)
let rec pop_all_fifo (f : 'a fifo) : 'a list =
match pop_fifo f with
| _, None -> []
| f', Some x -> x :: pop_all_fifo f'
;;
assert (pop_all_fifo empty_fifo = []);;
assert (pop_all_fifo f1 = [ 1 ]);;
assert (pop_all_fifo f4 = [ 1; 2; 3; 4 ]);;
(* Un test mélangeant les opérations de push et de pop de la file *)
assert (
pop_all_fifo (push_list_fifo [ 3; 4 ] (fst (pop_fifo f2)))
= [ 2; 3; 4 ])
# TP5: Parcours génériques de structure
## 1. Manipulations de listes à l'aide de fonctions génériques
On considère les fonctions suivantes prédéfinies en OCaml, voir [la documentation du module `List`](https://v2.ocaml.org/api/List.html) :
- **`List.map: ('a -> 'b) -> 'a list -> 'b list`** Cette fonction transforme une liste en utilisant la fonction passée en paramètre pour transformer les éléments un par un. Ainsi `List.map f [x1; x2; x3; ... ]` renverra une liste égale à `[f x1; f x2; f x3; ... ]`.
- **`List.filter: ('a -> bool) -> 'a list -> 'a list`** Cette fonction transforme une liste en utilisant la fonction passée en paramètre conserver uniquement certains éléments. Ainsi `List.filter f [x1; x2; x3; ... ]` renverra une liste contenant exactement les `xi` pour lesquels `f xi` vaut `true`.
- **`List.for_all: ('a -> bool) -> 'a list -> bool`** Cette fonction indique si une fonction renvoie vrai pour tous les éléments d'une liste. Ainsi `List.for_all f [x1; x2; x3; ... ]` renvoie `true` si pour tous les `xi`, on a `f xi` vaut `true`.
## 1.1. Utilisation des fonctions sur les listes
> En utilisant `List.map`, coder les fonctions suivantes :
>
> - Une fonction `f1` qui prend une liste d'`int` et renvoie la liste de leur représentation sous forme de `string`.
> - Une fonction `f2` qui prend un `int` `x` et une liste d'`int` `l` et renvoie la liste transformée en multipliant chaque élément de `l` par `x`.
> - Une fonction `f3` qui prend deux `int` `x` et `y` et une liste d'`int` `l` et renvoie la liste (de `bool`) indiquant pour chacun des éléments de `l` s'il est bien compris entre `x` et `y`.
Bien tester vos fonctions avec `assert`.
> En utilisant `List.filter`, coder les fonctions suivantes :
>
> - Une fonction `f4` qui prend deux `int` `x` et `y` et une liste d'`int` `l` et renvoie la liste des éléments de `l` qui sont compris entre `x` et `y`.
> - Une fonction `f5` qui prend une liste de liste d'`int` et renvoie la liste sans les (sous-)listes vides.
Bien tester vos fonctions avec `assert`.
En OCaml, on peut voir un opérateur comme une fonction en le plaçant tout seul entre parenthèses. Ainsi `(!=)` est une fonction qui prend deux arguments et renvoie `true` s'ils sont différents. Ainsi `(!=) 1 2` renvoie `true`, alors que `(!=) 1 1` renvoie `false`.
> Recoder la fonction `f5` sans le mot clé `fun` en utilisant une application partielle de `(!=)`.
On peut bien sûr combiner `List.map` et `List.filter`.
> Coder une fonction `f6` qui prend une liste d'`int option` `l` et renvoie une liste d'`int` contenant les valeurs `int` contenues dans `l`. Par exemple, `f6 [ Some 3; None; Some 18; Some 42; None; Some 37; None]` vaudra `[3; 18; 42; 37]`.
## 1.2 Recodage de fonctions sur les listes
> Recoder `List.map`, `List.filter` et `List.for_all`.
**Faire valider votre progression par votre chargé de TP**
## 2. Arbre n-aires: recodage
Dans cet exercice, on reprend les fonctionnalités développées dans la section 1. du [TP4](tp4.md), mais en les recodant avec les fonctions déjà fournies avec OCaml.
Remarque : en OCaml, on peut écrire `fun x y -> ...` à la place de `fun x -> fun y -> ...`.
### 2.1. Recodage de quelques fonctions de base avec la bibliothèque standard OCaml
> Reprendre le type `'a arbre_n` du TP4, section 1.1
On pourra reprendre les tests écrits lors du TP4.
> Recoder la fonction `hauteur_arbre` sans utiliser `hauteur_foret`, mais en appelant directement `List.map` et `List.fold_left` pour extraire les hauteurs des arbres fils et trouver la hauteur maximale parmi celles-ci.
> Recoder `list_of_arbre_aux` en utilisant `List.fold_right` pour remplacer les appels à `list_of_foret`.
> Encapsuler `list_of_arbre_aux` avec un `let ... in ...` dans la définition de `list_of_arbre`.
> On écrira le `let` avant de prendre l'argument de `list_of_arbre`.
**Faire valider votre progression par votre chargé de TP**
### 2.2. Gestion d'option, fold et minimum
Pour gérer proprement le calcul du minimum, on va s'équiper d'un décorateur `lift_option_2` ayant le comportement suivant. Soit `f: 'a -> 'a -> 'a`. On suppose que `g = lift_option_2 f`. Alors `g` aura le type `'a option -> 'a -> 'a option`. Si le premier argument de `g` est `None`, alors `g` renvoie `Some x` si `x` est son second argument. Sinon `g` renvoie `Some (f y x)``y` est la valeur contenue dans son premier argument et `x` est son second argument.
> Définir `lift_option_2` en commençant par préciser son type. Tester avec `assert` en passant la fonction `min` à `lift_option_2`.
On veut à présent définir `fold_left_arbre` qui agrège une valeur via un accumulateur à la manière de `List.fold_left`, mais en parcourant un arbre et pas une liste.
> Définir le type, puis coder `fold_left_arbre`. On pourra utiliser intelligemment `List.fold_left` pour gérer le cas des `Noeud`.
> Tester en calculant la somme des éléments d'arbres bien choisis.
On peut remarquer que `reduce` du TP4 ressemble énormément à `fold_left_arbre`: les deux vont parcourir les éléments de l'arbre les combinant via un accumulateur. Elles diffèrent cependant sur les points suivants:
- `reduce` prend en argument une fonction de type `'a -> 'a -> 'a` alors que `fold_left_arbre` prend une fonction un peu plus générique de type `'b -> 'a -> 'b`.
- `reduce` renvoie forcément une `option` alors que `fold_left_arbre` renvoie uniquement une option si `'b` est lui-même une `option`.
> En utilisant intelligemment `fold_left_arbre`, recoder `reduce`.
> Recoder `list_of_arbre` en utilisant `fold_left_arbre`.
**Faire valider votre progression par votre chargé de TP**
(**********************************************************************)
(* 1 Manipulations de listes à l'aide de fonctions génériques *)
(**********************************************************************)
(**********************************************************************)
(* 1.1. Utilisation des fonctions sur les listes *)
(** Une fonction `f1` qui prend une liste d'`int` et renvoie la liste de leur représentation sous forme de `string`. *)
let f1 (l : int list) : string list = List.map (fun x -> string_of_int x) l
(* ou bien *)
let f1 = List.map string_of_int;;
assert (f1 [ 1; 2 ] = [ "1"; "2" ])
(** Une fonction `f2` qui prend un `int` `x` et une liste d'`int` `l` et renvoie la liste transformée en multipliant chaque élément de `l` par `x`. *)
let f2 (x : int) (l : int list) : int list = List.map (fun y -> x * y) l
(* ou bien *)
let f2 (x : int) : int list -> int list = List.map (fun e -> x * e);;
assert (f2 3 [ 1; 5; 3 ] = [ 3; 15; 9 ])
(** Une fonction `f3` qui prend deux `int` `x` et `y` et une liste d'`int` `l` et renvoie la liste (de `bool`) indiquant pour chacun des éléments de `l` s'il est bien compris entre `x` et `y`. *)
let f3 (x : int) (y : int) (l : int list) : bool list =
List.map (fun e -> x <= e && e <= y) l
(* ou bien *)
let f3 (x : int) (y : int) : int list -> bool list =
List.map (fun e -> x <= e && e <= y)
;;
assert (f3 3 7 [ 1; 4; 2; 12 ] = [ false; true; false; false ])
(** Une fonction `f4` qui prend deux `int` `x` et `y` et une liste d'`int` `l` et renvoie la liste des éléments de `l` qui sont compris entre `x` et `y`. *)
let f4 (x : int) (y : int) (l : int list) : int list =
List.filter (fun e -> x <= e && e <= y) l
(* ou bien *)
let f4 (x : int) (y : int) : int list -> int list =
List.filter (fun e -> x <= e && e <= y)
;;
assert (f4 3 7 [ 1; 4; 2; 12 ] = [ 4 ])
(** Une fonction `f5` qui prend une liste de liste d'`int` et renvoie la liste sans les (sous-)listes vides. *)
let f5 (l : int list list) : int list list = List.filter (fun l' -> l' != []) l
(* variante avec (=) *)
let f5 : int list list -> int list list = List.filter (( != ) []);;
assert (f5 [ []; [ 2; 3 ]; []; [ 1 ] ] = [ [ 2; 3 ]; [ 1 ] ])
(** Coder une fonction `f6` qui prend une liste d'`int option` `l` et renvoie une liste d'`int` contenant les valeurs `int` contenues dans `l`. Par exemple, `f6 [ Some 3; None; Some 18; Some 42; None; Some 37; None]` vaudra `[3; 18; 42; 37]`. *)
let f6 (l : int option list) : int list =
let l1 = List.filter (fun o -> o != None) l in
List.map
(fun o ->
match o with
| None -> 0 (* n'arrive pas à cause du filter précédent *)
| Some x -> x)
l1
(* ou bien *)
let f6 (l : int option list) : int list =
l
|> List.filter (( != ) None)
|> List.map (fun o ->
match o with
| None -> 0 (* n'arrive pas à cause du filter précédent *)
| Some x -> x)
(* ou encore en utilisant Option.get *)
let f6 (l : int option list) : int list =
l |> List.filter (( != ) None) |> List.map Option.get
;;
assert (
f6 [ Some 3; None; Some 18; Some 42; None; Some 37; None ] = [ 3; 18; 42; 37 ])
(**********************************************************************)
(* 1.2. Recodage de fonctions sur les listes *)
let rec map (f : 'a -> 'b) (l : 'a list) : 'b list =
match l with [] -> [] | x :: l' -> f x :: map f l'
let rec filter (f : 'a -> bool) (l : 'a list) : 'a list =
match l with
| [] -> []
| x :: l' -> if f x then x :: filter f l' else filter f l'
let rec for_all (f : 'a -> bool) (l : 'a list) : bool =
match l with [] -> true | x :: l' -> f x && for_all f l'
(**********************************************************************)
(* 2 Arbre n-aires: recodage *)
(**********************************************************************)
(**********************************************************************)
(* 2.1. Recodage de quelques fonctions de base avec la bibliothèque
standard OCaml *)
type 'a arbre_n = Feuille of 'a | Noeud of 'a arbre_n list
(**
Calcule la hauteur d'un arbre.
@param a l'arbre dont on veut calculer la hauteur
@return la hauteur de l'arbre
*)
let rec hauteur_arbre (a : 'a arbre_n) : int =
match a with
| Feuille _ -> 1
| Noeud foret ->
let hauteurs = List.map hauteur_arbre foret in
let h_max = List.fold_left max 0 hauteurs in
h_max + 1
(**
Extrait les éléments d'un arbre dans une liste
@param l'arbre dont on veut les éléments
@return la liste des éléments de l'arbre obtenue par un parcours en profondeur.
*)
let list_of_arbre : 'a arbre_n -> 'a list =
(*
Ajoute les éléments de l'arbre à la liste.
@param a l'arbre dont on veut extraires les éléments
@param acc la liste à laquelle on veut ajouter les éléments.
*)
let rec list_of_arbre_aux (a : 'a arbre_n) (acc : 'a list) : 'a list =
match a with
| Feuille x -> x :: acc
| Noeud foret ->
List.fold_left (fun acc2 fils -> list_of_arbre_aux fils acc2) acc foret
in
fun a -> list_of_arbre_aux a []
(**********************************************************************)
(* 2.2. Gestion d'option, fold et minimum *)
(**
[lift_option_2 f] choisi son deuxième argument si son premier argument est None.
Sinon utilise f pour produire une valeur avec ses deux arguments.
@param f la fonction utilisée pour combiner les arguments
@param x une option
@param y la valeur à combiner à la valeur de x ou à prendre si x est None
@return Some de la combinaison des valeurs de x et y, ou bien y si x est None
*)
let lift_option_2 (f : 'a -> 'a -> 'a) : 'a option -> 'a -> 'a option =
fun x y -> match x with None -> Some y | Some x' -> Some (f x' y)
(**
aggrège une valeur en utilisant un accumulateur et une fonction appelée pour
mettre à jour l'accumulateur en fonction de l'élément de l'arbre rencontrée.
Appelle la fonction en utilisant les un après les autres tous les éléments de
l'accumulateur.
@param f la fonction de mise à jour de l'accumulateur
@param init la valeur de départ de l'accumulateur
@param a l'arbre à parcourir
@return la valeur de l'accumulateur résultant des mises à jour
successives par les appels à f sur les éléments de a.
*)
let rec fold_left_arbre (f : 'b -> 'a -> 'b) (init : 'b) (a : 'a arbre_n) : 'b =
match a with
| Feuille x -> f init x
| Noeud foret -> List.fold_left (fold_left_arbre f) init foret
(* Pour le dernier cas on aurait pu écrire
List.fold_left (fun acc fils -> fold_left_arbre f acc fils) init foret
mais c'est plus long
*)
(**
Aggrège une valeur en utilisant une fonction de combinaison de valeurs
@param f la fonction de combinaison de valeurs
@param a l'arbre dont on veut combiner les valeurs
@return Some du résultat de la combinaisons des valeurs de a par f,
ou None si a n'a pas d'élément
*)
let reduce (f : 'a -> 'a -> 'a) (a : 'a arbre_n) : 'a option =
fold_left_arbre (lift_option_2 f) None a
(**
Extrait les éléments d'un arbre dans une liste
@param l'arbre dont on veut les éléments
@return la liste des éléments de l'arbre obtenue par un parcours en profondeur.
*)
let list_of_arbre' : 'a arbre_n -> 'a list =
fold_left_arbre (fun l e -> e :: l) []
# TP6 : Parcours et transformations de structures
**Attention** dans les sujets qui suivent, il est demandé de ne **pas regarder le corrigé** pour faire les exercices.
Lire un corrigé et (re)faire l'exercice seul (sans regarder une correction) sont deux choses très différentes.
**Votre chargé de TP validera votre progression en fin de séance.**
## 1. Manipulations de listes
Coder l'exercice 1 du [TD5](../td/lifpf-td5-enonce.pdf), en faisant en priorité ce qui n'a pas pu être fait en séance de TD.
## 2. Parcours et transformation d'arbres de recherche
Coder l'exercice 3 du [TD4](../td/lifpf-td4-enonce.pdf)
## 3. Calculatrice
Terminer complètement le codage de la calculatrice du [TP3](tp3.md).
# TP7 - Application et configuration par fonctions
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_.
## 1. Application et modules
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.
Les différentes parties de l'application intiale sont les suivantes:
- 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.
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"):
```mermaid
graph TD
A[Association] --> B[Usine]
B --> C[LutinsApp]
C --> D[code directement dans usinelutins.ml]
```
### 2. Premiers modules
Créer un fichier `usinelutins.ml`. À titre d'exemple de module on donne le code suivant, à placer au début du fichier `usinelutins.ml`:
```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
```
Créer un module `Usine` à la suite du module `Association`. Dans ce module, créer:
- 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`
> Ajouter des `assert` pour tester `string_of_jour` et `jour_opt_of_string`.
### 3. Utilisation du module `Usine`
On souhaite maintenant utiliser ce module. Pour cela il faut:
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`.
Ajouter le code suivant à la fin de `usinelutins.ml`:
```ocaml
let usage () = print_endline "\nUsage: ocaml usinelutins.ml jour"
let run args =
match args with
| _pgm :: jour_s :: _ -> print_endline "Bonjour, nous sommes un certain jour"
| _ -> usage ()
let _ = run (Array.to_list Sys.argv)
```
> 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`.
> 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.
### 4. Enrichir le module `Usine`
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`.
Définir ce type le module `Usine`:
```ocaml
type configuration = (string -> string option) * (string -> string -> int option);;
```
Ajouter les fonctions suivantes dans le module `Usine`:
- `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 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.
### 5. Codage de l'application de gestion des jouets - début
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.
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.
Créer également une variable globale contenant la liste des noms des lutins.
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.
Enfin créer un dernier module `LutinsApp` qui contiendra:
- une fonction `affiche_jouets: (string * int) list -> string` qui calculera une chaîne d'affichage de la sortie de la fonction `calcule_jouets_config`;
Modifier la fonction `run: string list -> unit` pour affichera (via `print_endline`) les jouets produits durant le jour passé en argument.
Avec l'implémentation actuelle, il est possible que certains jouets s'affichent plusieurs fois, cela sera corrigé avec la section suivante.
### 6. Optimisation du module Association
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.
# Programmation fonctionnelle: TP8
## Séances précédentes
Terminer les TPs précédents.
## Calculatrice programmable
On prend comme point de départ la calculatrice du [TP3](tp3.md).
On veut maintenant ajouter dans la calculatrice la possibilité de coder des fonctions.
On ajoute deux constructeurs aux expressions:
- `Fun` pour représenter la création d'une fonction (ce qui correspond à un `fun x -> exp` en en OCaml et à λx.expr en λ-calcul);
- Ce constructeur aura deux données: le nom de la variable et l'expression correspondant au corps de la fonction.
- `App` pour représenter l'application d'une fonction
- Ce constructeur aura également deux données: l'expression qui donnera la fonction à appeler et l'expression qui donnera l'argument à passer à cette fonction.
On modifie également le type `resultat` car un résultat peut maintenant être une fonction:
- `Ok` devient `Int`
- On ajoute un constructeur `Fermeture` qui représente une fermeture, c'est à dire une fonction et sont environnement d'exécution
- Ce constructeur aura 3 données: le nom du paramètre, l'expression du corps de la fonction et enfin l'environnement de la fonction (représenté par une liste de paires `string * resultat`)
Il reste à mettre à jour la fonction d'évaluation des expressions.
Les environnements sont maintenant représentés, comme dans les fermetures, par les listes de paires `string * resultat`.
Il faut maintenant coder l'évaluation des nouvelles expressions.
L'évaluation d'un `Fun` consiste à créer la fermeture correspondante.
La variable et l'expression sont déjà fournies dans les données du `Fun`.
Pour l'environnement, on pourra dans un premier temps prendre tout l'environnement courant.
L'évaluation d'un `App` consiste à:
1. Évaluer l'expression qui calcule la fonction. Celle-ci doit renvoyer une fermeture (voir les erreurs plus loin)
2. Évaluer l'expression de l'argument.
3. Créer un nouvel environnement constitué de l'environnement de la fermeture auquel on ajoute la paire (x,v) où x est le nom du paramètre de la fermeture et v est la valeur calculée pour l'argument.
4. Évaluer le corps de la fonction dans ce nouvel environnement
**Erreurs:** si un appel récursif à une évaluation produit une erreur, on renverra cette erreur.
Si l'évaluation d'une expression calculant une fonction (_c.-à-d._ la première expression d'un `App`) produit un `Int`, on renverra une nouvelle erreur `PasUneFonction` (à ajouter au type `eval_err`).
Si une `Fermeture` est passée en argument à un opérateur binaire, renvoyer une nouvelle erreur `PasUnInt`.
**Pour aller plus loin:** maintenant que l'on dispose d'une construction pour appliquer des fonctions, les opérateurs binaires pourraient être recodés comme des des fonctions _natives_.
Une fonction native peut être représentée par une fonction OCaml qui prend un résultat en argument et renvoie un nouveau résultat.
Si on veut coder une fonction à deux arguments, il suffit de la curryfier, c'est-à-dire que le `resultat` renvoyé par la fonction OCaml associée est à nouveau une fonction native qui prendra le deuxième argument qui renverra le résultat final.
Ajouter un constructeur `Native` dans le type `resultat` et modifier la fonction d'évaluation en traitant le cas de `Native` similairement au cas `Fermeture`, sauf pour `App`.
Pour ce dernier cas, on évaluera simplement l'argument puis on appelera la fonction OCaml donnée dans `Native` pour obtenir le résultat.
Enfin on écrira des fonctions natives pour les 4 opérateurs binaires et on créera un environnement initial associant à chaque symbole binaire sa fonction native.
Dans les tests, on remplacera l'utilisation de Binop par des `App` avec comme fonction la variable ayant le symbole binaire concerné, par exemple: `(App(App(Var "+", Cst 2), Cst 3))` permet de représenter `2 + 3`.