Skip to main content Informatique en prépa - Lycée Malherbe 

0- Révision OCaml : fractales

Ce TP sert à faire un tour du OCaml au programme en étudiant des fractales.

0. Dessiner sur le site

Le site fournit une fonction pour dessiner :

segment : float * float -> float * float -> unit

segment (x1, y1) (x2, y2) trace le segment qui relie le point au point .

Un petit exemple :

OCaml

Loading editor...

type point = float * float;;

segment (0., 0.) (1., 0.);
segment (1., 0.) (0.5, 0.8);
segment (0.5, 0.8) (0., 0.)

À noter que le (0., 0.) est en bas à gauche de la zone de dessin.

I. Flottants et n-uplets

Rappel : les flottants

En OCaml, les entiers (int) et les flottants (float) ne se mélangent pas :

  • les constantes flottantes s'écrivent avec un point : 1., 0.5, 3.14 (et 1 est un entier)
  • les opérations sur les flottants ont un point : +., -., *., /.
  • pour convertir : float_of_int 3 vaut 3., int_of_float 3.7 vaut 3 ;
  • fonctions utiles : sqrt, cos, sin (angles en radians), et la constante Float.pi.
let x = 2. *. Float.pi /. 3.   (* et non 2 * Float.pi / 3 *)
Rappel : les n-uplets

Un point est un couple de flottants. Pour récupérer ses composantes : on peut le déstructurer directement, dans un let ou dans les arguments d'une fonction.

let p = (1., 2.);;
let (x, y) = p in x +. y;;          (* vaut 3. *)
let scalaire (x, y) (z, w) = x *. z +. y *. w;;  (* float * float -> float * float -> float *)
Question 1

Écrivez une fonction milieu : point -> point -> point qui renvoie le milieu d'un segment (représenté par ses deux extrémités).

OCaml

Loading editor...

let milieu ((x1, y1) : point) ((x2, y2) : point) : point =
  failwith "à compléter";;

milieu (0., 0.) (2., 4.) (*doit afficher (1., 2.)*)
Question 2

Écrivez une fonction distance : point -> point -> float qui renvoie la distance séparant deux points.

OCaml

Loading editor...

let distance ((x1, y1) : point) ((x2, y2) : point) : float =
  failwith "à compléter";;

distance (0., 0.) (3., 4.)
Rappel :

unit et la séquence segment ne renvoie rien d'intéressant : elle a un effet (dessiner). Son résultat est (), l'unique valeur du type unit.

Pour faire plusieurs choses à la suite, on utilise la séquence e1; e2 : on évalue e1 (qui doit être de type unit), puis e2. La séquence e1; e2 prend la valeur de son dernier élément, ici e2. Attention, ; sépare deux expressions : pas de ; après la dernière.

let deux_segments () =
  segment (0., 0.) (1., 0.);
  segment (1., 0.) (1., 1.)
Question 3

Écrivez une fonction carre : point -> float -> unit telle que carre (x, y) c dessine le carré de côté c dont le coin en bas à gauche est le point (x, y).

OCaml

Loading editor...

let carre ((x, y) : point) (c : float) : unit =
  failwith "à compléter";;

carre (0., 0.) 1.;
carre (0.5, 0.5) 1.

Pour dessiner des fractales, il sera bien plus pratique de raisonner avec des angles : on part d'un point, et on avance d'une certaine longueur dans une certaine direction.

Question 4

Écrivez une fonction avance : point -> float -> float -> point telle que avance p l a renvoie le point obtenu en partant de p et en avançant d'une longueur l dans la direction d'angle a (en radians, mesuré depuis l'axe des abscisses et dans le sens direct).

Autrement dit, si , on renvoie .

OCaml

Loading editor...

let avance ((x, y) : point) (l : float) (a : float) : point =
  failwith "à compléter";;

avance (1., 1.) 2. (Float.pi /. 2.)
Question 5

En utilisant avance, écrivez une fonction hexagone : point -> float -> unit qui dessine un hexagone régulier de côté l partant du point p. Pour cette question, écrivez simplement les 6 segments à la main : on fera mieux juste après.

Utilisez des let ... in pour nommer les sommets successifs : let p1 = avance p l 0. in let p2 = avance p1 l (Float.pi /. 3.) in ...

OCaml

Loading editor...

let hexagone (p : point) (l : float) : unit =
  failwith "à compléter";;

hexagone (0., 0.) 1.

II. Des fonctions récursives qui dessinent

Rappel : fonctions récursives

Une fonction récursive se déclare avec let rec. Il faut toujours :

  • un (ou des) cas de base, qui s'arrête sans appel récursif (sans quoi on boucle à l'infini)
  • des appels récursifs sur des arguments « plus petits », qui se rapprochent du cas de base.

Quand une fonction ne fait que dessiner, le cas de base ne fait rien : il renvoie (). Un if sans else est autorisé quand la branche then est de type unit (le else sous-entendu renvoie ()). Pour mettre une séquence dans un then, on l'entoure de parenthèses (ou de begin ... end) :

let rec compte_a_rebours n =
  if n >= 0 then begin
    print_int n;
    print_string " ";
    compte_a_rebours (n - 1)
  end

