Novantanove bottiglie di birra

Con la scorsa lezione abbiamo chiuso il discorso sulle funzioni e introdotto le variabili, sulle quali peraltro abbiamo detto quasi tutto quello che ci serve e dobbiamo solo investigarne i tipi. Pare inoltre che ci sarà parecchio movimento riguardo i rilasci di Processing quindi, se volete, continuate a fare un giro sulla sua pagina di download per vedere se sono uscite nuove versioni. Una volta che la 2.0 sarà uscita dalla beta e rilasciata ufficialmente, le cose dovrebbero calmarsi.

In ogni caso vi avevo promesso che avremo scoperto la ragione per cui abbiamo inventato i computer, e questa è presto detta: i computer sono bravissimi a fare molti conti velocemente e a ripeterli più e più volte senza stancarsi, e per questo uno dei cardini della programmazione è il ciclo. Niente battute, vi prego.

Cicli

Il nome non suggerisce molto ma i cicli sono tra i costrutti più diffusi ed utilizzati per fare qualsiasi cosa che abbia un minimo significato. Per capirci, quando l’altra volta ho detto che Processing chiamava di continuo la funzione draw(), la verità è che la chiamata alla funzione viene inserita, evidentemente insieme ad altre chiamate e istruzioni, in un ciclo che continua a ripetersi finché non decidiamo di interrompere l’esecuzione. Un ciclo di questo tipo si chiama infinito, per quanto impropriamente dato che c’è modo di interromperlo, ma non di soli cicli infiniti vive il programmatore.

Torniamo alla funzione che avrebbe dovuto elevare un numero ad una potenza. Per chi non se la ricorda, la funzione aveva una forma tipo

int power(int number, int exponent);

e, se ci ricordiamo rapidamente dalla matematica delle elementari — o medie? — una potenza si calcola moltiplicando per sé stesso il numero chiamato base tante volte quante ne indica l’esponente. Cioè se scriviamo $3^5$ il risultato è $3\times 3\times 3\times 3\times 3 = 243$. Ma allora è facile: basta prendere una variabile, metterci dentro la base, e moltiplicarla per la base cinque volte di fila.

int power(int number, int exponent) {
  int temp;
  temp = 1;

  for(int i = 0; i < exponent; i++) {
    temp = temp * number;
  }

  return temp;
}

Ormai sappiamo le prime due righe cosa fanno, quindi passiamo alla star del giorno: il ciclo for. Questo ciclo, che somiglia a una funzione ma non lo è, si usa normalmente quando sappiamo quante ripetizioni vogliamo eseguire. Tra parentesi tonde troviamo quella che sembrerebbe una lista di parametri, solo separati da punti e virgole, invece che da semplici virgole, e questo è perché quelle lì sono vere e proprie istruzioni. Nel 99.999% dei casi, quelle tre istruzioni sono del tutto analoghe a queste, ma c’è gente al mondo che sa quello che fa e quindi ci mette dentro cose improbabili, compreso il nulla, ma andiamo con calma.

Come prima cosa dichiariamo la variabile i intera e la inizializziamo a 0. Questa variabile ci servirà per contare quante volte è già stato eseguito il blocco di istruzioni contenuto tra parentesi graffe e alla prima esecuzione ovviamente il conteggio è zero. La seconda istruzione ci dice quando dobbiamo fermarci, e noi sappiamo che vogliamo moltiplicare la base per sé stessa tante volte quante ne dice l’esponente e quindi, assumendo che il contatore conti in avanti ad incrementi di una unità, diciamo che il ciclo deve continuare finché il contatore non è arrivato all’esponente. La terza istruzione è proprio quella che serve per mandare avanti il conteggio. Ammetto che i++ non sembri avere molto significato, men che meno quello di mandare avanti un contatore, ma in realtà è una scrittura legata alle istruzioni elementari dei microprocessori, e una tra queste era l’incremento di una unità. La scrittura i++ significa proprio “prendi la variabile i e poi incrementala di uno” [1].

All’interno del blocco che for ripeterà c’è una cosa che ai matematici fa sempre rabbrividire, ma è perché loro non sono abituati a vivere in un mondo in evoluzione nel tempo. La scrittura $x = x+1$ in matematica non ha alcun significato perché, se portiamo le $x$ dallo stesso lato otteniamo $x-x=1$, cioè $0 = 1$. Fortunatamente, in programmazione, un’espressione del tipo

temp = temp * number;

va vista in due tempi:

  1. si calcola temp * number col valore attuale di temp;
  2. si assegna a temp il valore appena calcolato, che al momento sta fluttuando in qualche parte della memoria nota solo al computer.

