Desenarea fractalilor

Fractalii (ro, en) sunt figuri geometrice la care o parte este similară întregului. Astfel, fractalii pot fi desenați înlocuind succesiv într-o figură inițială o parte simplă (un segment) cu o copie micșorată a întregii figuri. Acest procedeu poate fi definit riguros prin reguli de rescriere care formează o gramatică, obținându-se așa-zisele L-sisteme.

Vom desena fractalul dragon. Acesta poate fi descris cu ajutorul a două părți numite X și Y, rescrise la fiecare pas după regulile:

X -> X+YF
Y -> FX-Y
unde F reprezintă un segment înainte, + și - rotiri de 90 de grade la stânga și respectiv dreapta. Desenarea pornește de la secvența inițială FX. În desenul final, după repetarea dorită a regulilor de rescriere, X și Y se ignoră.

Rezolvăm problema în pași, scriind funcții cât mai simple pentru părțile componente, și ne preocupăm doar la sfârșit de detaliile de implementare a desenării (care poate fi făcută în multe feluri).

Scriem programul în dialectul OCaml al limbajului ML, dar principiile pe care le urmăm sunt valabile independent de limbaj.

Întâi implementăm regulile de rescriere, definind o funcție care transformă fiecare simbol (caracter) într-o listă de simboluri. Caracterele '+', '-' și 'F' nu se transformă, deci rezultatul e o listă cu un singur element, chiar caracterul respectiv.

let transf c =
  if c = 'x' then ['x';'+';'y';'f']
  else if c = 'y' then ['f';'x';'-';'y']
  else [c]
Folosim o listă pentru șirul simbolurilor de desenat. Lista e transformată aplicând individual funcția transf fiecărui element. Aceasta se poate face cu funcția predefinită List.map care are două argumente: funcția de aplicat și lista de prelucrat. Aplicând de exemplu List.map transf ['f';'x'] obținem șirul după primul pas de transformare: [['f']; ['x'; '+'; 'y'; 'f']] Remarcăm însă că aceasta este o listă având ca elemente alte liste (două în acest caz). Folosim funcția List.concat care concatenează toate elementele dintr-o listă de liste într-o listă (pe un singur nivel). Astfel, putem defini un pas de prelucrare:
let step lst = List.concat (List.map transf lst)
(Această prelucrare este simplă dar nu prea eficientă, concatenarea parcurgând rezultatul parțial în mod repetat. Gândiți-vă la o implementare mai bună).

Va trebui să aplicăm această funcție de n ori (cu n dat), pentru a obține lista de simboluri care definește desenarea dragonului. Definim recursiv aplicarea unei funcții de n ori, ținând cont că f0(x) = x și fn(x) = fn-1(f(x)) pentru n > 0. Scriem atunci:

let rec powf f n x = if n = 0 then x else powf f (n-1) (f x)
și putem testa, de exemplu:
let inc2 x = x + 2
powf inc2 5 3 = 13  (* 3 incrementat cu 2 de 5 ori*)
sau puteam scrie și direct: powf ((+) 1) 5 3 unde (+) 2 este funcția care adună 2 la argument.
În cazul nostru, de exemplu powf step 2 ['f';'x'] = ['f';'x';'+';'y';'f';'+';'f';'x';'-';'y';'f']

Ca rezultat după n pași avem deci șirul

let evolve n = powf step n ['f';'x']
Ne punem acum problema desenării efective. Pentru simplitate, alegem să scriem prelucrările pentru desenat într-un fișier SVG (Scalable Vector Graphics) pentru a putea fi vizualizat ulterior. Pentru aceasta, producem o secvență de comenzi h și v care desenează un segment orizontal și respectiv vertical de dimensiune dată. De exemplu, "h 5 v -3" înseamnă desenarea din punctul curent al unui segment de 5 unități la dreapta și apoi 3 unități în jos. Cum desenarea fractalului nostru implică rotirea cursorului, e necesar să reținem poziția lui, și implementăm funcții de rotire:
let dir = ref 0
let minus() = dir := (!dir + 1) mod 4
let plus() = dir := (!dir + 3) mod 4
Prima linie declară dir ca fiind o variabilă (o valoare modificabilă); iar celelalte două funcții actualizează valoarea ei (care reprezintă unghiul fată de axa x în multipli de 90 de grade). Sintaxa !dir reprezintă valoarea lui dir, iar := e atribuirea. Niște calcule simple arată că un avans înainte de lungime len se scrie cu operațiile de avans orizontal sau vertical astfel:
let f len = if !dir mod 2 = 0
  then (print_string " h "; print_int (len * (1 - !dir)))
  else (print_string " v "; print_int (len * (2 - !dir)))
În final, desenarea unui simbol ('+', '-' sau 'F') din listă se scrie (alegând lungimea segmentului de 8 unități):
let draw_elt c =
  if c = 'f' then f 8
  else if c = '-' then minus()
  else if c = '+' then plus()

let draw_list n = List.iter draw_elt (evolve n)
și apelul draw_list 10 ne va produce comenzile pentru desenarea unui dragon cu 10 nivele de detaliere. Comenzile rezultate pot fi inserate în structura unui fișier SVG, de exemplu:
<?xml version="1.0" encoding="UTF-8" standalone="no">
<svg
   xmlns:svg="http://www.w3.org/2000/svg"
   xmlns="http://www.w3.org/2000/svg"
   version="1.0"
   width="400"
   height="800"
   id="svg001">
  <defs
     id="defs001"/>
  <path
     d="M 200 400 h 8 v -8 ... RESTUL COMENZILOR ... h -8 v -8"
     style="fill:none;stroke:#0000ff;stroke-width:2px"
     id="path001"/>
</svg>
rezultând imaginea dorită.

În rezumat:

Acestea sunt principii generale. Respectarea lor ne ajută să programăm bine și frumos: să scriem programe corecte, ușor de înțeles, de adaptat și întreținut, oricare ar fi limbajul pe care îl alegem.
Marius Minea
Last modified: Mon Oct 4 15:0:00 EET 2010