Sans le begin ... end, seul le print_int n serait dans le if : tout le reste serait exécuté quoi qu'il arrive... et la fonction ne s'arrêterait jamais.

Question 6

  1. Écrivez une fonction récursive cotes : int -> float -> point -> float -> float -> unit telle que cotes k t p l a dessine k segments de longueur l à la suite : le premier part de p dans la direction a, chaque segment suivant part de la fin du précédent, et on tourne de t entre deux segments.
  2. En déduire une fonction polygone_regulier : int -> point -> float -> unit telle que polygone_regulier n p l dessine un polygone régulier à n côtés de longueur l, partant de p. Dessinez un triangle, un carré, un hexagone et un polygone à 50 côtés (les appels pour ces tests vous sont donnés à la fin du bloc).

Le cas de base est k = 0 : il n'y a rien à dessiner. Sinon, on calcule l'extrémité q du premier segment avec avance, on le trace, et il reste k - 1 segments à tracer à partir de q, dans une certaine direction.

OCaml

Loading editor...

let rec cotes (k : int) (t : float) (p : point) (l : float) (a : float) : unit =
  failwith "à compléter";;

let polygone_regulier (n : int) (p : point) (l : float) : unit =
  failwith "à compléter";;

polygone_regulier 3 (0., 0.) 1.;
polygone_regulier 4 (2., 0.) 1.;
polygone_regulier 6 (4., 0.) 1.;
polygone_regulier 50 (7., 0.) 0.15
Question 7

Écrivez une fonction spirale : int -> point -> float -> float -> unit telle que spirale n p l a dessine n segments : le premier part de p, a la longueur l et la direction a, et chaque segment suivant part de la fin du précédent, est 5 % plus court et tourne de (à gauche). Testez avec spirale 40 (0., 0.) 1. 0. puis changez l'angle.

OCaml

Loading editor...

let rec spirale (n : int) (p : point) (l : float) (a : float) : unit =
  failwith "à compléter";;

spirale 40 (0., 0.) 1. 0.

On passe maintenant à de vraies fractales : la fonction s'appelle plusieurs fois elle-même.

Question 8 : l'ensemble de Cantor

On part d'un segment horizontal. À chaque étape, on retire le tiers central de chaque segment, et on dessine ce qui reste juste en dessous.

Écrivez une fonction cantor : int -> float -> float -> float -> unit telle que cantor n x1 x2 y dessine le segment de à , puis, si n > 0, appelle cantor (n - 1) sur le tiers gauche et sur le tiers droit, à la hauteur y -. 0.1.

OCaml

Loading editor...

let rec cantor (n : int) (x1 : float) (x2 : float) (y : float) : unit =
  failwith "à compléter";;

cantor 5 0. 1. 0.
Question 9 : un arbre

Écrivez une fonction arbre : int -> point -> float -> float -> unit telle que arbre n p l a dessine un arbre de profondeur n :

  • si n = 0, on ne dessine rien ;
  • sinon, on dessine un tronc de longueur l, qui part de p dans la direction a, puis deux arbres de profondeur n - 1 qui partent du bout du tronc : un tourné de vers la gauche, l'autre de vers la droite, avec une longueur multipliée par .

Dessinez arbre 10 (0., 0.) 1. (Float.pi /. 2.), puis utilisez le bouton « Rejouer le tracé » : dans quel ordre les branches sont-elles dessinées ?

OCaml

Loading editor...

let rec arbre (n : int) (p : point) (l : float) (a : float) : unit =
  failwith "à compléter";;

arbre 10 (0., 0.) 1. (Float.pi /. 2.)
Question 10

Combien de segments dessine arbre n ? Démontrez-le par récurrence, puis vérifiez avec le compteur affiché sous le dessin.

Question 11 : le triangle de Sierpiński

Écrivez une fonction sierpinski : int -> point -> point -> point -> unit telle que sierpinski n a b c :

  • si n = 0, dessine le triangle ;
  • sinon, calcule les milieux des trois côtés, et dessine récursivement les trois « petits » triangles de Sierpiński qui ont chacun un sommet en commun avec (le triangle du milieu est laissé vide).
OCaml

Loading editor...

let rec sierpinski (n : int) (a : point) (b : point) (c : point) : unit =
  failwith "à compléter";;

sierpinski 6 (0., 0.) (1., 0.) (0.5, sqrt 3. /. 2.)
Question 12 (un peu plus dur) : l'arbre en H

Un « H » est formé d'une barre horizontale de longueur , et de deux barres verticales de longueur centrées sur ses extrémités. L'arbre en H de profondeur est un H, avec un arbre en H de profondeur , deux fois plus petit, centré sur chacune des quatre extrémités des barres verticales.

Écrivez une fonction arbre_h : int -> point -> float -> unit telle que arbre_h n centre l le dessine (rien si n = 0).

OCaml

Loading editor...

let rec arbre_h (n : int) ((x, y) : point) (l : float) : unit =
  failwith "à compléter";;

arbre_h 4 (0., 0.) 1.

III. Des listes de points

Jusqu'ici, nos fonctions dessinaient directement. On va maintenant séparer le calcul de la figure (une liste de points, une valeur comme une autre, qu'on peut transformer) de son dessin.