Il risultato netto è che abbiamo preso quello che c’era nella variabile temp, cioè all’inizio 1, e l’abbiamo moltiplicato per la base della potenza, diciamo per esempio 3. Risultato, 3. Fine della prima iterazione [2], contatore incrementato a 1. Alla seconda iterazione, in temp c’è 3, lo moltiplichiamo di nuovo per il contenuto di number, cioè 3, e in temp finisce 9. Fine della seconda iterazione, contatore incrementato a 2. Se fate i conti a manina ad un certo punto ci troviamo nella situazione in cui temp conterrà 243, il contatore i avrà contato 4 iterazioni, e supponiamo di aver passato come exponent il valore 5. Al termine del blocco il ciclo for provvede ad incrementare il contatore, che quindi varrà 5, e la condizione i < exponent, controllata ogni volta prima di cominciare una nuova esecuzione, diventa falsa, infatti ora i vale 5 che è proprio quello che avevamo passato come exponent: il ciclo si ferma, il programma riprende l’esecuzione dalla prima riga dopo il blocco del for [3].

Infine, come ultima istruzione, c’è una parola chiave che non avevamo ancora visto: return non è una funzione e quindi non richiede parentesi ma serve a far uscire il valore specificato dalla funzione. In questo caso facciamo uscire [4] il valore di temp e, dal momento che esce dallo stesso lato della funzione in cui è stato dichiarato, cioè sinistra, lo possiamo usare per assegnare una variabile. Per esempio potremmo scrivere

int p;
p = power(3, 5);

e in p troveremo il valore 243.

Non sempre sappiamo esattamente quante volte vogliamo ripetere un ciclo ma solo quando vogliamo interromperlo. Per esempio prendiamo un bollitore dell’acqua: lo mettiamo sul fuoco ma non sappiamo esattamente dopo quanti minuti spegnere il fuoco, però sappiamo che vogliamo far bollire l’acqua, e quindi vogliamo toglierlo dal fuoco quando la temperatura arriva a 100 gradi. Per questo esiste un altro tipo di ciclo, il while, che nel nostro caso si scriverebbe più o meno così:

float T = getWaterTemperature();

while(T < 100.0f) {
  doOtherThings();
  T = getWaterTemperature();
}

e si legge “finché il valore di T è minore di 100, esegui il blocco”. Infatti appena la temperatura raggiunge 100.0f gradi (o li supera), il ciclo termina l’esecuzione e… indovinato, il programma riprende da dopo il blocco. Attenzione che la condizione viene controllata prima di ogni esecuzione (come nel for, peraltro) e quindi, se per qualsiasi motivo la temperatura dell’acqua fosse già uguale o superiore a 100 gradi il ciclo non verrebbe mai eseguito. Nel caso volessimo invece che il blocco venga eseguito almeno una volta, esiste un altro costrutto, il do...while che si scrive

float T = getWaterTemperature();

do {
  doOtherThings();
  T = getWaterTemperature();
} while(T < 100.0f);

anche se io ho un trauma infantile da quando studiavo Pascal alle medie che aveva un terzo tipo di ciclo, chiamato repeat...until che invertiva il valore della condizione, e quindi ho preferito imparare ad usare bene il while liscio a cui, con gli opportuni accorgimenti, gli si fa fare anche il lavoro del do...while. Inoltre sono sicuro che i più acuti tra voi avranno notato a questo punto che il for non è altro che un while mascherato, e infatti il ciclo che abbiamo usato prima per calcolare l’elevamento a potenza si può riscrivere così:

int power(int number, int exponent) {
  int temp;
  temp = 1;

  int i = 0;
  while(i < exponent) {
    temp = temp * number;
    i++;
  }

  return temp;
}

e otteniamo lo stesso risultato, col solo svantaggio di aver lasciato in giro la variabile i nello scope della funzione e non all’interno del ciclo. Svantaggio minimo dato che disponiamo del for, ma è comunque bene conoscere l’equivalenza tra i due, anche perché in futuro potrebbe aiutarci a leggere un for particolarmente astruso, o a riscrivere un while in forma più compatta qualora sapessimo a priori quante iterazioni fargli fare. A proposito di iterazioni, fin’ora non ho mai menzionato il motivo per cui la variabile contatore si chiama i. Beh, non c’è un motivo, in realtà, perché vedremo tra poco come inserire un ciclo dentro l’altro per fare cose ancora più utili (e complicare la vita ai computazionisti che è sempre cosa buona e giusta) e quindi dovremo usare variabili diverse. Comunque, le variabili che fungono da contatori nei cicli for risentono del fatto che questi cicli vengono spesso usati per scorrere matrici, e gli indici delle matrici, in matematica, si chiamano tradizionalmente $i$, $j$, $k$ e così via. La cosa importante da ricordare è che le variabili devono dire qualcosa a noi, non per forza ai matematici, e quindi se abbiamo un buon motivo per usare un altro nome, tipo ricordarci a cosa si riferisce un certo contatore, usiamolo senza pensarci due volte.

