Programmation Fonctionnelle, Correction de l'examen

Version & licenses
Creative Commons License

Programmation Fonctionnelle, Correction de l'examen

Guyslain Naves

Exercice 1 : Parcoursup

  1. La partie compliquée concerne la représentation des notes. Puisque le nombre de matières est grand, et susceptible de changer d'année en année, on se contente de les identifier par une chaîne de caractère plutôt qu'un type ad-hoc. Par contre chaque candidat passe un petit nombre de matières, il n'est donc pas utile d'utiliser une structure de données compliquée pour mémoriser toutes les notes, une liste fait l'affaire.

    1. type date =
    2. { day : int;
    3. month : int;
    4. year : int
    5. }

    6. type subject = string
    7. type mark = float

    8. type candidate =
    9. { name : string;
    10. birthday : date;
    11. marks : (subject * mark) list
    12. }
  2. Pour construire une structure de dictionnaire efficace, on utilise le foncteur Map.Make. Il prend pour argument une module contenant un type, le type des clés (ici les candidats), et une fonction de comparaison des clés.

    1. module OrderedCandidates =
    2. struct
    3. type t = candidate
    4. let compare cand1 cand2 = String.compare cand1.name cand2.name
    5. end
  3. On veut bien sûr éviter de réécrire un algorithme de tri, donc utiliser List.sort. Il faut donc construire une fonction de comparaison. Celle donnée en argument à notre sort ne compare pas les éléments de la liste, mais on peut transformer les éléments de la liste vers le type comparable. Un piège est de vouloir faire List.map by, car alors on perd les éléments. Ce qu'il faut c'est modifier la fonction de comparaison.

    1. let sort ~by:criterion ~cmp list =
    2. let compare_by_criterion elt1 elt2 =
    3. cmp (criterion elt1) (criterion elt2)
    4. in
    5. List.sort compare_by_criterion list
  4. Il faut récupérer les notes des trois matières, les agréger, puis comparer et trier les étudiants avec. Cela suggère les fonctions suivantes.

    1. let find_subject_mark subject candidate =
    2. List.Assoc.find_opt subject candidate.marks
    3. |> Option.value ~default:0


    4. let grade candidate =
    5. 3 * find_subject_mark "mathématiques" candidate
    6. + 2 * find_subject_mark "physique" candidate
    7. + 1 * find_subject_mark "français" candidate
    8. (* with more subjects we would factorize this as :
    9. coefs_subjects_list
    10. |> List.map (fun (coef,subject) -> coef * find_subject_mark subject candidate)
    11. |> List.fold_left (+) 0
    12. *)


    13. let sort_candidates candidates =
    14. sort
    15. ~by:grade
    16. ~cmp:Int.compare (* or (fun a b -> a - b) *)
    17. candidates

Exercice 2 : File de priorité.

  1. On utilise un type inductif pour représenter l'arbre binaire. Chaque nœud contient trois informations d'après l'énoncé : la priorité, la valeur et la taille du sous-arbre. Pour clarifier on utilise un type pour les nœuds.

    1. type 'elt node =
    2. { priority : priority;
    3. content : 'elt;
    4. size : int
    5. }

    6. type 'elt t =
    7. | Empty
    8. | Node of ('elt t * 'elt node * 'elt t)
  2. La taille est immédiatement disponible dans le nœud.

    1. let size = function
    2. | Empty -> 0
    3. | Node (left,node,right) -> node.size
  3. On doit récupérer les éléments dans l'ordre pour les mettre dans la liste. Le plus simple est donc d'utiliser les fonctions sur les files de priorité permettant d'extraire l'élément minimum. Par exemple, observe nous permet de procéder par filtrage par motif.

    1. let rec to_list queue =
    2. match observe queue with
    3. | None -> []
    4. | Some (min_prio, min_elt, tail) ->
    5. min_elt :: to_list tail
  4. fold prend en plus une fonction et un état à retourner lorsque la file de priorité est vide. On ajoute donc ces deux arguments et on retourne l'état initial dans le premier cas. Pour le deuxième cas, on garde l'appel récursif, on remplace l'opération de construction de liste par l'appel à la fonction f et cela donne :

    1. let rec fold f state queue = (* incorrect *)
    2. match observe queue with
    3. | None -> state
    4. | Some (min_prio, min_elt, tail) ->
    5. f min_elt (foldf state tail)

    Ce n'est pas encore parfait, l'ordre des arguments de f n'est pas bon, et on applique f à l'élément minimum en fin de calcul (on veut le faire au début). On corrige donc :

    1. let rec fold f state queue =
    2. match observe queue with
    3. | None -> state
    4. | Some (min_prio, min_elt, tail) ->
    5. fold f (f state min_elt) tail

    On obtient une fonction très proche de celle définissant List.fold_left. Pour obtenir to_list, on redonne la fonction d'insertion en tête de liste (attention (::) n'est pas une fonction, on ne peut le passer en argument).

    1. let to_list queue =
    2. fold (fun list elt -> elt :: list) [] queue
  5. On peut par exemple tester que le nombre d'éléments varie correctement pour pop, push et merge.

    1. let pop_decrease_size_by_one queue =
    2. let open MyPriorityQueue in
    3. if is_empty queue then
    4. size (pop queue) = 0
    5. else
    6. size (pop queue) = size queue

    7. let push_increase_size_by_one priority elt queue =
    8. let open MyPriorityQueue in
    9. size (push priority elt queue) = size queue + 1

    10. let merge_size_is_sum_of_sizes queue1 queue2 =
    11. let open MyPriorityQueue in
    12. size (merge queue1 queue2) = size queue1 + size queue2