Rappel : les listes

  • Une liste est soit vide [], soit de la forme x :: reste (un premier élément x, suivi d'une liste reste). La notation [a; b; c] est un raccourci pour a :: b :: c :: [].
  • Tous les éléments ont le même type : une liste de points a le type point list.
  • On les parcourt par filtrage (match), récursivement :
let rec longueur l = match l with
  | [] -> 0
  | _ :: reste -> 1 + longueur reste
  • Les motifs peuvent être plus précis : [x] (exactement un élément), x :: y :: reste (au moins deux éléments)... Les motifs sont essayés dans l'ordre, et doivent couvrir tous les cas (sinon, OCaml affiche un avertissement : lisez-le !).
  • l1 @ l2 concatène deux listes. Attention : son coût est proportionnel à la longueur de l1 (il faut recopier l1).
Question 13

Écrivez une fonction récursive ligne : point list -> unit qui dessine la ligne brisée passant par les points de la liste, dans l'ordre : ligne [p1; p2; p3] trace les segments et . Une liste de 0 ou 1 point ne dessine rien.

Filtrez avec les motifs p :: q :: reste (au moins deux points : on trace , puis on continue à partir de q) et _ (tous les autres cas).

OCaml

Loading editor...

let rec ligne (l : point list) : unit =
  failwith "à compléter";;

ligne [(0., 0.); (1., 1.); (2., 0.); (3., 1.)]
Question 14

Écrivez une fonction polygone : point list -> unit qui dessine la ligne brisée fermée : on revient au premier point à la fin. Utilisez ligne et @.

OCaml

Loading editor...

let polygone (l : point list) : unit =
  failwith "à compléter";;

polygone [(0., 0.); (1., 0.); (1.5, 1.); (0.5, 1.5); (-0.5, 1.)]

La courbe de Koch

La courbe de Koch de profondeur entre deux points et s'obtient ainsi :

p p a a p--a c c a--c b b a--b c--b q q b--q

Plutôt que de dessiner directement, koch n p q va renvoyer la liste des points de la courbe. Pour ne pas répéter les points de jonction (le point est à la fois la fin du premier morceau et le début du deuxième), on convient que koch n p q renvoie les points de la courbe sans le dernier point : la courbe complète est donc koch n p q @ [q].

Question 15

  1. Exprimez les coordonnées de et en fonction de celles de et .
  2. Le point s'obtient en partant de , et en avançant de dans la direction de tournée de . Écrivez une fonction sommet : point -> point -> point qui renvoie à partir de et .

La direction du vecteur est l'angle atan2 dy dx (attention à l'ordre des arguments).

OCaml

Loading editor...

let sommet (a : point) (b : point) : point =
  failwith "à compléter";;

sommet (0., 0.) (1., 0.)
Question 16

Écrivez la fonction récursive koch : int -> point -> point -> point list, puis dessinez koch 4 (0., 0.) (1., 0.) @ [(1., 0.)] avec ligne.

OCaml

Loading editor...

let rec koch (n : int) (p : point) (q : point) : point list =
  failwith "à compléter";;

ligne (koch 4 (0., 0.) (1., 0.) @ [(1., 0.)])
Question 17

  1. Combien de points contient koch n p q ? Vérifiez avec List.length pour quelques valeurs de n.
  2. Dessinez le flocon de Koch : trois courbes de Koch sur les côtés d'un triangle équilatéral. Dans quel sens faut-il parcourir le triangle pour que les pointes soient vers l'extérieur ?
OCaml

Loading editor...

List.length (koch 5 (0., 0.) (1., 0.))
OCaml

Loading editor...

let flocon (n : int) : point list =
  failwith "à compléter";;

ligne (flocon 4)
Question 18 : complexité

On note le coût de koch n p q. On rappelle que l1 @ l2 coûte un temps proportionnel à la longueur de l1.

  1. Établissez une relation de récurrence sur , et déduisez-en que .
  2. La taille du résultat est : on aimerait un coût en . Pour cela, on écrit une fonction auxiliaire koch_acc : int -> point -> point -> point list -> point list telle que koch_acc n p q acc renvoie les points de la courbe de Koch (sans ) suivis des éléments de la liste acc. Ainsi, koch n p q = koch_acc n p q []. Écrivez cette fonction sans utiliser @.

Dans le cas n = 0, c'est simplement p :: acc. Sinon, il faut placer les 4 morceaux devant acc : commencez par le dernier morceau (de à ), dont le résultat devient l'accumulateur du morceau précédent, etc.

OCaml

Loading editor...

let rec koch_acc (n : int) (p : point) (q : point) (acc : point list) : point list =
  failwith "à compléter";;

koch_acc 5 (0., 0.) (1., 0.) [] = koch 5 (0., 0.) (1., 0.)

Transformer des figures : List.map

Une figure est maintenant une simple liste de points : pour la déplacer, la tourner ou l'agrandir, il suffit d'appliquer la même transformation à chacun de ses points.

Rappel : fonctions d'ordre supérieur