Veniamo ora alla parte divertente che, tra l’altro, contiene una cosa che vedremo in maggior dettaglio la prossima lezione, quindi se non vi è chiarissima subito, portate pazienza qualche giorno.

int width, height;
float radius, fps;

void setup() {
  width = 600;
  height = 400;
  radius = 5.0f;
  fps = 30.0f;

  size(width, height);
  frameRate(fps);
  noStroke();
  colorMode(HSB, 360, 100, 100);
}

void draw() {
  float t, r, distance;

  background(0, 0, 0);
  t = frameCount/fps;

  for(int y = 10; y < height; y = y + 20) {
    for(int x = 10; x < width; x = x + 20) {
      distance = sqrt(pow(mouseX - x, 2) + pow(mouseY - y, 2));
      r = radius + 2*sin(x - 4*t) + 2*cos(y - 2*t);
      if(distance < 100.0f) {
        r = r + (100.0f - distance)/10.0f;
      }
      fill(map(x, 0.0f, width, 0.0f, 360.0f), distance, 100);
      ellipse(x, y, r, r);
    }
  }
}

Le prime due righe le conosciamo già, la funzione setup() capiamo facilmente quello che fa [5], e quindi passiamo direttamente alla funzione draw() che inizia dichiarando un po’ di variabili tra cui quella che terrà conto del tempo, come abbiamo già visto la volta scorsa, e colorando di nero lo sfondo.

Dopo aver calcolato quanti secondi sono trascorsi dall’inizio dell’esecuzione, ci buttiamo dritti nell’azione. Il primo ciclo for lavora su una variabile y che parte da 10 e viene incrementata di 20 finché è minore dell’altezza della finestra. In pratica ci stiamo muovendo in verticale di 20 unità in 20 unità a partire dalla coordinata y = 10. Immediatamente a seguire iniziamo un altro ciclo for che lavora su un’altra variabile x che parte da 10 e viene incrementata di 20 finché è minore della larghezza della finestra.

Calma e un passo alla volta. Entriamo nel primo for con la y = 10, entriamo immediatamente nel secondo for con x = 10. Questo secondo for arriva alla fine della prima iterazione, incrementa x di 20 e riprende l’iterazione successiva, e così via finché non arriva alla larghezza della finestra. Ebbene sì, abbiamo completato una riga, ma la parte migliore viene adesso perché una volta che il for più interno ha finito le sue iterazioni, tocca al for più esterno cominciare la sua seconda iterazione, con y incrementata di 20. A quel punto il for più interno ricomincia da x = 10 e va fino in fondo alla riga… mal di testa? A me ne sono venuti più di qualcuno quando cercavo di dare un senso ai cicli cosiddetti annidati e l’unico modo che ho trovato per uscirne sano è fare questo genere di conti a mano finché non ci si prende dimestichezza.

Dentro al doppio ciclo succede che disegnamo dei cerchietti (funzione ellipse()) nelle posizioni indicate dai due indici dei cicli. Questo significa che disegneremo righe e colonne di cerchietti con una sola riga di codice: visto perché abbiamo inventato i computer? Se ci fermassimo a disegnare i cerchietti però ci sarebbe ben poco di divertente e quindi ricominciamo dalla prima riga. Prima di tutto calcoliamo una distanza. Se vi ricordate un po’ di geometria elementare, e il teorema di Pitagora, vedete bene che stiamo calcolando la distanza tra il centro di ciascun cerchietto e la posizione del mouse. In particolare la funzione pow() eleva il primo parametro all’esponente indicato come secondo parametro, e la funzione sqrt() calcola la radice quadrata del suo parametro. In pratica abbiamo scritto $d = \sqrt{a^2 + b^2}$, con $a$ la distanza tra centro e mouse in ascissa, e $b$ la distanza tra centro e mouse in ordinata.

Immediatamente dopo calcoliamo il raggio dei cerchietti. Direte: ma non l’avevamo impostato prima di cominciare? Sì, ma per rendere le cose più interessanti ho pensato di far variare il raggio in base al tempo e alla posizione. Non vi sto a spiegare la formula perché avrebbe poco senso, voi come al solito potete smanacciare coi parametri o addirittura stravolgerla e vedere cosa succede.

