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
Un petit exemple :
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(et1est un entier) - les opérations sur les flottants ont un point :
+.,-.,*.,/. - pour convertir :
float_of_int 3vaut3.,int_of_float 3.7vaut3; - fonctions utiles :
sqrt,cos,sin(angles en radians), et la constanteFloat.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).
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.
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).
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
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 ...
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
- Écrivez une fonction récursive
cotes : int -> float -> point -> float -> float -> unittelle quecotes k t p l adessineksegments de longueurlà la suite : le premier part depdans la directiona, chaque segment suivant part de la fin du précédent, et on tourne detentre deux segments. - En déduire une fonction
polygone_regulier : int -> point -> float -> unittelle quepolygone_regulier n p ldessine un polygone régulier àncôtés de longueurl, partant dep. 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.
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 spirale 40 (0., 0.) 1. 0. puis changez l'angle.
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 n > 0, appelle cantor (n - 1) sur le tiers gauche et sur le tiers droit, à la hauteur y -. 0.1.
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 depdans la directiona, puis deux arbres de profondeurn - 1qui partent du bout du tronc : un tourné devers 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 ?
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).
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
Écrivez une fonction arbre_h : int -> point -> float -> unit telle que arbre_h n centre l le dessine (rien si n = 0).
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 formex :: reste(un premier élémentx, suivi d'une listereste). La notation[a; b; c]est un raccourci poura :: 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 @ l2concatène deux listes. Attention : son coût est proportionnel à la longueur del1(il faut recopierl1).
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
Filtrez avec les motifs p :: q :: reste (au moins deux points : on trace q) et _ (tous les autres cas).
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 @.
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
- si
, c'est le segment ; - sinon, on coupe
en trois, aux points (au tiers) et (aux deux tiers), on construit un triangle équilatéral sur le tiers central (avec « à gauche » quand on va de vers ), et on remplace chacun des 4 segments , , , par une courbe de Koch de profondeur .
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 koch n p q renvoie les points de la courbe sans le dernier point koch n p q @ [q].
Question 15
- Exprimez les coordonnées de
et en fonction de celles de et . - 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 -> pointqui renvoieà partir de et .
La direction du vecteur atan2 dy dx (attention à l'ordre des arguments).
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.
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
- Combien de points contient
koch n p q? Vérifiez avecList.lengthpour quelques valeurs den. - 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 ?
Loading editor...
List.length (koch 5 (0., 0.) (1., 0.))
Loading editor...
let flocon (n : int) : point list = failwith "à compléter";; ligne (flocon 4)
Question 18 : complexité
On note koch n p q. On rappelle que l1 @ l2 coûte un temps proportionnel à la longueur de l1.
- Établissez une relation de récurrence sur
, et déduisez-en que . - 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 listtelle quekoch_acc n p q accrenvoie 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
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 -> eest la fonction (anonyme) qui àxassociee.let f x = eest un raccourci pourlet f = fun x -> e.- Les fonctions à plusieurs arguments sont curryfiées :
let f x y = eest en faitlet f = fun x -> (fun y -> e). On peut donc appliquer partiellement une fonction : siavance : point -> float -> float -> point, alorsavance (0., 0.) 1.est une fonction de typefloat -> 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.
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 pdéplacepde; homothetie : float -> point -> point:homothetie k pmultiplie les coordonnées deppark;rotation : float -> point -> point:rotation a ptournepd'un angleaautour de l'origine. On rappelle que l'image deest .
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
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.
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 quef xvauttruepour tous les élémentsxdel?List.exists f l: est-ce quef xvauttruepour au moins un élément ?List.filter f l: la liste des élémentsxdeltels quef xvauttrue.List.mem x l: est-ce quexest un élément del?
Question 22
Le centre du flocon flocon n est le point
- Avec
List.for_all, vérifiez que tous les points deflocon 5sont à une distance au plusde . - Avec
List.filter, dessinez seulement les points deflocon 5qui sont à une distance au moinsde , chacun sous la forme d'un petit segment (de pàtranslation 0.005 0. p). - Avec
List.exists, vérifiez que le flocon passe bien par le point.
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 [].
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 :
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,
Groupeen 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.
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 entret pett q. - Pour
Zoom (k, g), la nouvelle transformation estfun p -> t (homothetie k p). Les autres sont similaires (avectranslationetrotation).
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
Écrivez une fonction sierpinski_fig : int -> figure. Pour Groupe de trois Trait).
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
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
Écrivez pythagore : int -> figure et dessinez pythagore 10.
Le premier sous-arbre a sa base sur le segment de
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 :
- après 1 pliage :
G - après 2 pliages :
G G D - après 3 pliages :
G G D G G D D
On passe de l'étape G, puis la suite de l'étape 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 xcrée un tableau de taillendont toutes les cases valentx,Array.init n fcrée le tableau[| f 0; f 1; ...; f (n-1) |].Array.length test sa taille,t.(i)sa case d'indicei(de0àArray.length t - 1), ett.(i) <- vmodifie cette case (c'est une expression de typeunit).Array.iter,Array.map,Array.for_all,Array.exists,Array.mem... fonctionnent comme pour les listes.
Rappel : boucles
for i = a to b do ... doneexécute le corps pourivalanta,a + 1, ...,b(les deux bornes sont incluses). Il n'y a rien à faire sib < a.while condition do ... doneré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).
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 Array.make, puis remplissez-le avec une boucle.
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 0crée une référence (de typeint ref) contenant0;!rest son contenu, etr := 5le remplace (expression de typeunit) ;incr retdecr rajoutent 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 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.
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 100crée une table vide (le nombre n'a pas d'importance) ;Hashtbl.add h cle valeurajoute une association,Hashtbl.remove h clela supprime ;Hashtbl.mem h cleteste si la clé est présente ;Hashtbl.find h clerenvoie la valeur associée (et lève une exception si la clé est absente),Hashtbl.find_opt h clerenvoie une option ;Hashtbl.iter (fun cle valeur -> ...) hparcourt 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 :
- La courbe passe-t-elle plusieurs fois par un même point ? Combien de points sont visités deux fois ? Plus de deux fois ?
- (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é ?
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)
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 :
- un mot de départ, l'axiome, par exemple
F; - des règles, qui disent par quoi remplacer chaque lettre, par exemple
F→F+F-F-F+F. Les lettres sans règle restent telles quelles.
À 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 - « tourner à droite de
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 sest la longueur des, ets.[i]son caractère d'indicei(de0àString.length s - 1). Les chaînes ne sont pas modifiables.s1 ^ s2concatè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).
Loading editor...
let liste_de_chaine (s : string) : char list = failwith "à compléter";; liste_de_chaine "F+F-F"
Question 34
- Écrivez une fonction
regles : (char * string) list -> (char, char list) Hashtbl.tqui crée la table des règles à partir d'une liste de couples (lettre, mot de remplacement). Par exempleregles [('F', "F+F-F-F+F")]. - Écrivez une fonction récursive
reecrit : (char, char list) Hashtbl.t -> char list -> char listqui applique une étape de réécriture à un mot. UtilisezHashtbl.find_opt. - Écrivez une fonction
iteree : int -> (char, char list) Hashtbl.t -> char list -> char listqui appliquenétapes de réécriture.
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 avance, et List.iter.
Dessinez la courbe de Koch carrée de profondeur 4 (avec 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.
Loading editor...
let tortue (delta : float) (mot : char list) : unit = failwith "à compléter";; tortue (Float.pi /. 2.) (iteree 4 koch_carre ['F'])
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 :
[: la tortue retient sa position et son angle actuels (en les empilant sur une pile) ;]: la tortue revient à la dernière position retenue (en la dépilant), sans tracer.
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 pempilex;Stack.pop pdépile et renvoie l'élément du sommet (exception si la pile est vide) ;Stack.is_empty pteste 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
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
- Écrivez une fonction récursive terminale
ajoute_envers : 'a list -> 'a list -> 'a listtelle queajoute_envers l accrenvoie les éléments deldans l'ordre inverse, suivis des éléments deacc. Par exemple,ajoute_envers [1; 2; 3] [4; 5]vaut[3; 2; 1; 4; 5]. En déduire une fonctionrenverse : 'a list -> 'a listqui renverse une liste. - Écrivez une version récursive terminale
reecrit_tdereecrit, et la fonctioniteree_tcorrespondante. 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 ?
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).
| Nom | Axiome | Règles | Lettres qui tracent | |
|---|---|---|---|---|
| Dragon | FX | X → X+YF+, Y → -FX-Y | F | |
| Pointe de flèche de Sierpiński | A | A → B-A-B, B → A+B+A | A, B | |
| Courbe de Gosper | A | A → A-B--B+A++AA+B-, B → +A-BB--B-A++A+B | A, B | |
| Buisson | F | F → 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 !
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'])Loading editor...
Entrez le code donné en cours pour accéder à la solution :