En OCaml, les fonctions sont des valeurs comme les autres : on peut les passer en argument, les renvoyer, les stocker dans une liste...

  • fun x -> e est la fonction (anonyme) qui à x associe e. let f x = e est un raccourci pour let f = fun x -> e.
  • Les fonctions à plusieurs arguments sont curryfiées : let f x y = e est en fait let f = fun x -> (fun y -> e). On peut donc appliquer partiellement une fonction : si avance : point -> float -> float -> point, alors avance (0., 0.) 1. est une fonction de type float -> point.
Question 19

Écrivez une fonction récursive applique : (point -> point) -> point list -> point list telle que applique f l renvoie la liste des images par f des points de l. Testez-la en déplaçant le flocon de 1 vers la droite, avec une fonction anonyme.

OCaml

Loading editor...

let rec applique (f : point -> point) (l : point list) : point list =
  failwith "à compléter";;

let f = flocon 3 in
ligne f;
ligne (applique (fun (x, y) -> (x +. 1., y)) f)

Cette fonction est tellement utile qu'elle existe déjà, pour tous les types : c'est List.map : ('a -> 'b) -> 'a list -> 'b list. Les 'a et 'b désignent des types quelconques : List.map est polymorphe.

De même, List.iter : ('a -> unit) -> 'a list -> unit applique une fonction (qui a un effet) à chaque élément d'une liste, dans l'ordre. Par exemple List.iter print_int [1; 2; 3] affiche 123.

Question 20

Écrivez les transformations suivantes, de type point -> point une fois tous leurs arguments donnés sauf le dernier :

  • translation : float -> float -> point -> point : translation dx dy p déplace p de ;
  • homothetie : float -> point -> point : homothetie k p multiplie les coordonnées de p par k ;
  • rotation : float -> point -> point : rotation a p tourne p d'un angle a autour de l'origine. On rappelle que l'image de est .
OCaml

Loading editor...

let translation (dx : float) (dy : float) ((x, y) : point) : point =
  failwith "à compléter";;
let homothetie (k : float) ((x, y) : point) : point =
  failwith "à compléter";;
let rotation (a : float) ((x, y) : point) : point =
  failwith "à compléter";;

rotation (Float.pi /. 2.) (1., 0.)
Question 21 : une rosace

En utilisant List.iter sur une liste d'angles, List.map et rotation, dessinez 8 copies de la courbe koch 3 (0., 0.) (1., 0.) @ [(1., 0.)], tournées de autour de l'origine.

Bonus : construisez la liste des angles avec une fonction récursive plutôt qu'à la main, et ajoutez des copies de plus en plus petites avec homothetie.

OCaml

Loading editor...

let courbe = koch 3 (0., 0.) (1., 0.) @ [(1., 0.)];;

(* à compléter *)
D'autres fonctions du module

List

  • List.for_all f l : est-ce que f x vaut true pour tous les éléments x de l ?
  • List.exists f l : est-ce que f x vaut true pour au moins un élément ?
  • List.filter f l : la liste des éléments x de l tels que f x vaut true.
  • List.mem x l : est-ce que x est un élément de l ?
Question 22

Le centre du flocon flocon n est le point .

  1. Avec List.for_all, vérifiez que tous les points de flocon 5 sont à une distance au plus de .
  2. Avec List.filter, dessinez seulement les points de flocon 5 qui sont à une distance au moins de , chacun sous la forme d'un petit segment (de p à translation 0.005 0. p).
  3. Avec List.exists, vérifiez que le flocon passe bien par le point .
OCaml

Loading editor...

let g = (0.5, sqrt 3. /. 6.);;

(* 1. *)

(* 2. *)

(* 3. *)
Rappel : le type

option Une valeur de type 'a option est soit None (« pas de valeur »), soit Some x avec x de type 'a. On s'en sert pour les fonctions qui ne peuvent pas toujours renvoyer un résultat, et on l'utilise avec un match :

match resultat with
| None -> ...
| Some x -> ...
Question 23

Écrivez une fonction récursive plus_haut : point list -> point option qui renvoie le point de plus grande ordonnée d'une liste (None si la liste est vide). Testez-la sur flocon 3 et sur [].

OCaml

Loading editor...

let rec plus_haut (l : point list) : point option =
  failwith "à compléter";;

plus_haut (flocon 3);;
plus_haut []

IV. Un type récursif pour les figures

Une liste de points ne permet de représenter qu'une seule ligne. Pour des figures plus riches, on définit notre propre type :

OCaml

Loading editor...

type figure =
  | Trait of point * point          (* un segment *)
  | Groupe of figure list           (* plusieurs figures superposées *)
  | Deplace of float * float * figure  (* la figure, translatée de (dx, dy) *)
  | Zoom of float * figure          (* la figure, agrandie d'un facteur k (centre : l'origine) *)
  | Tourne of float * figure;;      (* la figure, tournée d'un angle a autour de l'origine *)

let carre_unite = Groupe [Trait ((0., 0.), (1., 0.)); Trait ((1., 0.), (1., 1.));
                          Trait ((1., 1.), (0., 1.)); Trait ((0., 1.), (0., 0.))];;