E qui veniamo alla parte che vedremo nella prossima lezione. Se conoscete un po’ di inglese, “if” significa “se”, e quello che c’è tra parentesi sembra proprio una condizione simile a quelle che abbiamo usato nei cicli. Infatti il costrutto if serve proprio ad eseguire un blocco di codice se una certa condizione è vera, e questo è tutto quello che vi dirò a proposito per questa volta. Nel concreto, quello che facciamo qui è prendere il raggio del cerchietto che abbiamo appena calcolato e aggiungerci una certa quantità che dipende dalla distanza del suo centro dalla posizione del mouse. In questo caso questa quantità è più grande tanto più il mouse è vicino al centro, e quindi i cerchietti più vicini al mouse risulteranno mediamente più grandi. L’utilità dell’if è nell’applicare questo effetto solo ai cerchietti che si trovano entro un raggio di 100 unità dalla posizione del mouse.

Infine, con la funzione fill impostiamo il colore dei cerchietti. Più avanti spiegherò meglio le varie codifiche dei colori, per il momento vi basti sapere che non stiamo usando la RGB che già conosciamo, ma la HSB che usa una terna di numeri per indicare tonalità (hue), saturazione (saturation) e luminosità (brightness). Tradizionalmente questi parametri variano tra 0 e 360 nel caso della tonalità e tra 0 e 100 negli altri due casi. Noi impostiamo la luminosità sempre a 100 così abbiamo dei bei colori brillanti, e la saturazione la rendiamo dipendente dalla distanza. Quando la saturazione è 0, il colore diventa bianco, quando la saturazione cresce, il colore diventa… colorato [6]. Vediamo infine l’ultima funzione della giornata, map(), che serve prendere un numero che varia in un certo intervallo e ricalcolarlo perché vari in un altro intervallo. Il primo parametro è il numero che vogliamo ricalcolare, i secondi due sono i due estremi, inferiore e superiore, dell’intervallo di partenza, e gli ultimi due sono gli estremi dell’intervallo di destinazione. In questo caso noi sappiamo che la variabile x può variare tra 0 e la massima larghezza della finestra e vogliamo ricalcolarla in modo che vari tra 0 e 360 così da poter selezionare una tonalità. Eseguite e godetevi lo spettacolo.

Ricapitolando, in questa lezione abbiamo visto come ripetere molte volte le stesse istruzioni variandone eventualmente i parametri, che è una cosa utilissima e che useremo molto di frequente. Come al solito voi smanacciate i parametri, e questa volta potete smanacciare anche un po’ di più. Prendete confidenza coi cicli e con le formule di geometria che sono importanti e, se volete mettervi alla prova, scrivete un programma che stampi il testo della canzone “99 bottles of beer” usando un ciclo a scelta [7]. La prossima volta vedremo come i computer riescono a prendere decisioni con quelli che si chiamano costrutti condizionali.

  1. Quel “poi” suona male ma c’è un motivo: la scrittura ++i significa “incrementa la variabile i di uno e poi usala”, che detta così sembra una sfumatura, e nell’ambito di un ciclo for ha lo stesso significato, ma se scrivessimo int j = i++; avremmo che i vale 1 più di j, mentre se scrivessimo int j = ++i avremmo che le due variabili valgono uno più di quanto valesse prima i. Fico, eh? Un po’ di pratica scaccia via ogni dubbio.
  2. Un’esecuzione completa del blocco associato ad un ciclo si chiama iterazione.
  3. “Blocco del ciclo for è un po’ lunga da scrivere e, di norma, il blocco e il ciclo si intendono come un oggetto unico, quindi quando d’ora in poi dirò “il ciclo for intenderò l’insieme delle istruzioni tra parentesi tonde e quelle tra parentesi quadre.
  4. In gergo si dice “restituire” o, italianizzando maldestramente la parola, “ritornare”.
  5. Ci sono due funzioni che non conosciamo e la funzione colorMode() la vedremo quando parleremo delle codifiche dei colori, per ora facciamo finta di niente e proseguiamo.
  6. I più svegli tra voi avranno già notato che la variabile distance può tranquillamente assumere valori superiori a 100, ma Processing è abbastanza furbo da troncare al valore massimo ogni valore che lo supera, e quindi siamo tranquilli. Occhio che questa è un’assunzione da non fare mai: io ho guardato nella documentazione per essere sicuro di non fare danni. Questa è una cosa che vale la pena tenere sempre bene a mente: la documentazione salva la vita. Mai cercare di indovinare perché, come diceva la mia prof di matematica delle superiori, le cose spesso vengono giuste anche per sbaglio, quindi noi crediamo di aver fatto tutto bene e invece, sotto sotto, abbiamo fatto un errore che può rivelarsi fatale in futuro, magari perché gli sviluppatori di una certa libreria cambiano il comportamento in quei casi che noi andiamo erroneamente a toccare.
  7. Hint: come è vero che i++ incrementa il valore di i, è vero che i-- lo decrementa, ma per esercizio potreste anche non usare il decremento.

Commenti

Lorenzo Breda » 

Che chi usa cicli for esotici sappia cosa faccia è tutto da dimostrare.

Rispondi