Programmation Avancée

TP7: itérateurs persistants

On peut représenter une collection comme une fonction d'itération, par exemple fun f -> List.iter f l permet en principe de tout faire sur la liste l bien qu'on n'ait pas directement accès à elle. En utilisant des exceptions dans la fonction à itérer, on peut éviter de parcourir toute la collection. Par contre, on n'a aucun moyen de reprendre l'itération à partir d'une position arbitraire, ce qui est nécessaire si l'on veut faire des recherches avec backtracking; en d'autres termes, callcc nous manque.

Pour combler ce manque, on se propose de passer des fonctions d'itération à une notion d'itérateur persistant:

module type ITER = sig
  type t
  type elt
  type init
  val init : init -> t
  val next : t -> (elt*t) option
end

La spécification est:

On demande que les itérateurs soient persistants dans le sens où itérateur doit pouvoir être réutilisé pour reprendre une énumération; en d'autres termes un itérateur de change pas quand on appelle sa fonction next.

Question 1

Construire un module L de signature ITER qui énumère simplement les éléments d'une liste d'entiers, dans l'ordre d'apparition dans la liste.

Question 2

Coder une fonction qui prend en argument un itérateur de type L.t et renvoie le nombre de sous-séquences dont la somme des éléments fasse 100. (On supposera que tous les éléments rencontrés sont des entiers strictement positifs.)

Question 3

On se donne maintenant un type d'expressions arithmétiques:

type op = char * (int -> int -> int)
let plus = '+',(+)
let times = '*',( * )
let minus = '-',(-)
type expr =
  | I of int
  | Op of op * expr * expr
let rec eval = function
  | I i -> i
  | Op (o,a,b) -> (snd o) (eval a) (eval b)

Implémentez une fonction iter_subexpr : expr -> (expr -> unit) -> unit qui itère une fonction sur toutes les sous-expressions d'une expression donnée. On s'interdira d'utiliser les exceptions et toute structure de données mutable.

Question 4

Transformez la fonction précédente en CPS. Vous obtenez iter_subexpr_k : expr -> (expr -> (unit -> 'a) -> 'a) -> (unit -> 'a) -> 'a. Définir une implémentation de ITER correspondant aux itérateurs sur sous-expressions, en posant elt = expr et t = (unit -> (elt*t) option) et enfin init e = iter_subexpr_k e f k avec f et k bien choisis.

Attention: Le type t proposé ici est trop immédiatemment récursif, et OCaml ne l'acceptera que si vous lui passez l'option -rectypes. L'alternative est de poser type t = { k = unit -> (elt*t) option }, ou encore type t = T of unit -> (elt*t) option, mais je déconseille ces complications.

Question 5

Dans le module précédemment obtenu, défonctionalisez le type t des itérateurs (qui correspond à un cas particulier du type des continuations de votre fonction d'itération en CPS).

On pourra simplifier le code (avant ou après défonctionalisation) en remarquant qu'un des arguments de votre fonction d'itération prend toujours la même valeur.

Vous obtiendrez ainsi une structure d'itérateur persistant de premier ordre. Peut être aurez-vous aussi re-découvert une implémentation usuelle de cet algorithme.

Adapter le code précédent pour chercher toutes les listes de sous-expressions d'une expression donnée, dont la somme des évaluations fasse 100.

Ci-dessous, un peu de code utile pour lancer un test où l'on doit trouver 6 solutions:

let t1 =
  Op (plus,
      Op (minus,
          Op (plus, Op (times, I 2, I 12), I 7),
          I 42),
      Op (plus, I 11, I 5))

let rec pp_expr ch = function
  | I i -> Format.fprintf ch "%d" i
  | Op (o,a,b) -> Format.fprintf ch "(%a%c%a)" pp_expr a (fst o) pp_expr b

let rec pp_list pp ch = function
  | [] -> ()
  | [a] -> pp ch a
  | a::b::more -> Format.printf "%a, %a" pp a (pp_list pp) (b::more)

let () = Format.printf "Exemple:\n %a\n" (pp_list pp_expr) [t1]
let () = assert (eval t1 = 5)

Question 6

Facile. Définir des itérateurs persistants de premier ordre pour les entiers situés aux feuilles d'une expression arithmétique. Vous pouvez attaquer le problème directement, ou bien passer par une fonction d'itération, une CPS et une défonctionalisation.

Moyen. De même pour les listes extraites d'une liste donnée. (Les listes extraites sont obtenues en enlevant des éléments à des positions arbitraires, par exemple [] et [1;2] sont extraites de [1;3;2;4].)

Difficile. De même pour les expressions extraites d'une expression donnée. (On définit une expression extraite comme étant obtenue par suppression de noeuds dans l'arbre syntaxique original. (Par exemple, à partir de (1+2)*(3+(4*5)) on peut extraire les expressions 1*4, (1+2)*(3+5), etc.)

Difficile. De même pour les sous-expressions (sous-arbres) d'une expression arithmétique, mais selon un parcours en largeur préfixe. (Par exemple, (1+2)+3 doit donner l'expression elle même, suivie de 1+2, 3, 1 et enfin 2.)