let exemple = Groupe [carre_unite; Deplace (2., 0., Tourne (Float.pi /. 4., Zoom (0.5, carre_unite)))]
Rappel : types sommes

  • Un type somme, ou union, liste les différentes formes possibles d'une valeur. Chaque forme a un constructeur (qui commence par une majuscule), éventuellement suivi d'arguments (of ...).
  • Le type est récursif quand certains constructeurs contiennent des valeurs du type lui-même : une figure contient des figures. Ici, Groupe en contient même une liste.
  • On l'utilise par filtrage, avec un cas par constructeur :
match f with
| Trait (p, q) -> ...
| Groupe l -> ...
| Deplace (dx, dy, g) -> ...
| Zoom (k, g) -> ...
| Tourne (a, g) -> ...
Rappel : fonctions mutuellement récursives

Pour parcourir une figure, on a besoin d'une fonction sur les figures et d'une fonction sur les listes de figures (pour Groupe), qui s'appellent l'une l'autre. On les définit ensemble, avec let rec ... and ... :

let rec f x = ... g ... 
and g y = ... f ...
Question 24

Écrivez deux fonctions mutuellement récursives nb_traits : figure -> int et nb_traits_liste : figure list -> int, qui comptent le nombre de traits d'une figure (resp. d'une liste de figures). nb_traits exemple doit valoir 8.

OCaml

Loading editor...

let rec nb_traits (f : figure) : int =
  failwith "à compléter"
and nb_traits_liste (l : figure list) : int =
  failwith "à compléter";;

nb_traits exemple
Question 25 : dessiner une figure

Pour dessiner une figure, on la parcourt en retenant la transformation à appliquer à ses points : au départ, c'est l'identité ; quand on rentre dans un Zoom (k, g), les points de g doivent d'abord être agrandis d'un facteur k, puis subir la transformation qu'on avait déjà.

Écrivez une fonction dessine_avec : (point -> point) -> figure -> unit telle que dessine_avec t f dessine l'image de la figure f par la transformation t, puis dessine : figure -> unit. Utilisez List.iter pour les groupes.

  • Pour Trait (p, q), on trace le segment entre t p et t q.
  • Pour Zoom (k, g), la nouvelle transformation est fun p -> t (homothetie k p). Les autres sont similaires (avec translation et rotation).
OCaml

Loading editor...

let rec dessine_avec (t : point -> point) (f : figure) : unit =
  failwith "à compléter";;

let dessine (f : figure) : unit =
  failwith "à compléter";;

dessine exemple
Question 26 : Sierpiński, le retour

Avec ce type, une fractale se décrit très simplement : le triangle de Sierpiński de profondeur (dans le triangle de sommets , , ) est formé de trois copies de celui de profondeur , réduites de moitié, dont deux sont déplacées.

Écrivez une fonction sierpinski_fig : int -> figure. Pour , c'est le triangle lui-même (un Groupe de trois Trait).

OCaml

Loading editor...

let rec sierpinski_fig (n : int) : figure =
  failwith "à compléter";;

dessine (sierpinski_fig 6);;
nb_traits (sierpinski_fig 6)
Question 27

Dans votre sierpinski_fig, combien d'appels récursifs faites-vous pour construire la figure de profondeur ? Si vous en faites trois, modifiez votre fonction pour n'en faire qu'un.

Combien de constructeurs (Trait, Groupe, ...) sont alors créés en mémoire pour construire sierpinski_fig n ? Comparez avec nb_traits (sierpinski_fig n).

Question 28 (plus difficile) : l'arbre de Pythagore

L'arbre de Pythagore de profondeur est formé du carré unité, sur le côté supérieur duquel on pose deux arbres de Pythagore de profondeur , réduits d'un facteur et tournés de , de façon à ce que leurs carrés de base forment un triangle rectangle isocèle avec le côté supérieur du carré. (Pour , il n'y a que le carré.)

Écrivez pythagore : int -> figure et dessinez pythagore 10.

Le premier sous-arbre a sa base sur le segment de à : on l'obtient en réduisant, puis en tournant de , puis en déplaçant de . Pour le second, la base va de à : par quel angle faut-il tourner ?

OCaml

Loading editor...

let rec pythagore (n : int) : figure =
  failwith "à compléter";;

dessine (pythagore 10)

V. Tableaux, références et boucles : la courbe du dragon

Prenez une bande de papier, pliez-la en deux, puis encore en deux, et encore... dans le même sens. Dépliez-la ensuite en laissant chaque pli à angle droit : vous obtenez la courbe du dragon.

Si on note G un pli qui tourne à gauche et D un pli qui tourne à droite, en parcourant la bande d'un bout à l'autre :

On passe de l'étape à l'étape en écrivant : la suite de l'étape , puis G, puis la suite de l'étape retournée (lue de droite à gauche) et inversée (G et D échangés). On représente une suite de plis par un tableau de booléens : true pour G et false pour D.

Rappel : tableaux

  • Un tableau a une taille fixée à sa création, et ses cases sont modifiables. Son type est 'a array.
  • [| 1; 2; 3 |] est un tableau. Array.make n x crée un tableau de taille n dont toutes les cases valent x, Array.init n f crée le tableau [| f 0; f 1; ...; f (n-1) |].
  • Array.length t est sa taille, t.(i) sa case d'indice i (de 0 à Array.length t - 1), et t.(i) <- v modifie cette case (c'est une expression de type unit).
  • Array.iter, Array.map, Array.for_all, Array.exists, Array.mem... fonctionnent comme pour les listes.