Exercice 3 : APB.

  1. Pour générer un booléen vrai avec probabilité $p$, il suffit de tirer un flottant entre $0$ et $1$ uniformément, et tester s'il est plus petit que $p$. float 1. est une générateur de flottants (et non un flottant). Il faut utiliser map pour modifier les valeurs générées pour en faire des booléens.

    1. let true_with_proba p =
    2. let open Random in
    3. float 1. >|= fun x ->
    4. x < p
  2. Même principe pour cette question.

    1. let choose_one list =
    2. let open Random in
    3. int (List.length list) >|= fun index ->
    4. List.nth index list
  3. Pour filtrer les éléments d'une liste par un test aléatoire, le plus simple est de procéder récursivement : on décide si on garde la tête, puis ce qu'on garde de la queue, et on retourne le résultat. L'appel récursif produit un générateur, on va donc devoir utiliser bind pour chaîner les décisions successives.

    1. let rec filter ~p =
    2. let open Random in
    3. function
    4. | [] -> return []
    5. | head::tail ->
    6. true_with_proba ~p >>= fun keeps_head ->
    7. filter ~p tail >>= fun filtered_tail ->
    8. if keeps_head then return (head :: filtered_tail)
    9. else return filtered_tail
  4. Pour ajuster le nombre d'éléments choisis, il faut connaitre les éléments sélectionnés et les éléments rejetés, ce qui demande de modifier filter.

    1. let rec bipartition ~p =
    2. let open Random in
    3. function
    4. | [] -> return ([], [])
    5. | head::tail ->
    6. true_with_proba ~p >>= fun choose_left ->
    7. bipartition ~p tail >>= fun (left,right) ->
    8. if choose_left then return (head :: left, right)
    9. else return (left, head :: right)

    On peut ensuite implémenter l'algorithme proposé :

    1. let rec choose_k ~k list =
    2. if k = 0 then return []
    3. else
    4. let proba = float k /. float (List.length list) in (* Divide_by_zero? *)
    5. let open Random in
    6. bipartition ~p:proba >>= fun (selected, rejected) ->
    7. adjust k selected rejected

    8. and adjust k selected rejected =
    9. let open Random in
    10. let len = List.length selected in
    11. if len = k then return selected
    12. else if len > k then choose_k ~k selected
    13. else
    14. choose_k ~k:(k - len) rejected >|= fun completion ->
    15. List.rev_append completion selected
  5. On réutilise bipartition pour couper la liste.

    1. let rec shuffle =
    2. let open Random in
    3. function
    4. | [] -> return []
    5. | [single] -> return [single]
    6. | list ->
    7. bipartition ~p:0.5 list >>= fun (left, right) ->
    8. shuffle left >>= fun fetl ->
    9. shuffle right >>= fun gtrih ->
    10. return (List.rev_append fetl gtrih)

    11. let choose_k' ~k list =
    12. shuffle list >|=
    13. List.take k
  6. On procède par étape sur chaque élément de la liste, on peut donc envisager une récursion. Il nous faut un argument de plus pour contenir la file de priorité avec les éléments choisis pour l'instant.

    1. let push_and_check_size k priority elt queue =
    2. let new_queue = MyPriorityQueue.push priority elt queue in
    3. if MyPriorityQueue.size new_queue > k then
    4. MyPriorityqueue.pop new_queue
    5. else
    6. new_queue

    7. let rec choose_k''_with_queue ~k queue =
    8. let open Random in
    9. function
    10. | [] -> return (MyPriorityQueue.to_list queue)
    11. | head::tail ->
    12. bits >>= fun priority ->
    13. choose_k''_with_queue k (push_and_check_size k priority head queue) tail

    On peut encore améliorer cette algorithme est remarquant que la probabilité que la nouvelle priorité soit plus grande que celles des priorités des éléments encore dans la file de priorité est $\frac{k}{n+1}$. Ainsi, il n'est pas nécessaire de calculer les priorités, mais seulement de garder une collection des $k$ éléments choisis. L'algorithme qu'on obtient alors est appelé reservoir sampling.

    1. let remove_one list =
    2. choose_one list >|= fun rejected ->
    3. List.remove rejected list

    4. let remove_one_if_full k list =
    5. if List.length list < k then return list
    6. else remove_one list

    7. let try_adding_to_reservoir k n elt reservoir =
    8. let proba = float k /. float (n + 1) in
    9. let open Random in
    10. true_with_proba ~p:proba >>= fun takes_elt ->
    11. if takes_elt then
    12. remove_one_if_full reservoir >|= fun non_full_reservoir ->
    13. elt :: non_full_reservoir
    14. else
    15. return reservoir

    16. let rec reservoir_sampling reservoir k n =
    17. let open Random in
    18. function
    19. | [] -> return reservoir
    20. | head::tail ->
    21. try_adding_to_reservoir k n head reservoir >>= fun new_reservoir ->
    22. reservoir_sampling new_reservoir k (n+1) tail