Rappel : boucles

  • for i = a to b do ... done exécute le corps pour i valant a, a + 1, ..., b (les deux bornes sont incluses). Il n'y a rien à faire si b < a.
  • while condition do ... done répète le corps tant que la condition est vraie.
  • Le corps d'une boucle est de type unit, et la boucle aussi.
Question 29

Écrivez une fonction retourne_inverse : bool array -> bool array qui renvoie un nouveau tableau : le tableau donné, lu de droite à gauche, avec ses valeurs inversées. Écrivez-en deux versions : une avec Array.make et une boucle for, une avec Array.init (sans boucle).

OCaml

Loading editor...

let retourne_inverse (t : bool array) : bool array =
  failwith "à compléter";;

let retourne_inverse_2 (t : bool array) : bool array =
  failwith "à compléter";;

retourne_inverse [| true; true; false |];;
retourne_inverse_2 [| true; true; false |]
Question 30

Écrivez une fonction récursive plis : int -> bool array qui renvoie la suite des plis après n pliages (le tableau vide [||] pour ). Si le tableau de l'étape a cases, celui de l'étape en a : créez-le avec Array.make, puis remplissez-le avec une boucle.

OCaml

Loading editor...

let rec plis (n : int) : bool array =
  failwith "à compléter";;

plis 3

Pour dessiner la courbe, on fait comme une tortue : elle a une position et une direction, avance d'une unité en traçant un segment, tourne à gauche ou à droite selon le pli, avance à nouveau, etc. La position et la direction changent au cours du temps : on va les stocker dans des références.

Ne me demandez pas pourquoi spécifiquement une tortue, mais c'est un vrai vocabulaire utilisé pour parler de ce genre de choses. Bref, imaginez-vous une petite tortue qui avance en laissant un trait derrière elle ?

Rappel : références

Une référence est une « boîte » dont on peut changer le contenu :

  • let r = ref 0 crée une référence (de type int ref) contenant 0 ;
  • !r est son contenu, et r := 5 le remplace (expression de type unit) ;
  • incr r et decr r ajoutent ou retirent 1 à une référence entière.

Les variables créées par let ne changent jamais : let x = 1 in ... x = x + 1 compare x à x + 1 (et vaut false), il ne modifie rien !

Question 31

On travaille avec des coordonnées entières : la tortue va dans l'une des 4 directions [| (1, 0); (0, 1); (-1, 0); (0, -1) |] (droite, haut, gauche, bas), et on retient seulement l'indice de sa direction dans ce tableau. Tourner à gauche revient à ajouter 1 à cet indice, tourner à droite à retirer 1, modulo 4.

Écrivez une fonction dragon : bool array -> unit qui dessine la courbe correspondant à une suite de plis : la tortue part de vers la droite, avance, puis pour chaque pli, tourne et avance. Utilisez deux références (position et direction), et float_of_int pour appeler segment. Dessinez dragon (plis 12), et rejouez le tracé.

Attention au modulo

En OCaml, a mod b a le signe de a : (-1) mod 4 vaut -1, pas 3. Pour tourner à droite, calculez plutôt (d + 3) mod 4.

OCaml

Loading editor...

let directions = [| (1, 0); (0, 1); (-1, 0); (0, -1) |];;

let dragon (plis : bool array) : unit =
  failwith "à compléter";;

dragon (plis 12)

Est-ce que le dragon se recoupe ?

En rejouant le tracé, on a l'impression que la courbe passe plusieurs fois au même endroit. Vérifions-le avec un dictionnaire.

Rappel : tables de hachage (module

Hashtbl) Un dictionnaire associe des valeurs à des clés. Le module Hashtbl en fournit une version mutable, efficace (opérations en temps constant en moyenne) :

  • let h = Hashtbl.create 100 crée une table vide (le nombre n'a pas d'importance) ;
  • Hashtbl.add h cle valeur ajoute une association, Hashtbl.remove h cle la supprime ;
  • Hashtbl.mem h cle teste si la clé est présente ;
  • Hashtbl.find h cle renvoie la valeur associée (et lève une exception si la clé est absente), Hashtbl.find_opt h cle renvoie une option ;
  • Hashtbl.iter (fun cle valeur -> ...) h parcourt toutes les associations.

Les clés peuvent être de presque n'importe quel type : entiers, chaînes, couples de points...

Attention : n'ajoutez pas deux fois la même clé avec Hashtbl.add (utilisez Hashtbl.remove d'abord, ou Hashtbl.mem pour tester).

Question 32

Écrivez une fonction sommets_visites : bool array -> (int * int, int) Hashtbl.t qui renvoie une table associant à chaque point (à coordonnées entières) par lequel passe la tortue le nombre de fois où elle y passe (le point de départ compte pour une fois). Reprenez le code de dragon, sans dessiner.

Utilisez-la pour répondre aux questions suivantes, pour plis 12 :

  1. La courbe passe-t-elle plusieurs fois par un même point ? Combien de points sont visités deux fois ? Plus de deux fois ?
  2. (plus dur) Passe-t-elle plusieurs fois par un même segment ? Pour le savoir, écrivez une fonction similaire, dont les clés sont les segments parcourus. Comment faire pour que le segment de à et celui de à aient la même clé ?
OCaml

Loading editor...

let sommets_visites (plis : bool array) : (int * int, int) Hashtbl.t =
  failwith "à compléter";;

let deux = ref 0 and plus = ref 0 in
Hashtbl.iter (fun _ k -> if k = 2 then incr deux else if k > 2 then incr plus) (sommets_visites (plis 12));
(!deux, !plus)
OCaml

Loading editor...

(* 2. *)

VI. Pour aller plus loin : les L-systèmes

Toutes nos fractales peuvent être décrites par des règles de réécriture de mots, interprétées par une tortue. C'est l'idée des L-systèmes, inventés par le biologiste Aristid Lindenmayer pour modéliser la croissance des plantes.

Un L-système est donné par :

À chaque étape, on remplace simultanément toutes les lettres du mot : F donne F+F-F-F+F, qui donne F+F-F-F+F+F+F-F-F+F-F+F-F-F+F-....

On interprète enfin le mot avec une tortue : F veut dire « avancer d'une unité en traçant », + « tourner à gauche d'un angle fixé », - « tourner à droite de », et les autres lettres ne font rien. Avec et l'exemple ci-dessus, on obtient une variante (carrée) de la courbe de Koch.

On représente les mots par des listes de caractères (char list), et les règles par une table de hachage de type (char, char list) Hashtbl.t.

Rappel : caractères et chaînes

  • Un caractère (char) s'écrit entre apostrophes : 'F', '+'. Une chaîne (string) entre guillemets : "F+F".
  • String.length s est la longueur de s, et s.[i] son caractère d'indice i (de 0 à String.length s - 1). Les chaînes ne sont pas modifiables.
  • s1 ^ s2 concatène deux chaînes (en recopiant les deux : à éviter dans une boucle).
Question 33

Écrivez une fonction liste_de_chaine : string -> char list qui renvoie la liste des caractères d'une chaîne, dans l'ordre (par exemple avec une fonction auxiliaire récursive qui prend en argument un indice).

OCaml

Loading editor...

let liste_de_chaine (s : string) : char list =
  failwith "à compléter";;

liste_de_chaine "F+F-F"
Question 34

  1. Écrivez une fonction regles : (char * string) list -> (char, char list) Hashtbl.t qui crée la table des règles à partir d'une liste de couples (lettre, mot de remplacement). Par exemple regles [('F', "F+F-F-F+F")].
  2. Écrivez une fonction récursive reecrit : (char, char list) Hashtbl.t -> char list -> char list qui applique une étape de réécriture à un mot. Utilisez Hashtbl.find_opt.
  3. Écrivez une fonction iteree : int -> (char, char list) Hashtbl.t -> char list -> char list qui applique n étapes de réécriture.
OCaml

Loading editor...

let regles (l : (char * string) list) : (char, char list) Hashtbl.t =
  failwith "à compléter";;

let rec reecrit (r : (char, char list) Hashtbl.t) (mot : char list) : char list =
  failwith "à compléter";;

let rec iteree (n : int) (r : (char, char list) Hashtbl.t) (mot : char list) : char list =
  failwith "à compléter";;

let koch_carre = regles [('F', "F+F-F-F+F")];;
List.length (iteree 4 koch_carre ['F'])
Question 35

Que vaut le coût de reecrit r mot, en fonction de la longueur de mot et des longueurs des règles ? Est-ce que l'utilisation de @ pose ici un problème ?

Question 36 : la tortue

Écrivez une fonction tortue : float -> char list -> unit telle que tortue delta mot interprète le mot comme expliqué plus haut. La tortue part de , vers la droite, et avance d'une longueur 1. Utilisez des références pour la position et l'angle, avance, et List.iter.

Dessinez la courbe de Koch carrée de profondeur 4 (avec ), puis le flocon de Koch : axiome F--F--F, règle F → F+F--F+F, .

match dans une fonction anonyme Dans List.iter (fun c -> match c with ... ) mot, les parenthèses autour de la fonction anonyme sont indispensables : sinon, le dernier cas du match « avalerait » la suite.

OCaml

Loading editor...

let tortue (delta : float) (mot : char list) : unit =
  failwith "à compléter";;

tortue (Float.pi /. 2.) (iteree 4 koch_carre ['F'])
OCaml

Loading editor...

(* le flocon de Koch *)

Des plantes

Pour dessiner des plantes, il faut des branches : la tortue doit pouvoir se souvenir d'une position, dessiner une branche, puis revenir à cette position. On ajoute deux lettres :

Les crochets peuvent être imbriqués : il faut bien une pile.

Rappel : piles (module

Stack)

  • let p = Stack.create () crée une pile vide ;
  • Stack.push x p empile x ;
  • Stack.pop p dépile et renvoie l'élément du sommet (exception si la pile est vide) ;
  • Stack.is_empty p teste si la pile est vide.

(Le module Queue fournit les mêmes fonctions pour une file : Queue.create, Queue.push, Queue.pop, Queue.is_empty.)

Question 37

Écrivez une fonction tortue_branches : float -> float -> char list -> unit qui gère aussi les crochets. Le premier argument est l'angle de départ de la tortue (pour que les plantes poussent vers le haut).

Dessinez ensuite la plante d'axiome X, de règles X → F+[[X]-X]-F[-FX]+X et F → FF, avec , un angle de départ de , et 5 étapes. Rejouez le tracé !

OCaml

Loading editor...

let tortue_branches (depart : float) (delta : float) (mot : char list) : unit =
  failwith "à compléter";;

let plante = regles [('X', "F+[[X]-X]-F[-FX]+X"); ('F', "FF")] in
tortue_branches (Float.pi /. 2.) (25. *. Float.pi /. 180.) (iteree 5 plante ['X'])

Essayez maintenant avec 6 étapes : vous obtenez une erreur Stack overflow (débordement de pile).

En effet, reecrit n'est pas récursive terminale : chaque appel reecrit r reste doit attendre le résultat de l'appel suivant pour calculer m @ .... Il y a donc autant d'appels imbriqués (stockés dans la pile d'appels) que de lettres dans le mot, soit plus de 6000 ici. Or la pile est limitée, et l'est particulièrement sur ce site : quelques milliers d'appels imbriqués au maximum.

Rappel : récursivité terminale

Un appel récursif est terminal quand son résultat est directement le résultat de la fonction : il n'y a plus rien à faire après. Une fonction dont tous les appels récursifs sont terminaux n'utilise pas la pile : elle s'exécute comme une boucle.

Pour rendre une fonction récursive terminale, on utilise en général un accumulateur : un argument supplémentaire qui contient le résultat « en cours de construction ».

(* pas terminale : il reste à faire 1 + ... après l'appel *)
let rec longueur l = match l with
  | [] -> 0
  | _ :: reste -> 1 + longueur reste

(* terminale : acc contient la longueur de ce qui a déjà été parcouru *)
let rec longueur_acc l acc = match l with
  | [] -> acc
  | _ :: reste -> longueur_acc reste (acc + 1)
Question 38 (plus difficile) : récursivité terminale

  1. Écrivez une fonction récursive terminale ajoute_envers : 'a list -> 'a list -> 'a list telle que ajoute_envers l acc renvoie les éléments de l dans l'ordre inverse, suivis des éléments de acc. Par exemple, ajoute_envers [1; 2; 3] [4; 5] vaut [3; 2; 1; 4; 5]. En déduire une fonction renverse : 'a list -> 'a list qui renverse une liste.
  2. Écrivez une version récursive terminale reecrit_t de reecrit, et la fonction iteree_t correspondante. Dessinez la plante avec 6 étapes.

Utilisez une fonction auxiliaire aux mot acc, où acc contient le début du résultat (ce qui a été obtenu en réécrivant les lettres déjà lues)... à l'envers. Il faut donc le renverser à la fin. Pourquoi est-ce que garder acc à l'endroit ne fonctionnerait pas ?

OCaml

Loading editor...

let rec ajoute_envers (l : 'a list) (acc : 'a list) : 'a list =
  failwith "à compléter";;

let renverse (l : 'a list) : 'a list =
  failwith "à compléter";;

let reecrit_t (r : (char, char list) Hashtbl.t) (mot : char list) : char list =
  failwith "à compléter";;

let rec iteree_t (n : int) (r : (char, char list) Hashtbl.t) (mot : char list) : char list =
  failwith "à compléter";;

ajoute_envers [1; 2; 3] [4; 5];;
let plante = regles [('X', "F+[[X]-X]-F[-FX]+X"); ('F', "FF")] in
tortue_branches (Float.pi /. 2.) (25. *. Float.pi /. 180.) (iteree_t 6 plante ['X'])
Question 39 : à vous de jouer

Voici quelques L-systèmes classiques. Pour certains, plusieurs lettres font avancer la tortue : modifiez votre tortue pour qu'elle prenne en argument la liste des lettres qui tracent (et utilisez List.mem).

NomAxiomeRèglesLettres qui tracent
DragonFXX → X+YF+, Y → -FX-YF
Pointe de flèche de SierpińskiAA → B-A-B, B → A+B+AA, B
Courbe de GosperAA → A-B--B+A++AA+B-, B → +A-BB--B-A++A+BA, B
BuissonFF → FF+[+F-F-F]-[-F+F+F]F

Utilisez iteree_t si vous l'avez écrite. Comparez la courbe du dragon obtenue avec celle de la partie V. Puis inventez votre propre fractale !

OCaml

Loading editor...

let tortue_generale (traceurs : char list) (depart : float) (delta : float) (mot : char list) : unit =
  failwith "à compléter";;

let degres d = d *. Float.pi /. 180.;;

let sierpinski_ls = regles [('A', "B-A-B"); ('B', "A+B+A")] in
tortue_generale ['A'; 'B'] 0. (degres 60.) (iteree 7 sierpinski_ls ['A'])
OCaml

Loading editor...


Entrez le code donné en cours pour accéder à la solution :