Programmi che fanno girare le palle

NOTA: Prima di cominciare vi segnalo che è uscita la versione 2.0b3 di Processing, cioè la terza beta. Potete scaricarla e usarla al posto della prima beta, che non fa mai male. Ma torniamo a noi.

L’ultima volta eravamo rimasti alle funzioni e, se ricordate bene, c’era un’ultima cosa che le funzioni fanno ma era troppo noiosa e quindi ci siamo messi a disegnare formine colorate che mi sembra un’attività più interessante.

Prima di finire il discorso sulle funzioni però è bene iniziarne un altro. Se riprendiamo il codice che abbiamo scritto la scorsa volta vediamo che abbiamo passato alle funzioni direttamente i valori che volevamo usare come parametri. A volte capita però di dover passare alle funzioni parametri di volta in volta diversi e allora cosa facciamo? Ogni volta modifichiamo i valori e ricompiliamo? Oppure prepariamo le chiamate a funzione [1] con tutti i possibili valori che potremmo dover usare e poi scegliamo la chiamata giusta con qualche magia? Niente di tutto ciò: per fortuna chi ha inventato i linguaggi di programmazione ci ha visto lungo e ha inventato anche

Le variabili

Le variabili, per essere capite, non richiedono una profonda conoscenza dei meccanismi interni dei computer ma è comunque una buona idea sapere di cosa stiamo parlando: la memoria. I computer hanno svariati tipi di memorie al loro interno, ma principalmente queste sono di due tipi: volatili e permanenti. Le memorie permanenti sono quelle su cui possiamo salvare i dati, spegnere il computer, riaccenderlo ed essere sicuri che i dati sono ancora lì [2], tipo l’hard disk, i dischi ottici e le chiavette USB. Le memorie volatili sono quelle che, quando si spegne il computer, perdono tutto il loro contenuto e alla riaccensione non è più possibile recuperarlo [3]. Tra le memorie volatili, quella che ci interessa in questo caso è la RAM, che sta per Random Access Memory, un acronimo che crea sempre qualche confusione ai non addetti ai lavori dato che non è che il computer va a leggere e scrivere a casaccio in questa memoria: lo fa con un certo metodo, e questo metodo sono le variabili [4].

Per capire il senso delle variabili dobbiamo immaginare la RAM come un gigantesco casellario in cui ogni casella ha un numero univoco e una capacità uguale a tutte le altre caselle. In queste caselle possiamo memorizzare dei dati che possono essere dei più svariati tipi: da semplici numeri interi, che occupano in genere una casella, a documenti più complessi che in una casella sicuramente non ci stanno e quindi vengono spezzettati in molte. Diciamo almeno qualche centinaio di migliaia.

Il punto però è che, per un computer, dentro a queste caselle ci sono solo numeri interi compresi tra 0 a tantissimo [5], e il senso a questi numeri lo diamo noi. Per esempio se vogliamo dire al computer che in una cella c’è scritto il numero 5 dovremo dirgli che in quella cella c’è un numero intero, mentre se vogliamo dirgli che dentro c’è il numero 10,983 dovremo dirgli che c’è un numero con la virgola. Se vogliamo memorizzare la scritta "Hello, world!" dovremo dirgli che dentro c’è una stringa e via così. La bellezza delle variabili, oltre a permetterci di memorizzare (temporaneamente) dei dati è che ci permette di usare nomi facili da ricordare, e soprattutto che se un certo dato sta sparso in più di una casella, e vedremo che è un caso piuttosto frequente ed utile, ci permette di far riferimento a quel dato come un blocco unico e non come ad un insieme di celle, sebbene ci saranno casi in cui sarà utile poter guardare singolarmente in ciascuna cella.

Bene, da questo momento in poi non parleremo (quasi) più di celle ma solo di dati di un certo tipo. Una variabile si compone di un tipo e di un identificatore (o nome, per gli amici):

int Pippo;

qui sopra abbiamo dichiarato che vogliamo immagazzinare un numero (int è la parola chiave che identifica gli interi, dal latino integer che poi in inglese è uguale ma vabbè) e che se vogliamo usare, nella migliore tradizione informatica, il nome Pippo per accedere a quel dato [6]. Ora come ora non abbiamo idea di cosa ci sia dentro la variabile: è estremamente probabile che Processing abbia provveduto a metterci uno zero ma non ci dobbiamo mai contare, e quindi la cosa più intelligente da fare a questo punto è inizializzarla, cioè metterci qualcosa dentro:

int Pippo;
Pippo = 42;

Notiamo che la prima riga specifica il tipo della variabile (int) ma la seconda no. Una volta che abbiamo dichiarato una variabile infatti non è più necessario ripetere di che tipo si tratta, anzi: è sbagliato, perché a rigore di logica significherebbe dichiarare una nuova variabile con lo stesso nome e questo non è consentito perché altrimenti ci confonderemmo inutilmente le idee, anche se non cambiamo il tipo. Ci sarebbe un modo per risparmiarci un po’ di fatica

int Pippo = 42;

cioè dichiarare e inizializzare la variabile sulla stessa riga, ed è una cosa piuttosto comune e che in futuro faremo molto spesso, ma per svariate ragioni all’inizio è bene tenere in ordine e separate le due cose, cioè prima si dichiara e poi si inizializza [7].

I tipi di dato che possiamo infilare nelle variabili sono virtualmente infiniti: ce ne sono un certo numero forniti dal linguaggio che si chiamano nativi o primitivi, e ce ne sono molti di più che possiamo creare noi a nostro piacimento, mettendoci dentro quello che ci fa più comodo, e questi si chiamano tradizionalmente astratti anche se di astratto spesso non hanno proprio niente [8] e servono per facilitarci nel lavoro ed estendere le capacità espressive del linguaggio che abbiamo scelto di usare [9]. La parte più interessante è che quasi tutti i linguaggi di un certo livello vengono forniti con un insieme di librerie standard che contengono dei tipi di dato astratti prefabbricati, e Processing non fa eccezione. Non mi metterò ad elencare qui tutti i tipi di dati primitivi di Processing (che poi è Java) ma li introdurrò uno ad uno man mano che li useremo. I più curiosi possono guardare questa lista ma non rompetevici la testa se non capite qualcosa. In più Processing introduce alcuni tipi astratti che useremo sicuramente e che non trovate in quella lista, quindi tanto vale.

Tornando a noi, è ora di chiudere il discorso sulle funzioni. Quando alle superiori si studiano le funzioni in matematica [10] si dice che queste sono degli oggetti che prendono uno o più numeri e sputano fuori un risultato. Ebbene, le funzioni che interessano a noi fanno esattamente questo: prendono dei parametri, ci fanno qualcosa e restituiscono un risultato. Ora, il concetto di “restituire un risultato” in informatica è un po’ vago perché la definizione di risultato è molto ampia e “niente” è un risultato perfettamente valido, ma andiamo con ordine e facciamo un esempio facile. Supponiamo di voler scrivere una funzione che eleva a potenza. Questa deve prendere come parametri un numero e un esponente, che per semplicità faremo finta che siano entrambi interi, e restituire un numero a sua volta intero. Una funzione che ha questo comportamento si dichiara così [11]:

int power(int number, int exponent);

per cui potremmo scrivere qualcosa tipo

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

e dentro alla variabile result sarà contenuto il numero 243. Ora, se provaste ad incollare quelle righe in Processing, il giochino non funzionerebbe perché non esiste una funzione dichiarata in quel modo [12]. Non ci resterebbe che implementarcela, ma per farlo dovremmo introdurre uno dei motivi principali per cui abbiamo inventato i computer, e prometto che lo faremo presto ma non ora.

Tuttavia per chiudere definitivamente il capitolo funzioni dobbiamo vedere come si implementa concretamente una funzione, cioè come si dice al computer cosa vogliamo che faccia quando la chiamiamo dal nostro codice. Fortunatamente Processing ci aiuta perché esistono un certo numero di funzioni speciali che si aspetta di trovare implementate e, se non le trova, fa finta di niente [13].

Fin’ora abbiamo scritto codice sparso nella finestrona bianca di Processing, ma quello che succedeva dietro le quinte è che Processing si accorgeva che mancava la prima funzione speciale e quindi prendeva il nostro codice e lo metteva dentro alla sua versione di questa funzione che si chiama setup() e viene chiamata come primissima cosa quando eseguiamo uno sketch. Uno sketch che si rispetti è composto di molte funzioni, alcune speciali di Processing e altre che eventualmente scriveremo noi, quindi questo è un buon momento per cominciare a comportarci bene e fare le cose nel modo giusto

void setup() {
  println("Hello, world!");
}

Indovinare cosa succederà quando eseguiremo questo codice non è difficile, quello che è un po’ più incasinato è capire cosa abbiamo scritto. Per prima cosa c’è una parola che non abbiamo ancora incontrato: void è un tipo di dato, come suggerisce la sua posizione prima del nome di una funzione, e sta a significare un dato privo di tipo, o di tipo vuoto [14]. È un tipo che si usa praticamente solo in questo contesto [15] e sta a significare che la funzione non restituisce alcun dato. Alla fine della riga, dopo la lista di parametri che in questo caso è vuota, c’è una parentesi graffa aperta. L’ultima riga è composta di una parentesi graffa chiusa, e tra queste due parentesi è racchiusa la lista di istruzioni che il computer dovrà eseguire quando setup() verrà chiamata da Processing.

Una cosa da notare: dopo la parentesi graffa chiusa non c’è il punto e virgola. Sebbene anche l’implementazione di una funzione sia una frase legittima, non è necessario terminarla con un punto e virgola. Possiamo metterlo, se vogliamo, ma non è necessario e nessuno lo fa mai. Il motivo è semplice, in realtà: le parentesi graffe delimitano quello che si chiama un blocco di istruzioni, e un blocco ha un significato simile ad un paragrafo: terminiamo tutte le frasi con un punto, ma non scriviamo niente per segnalare la fine di un paragrafo, ci limitiamo ad andare a capo, magari lasciare una riga vuota, e fine.

Un blocco inoltre introduce e delimita un altro concetto, lo scope, che possiamo tradurre come ambito o campo. Lo scope ha l’onere e l’onore di non far sapere all’esterno di esso cosa racchiude al suo interno. Per esempio se dichiariamo una variabile all’interno di un blocco, il suo scope è l’interno del blocco, ma appena usciamo dal blocco quella variabile non esiste più, che è una cosa estremamente comoda ma che può creare qualche confusione, specie quando lo scope non è particolarmente esplicito come vedremo nella prossima lezione. Comunque, il succo è che se scriviamo qualcosa tipo

int Pippo;
 
void setup() {
  int Pluto;
  Pippo = 10;
  Pluto = 20;
}
 
void awesomeFunction() {
  Pippo = 30;
  Pluto = 40;
}

il risultato è che Processing ci dirà qualcosa tipo Cannot find anything named "Pluto" perché Pluto è dichiarata all’interno dello scope della funzione setup() e quindi chiunque non condivida lo stesso scope di setup() non vi può accedere. Al contrario, entrambe le funzioni possono accedere alla variabile Pippo perché è dichiarata all’interno dello scope dentro il quale sono dichiarate le due funzioni. Quindi la regola è: da dentro uno scope si può guardare fuori, da fuori di uno scope non si può guardarci dentro, e quello che è dentro, vive e muore all’interno. Non sbatteteci la testa troppo, è un concetto talmente ubiquo che vi diventerà familiare in poco tempo con la pratica.

Per oggi abbiamo fatto abbastanza teoria, è ora di uscire a giocare un po’ [16] con questo sketch:

int centerX, centerY;
float fps;
 
void setup() {
  centerX = 200;
  centerY = 200;
  fps = 60.0f;
 
  size(2*centerX, 2*centerY);
  frameRate(fps);
}
 
void draw() {
  float t;
 
  background(0, 0, 0);
  fill(255, 255, 255);
 
  t = frameCount/fps;
  ellipse(centerX + 100*sin(2*PI*t), centerY + 100*cos(2*PI*t), 60, 60);
}

La prima riga è abbastanza chiara: dichiariamo due variabili di tipo intero. A occhio, sono le coordinate x e y di un centro nel piano cartesiano. Sento già i primi stridori di denti, e quindi è bene mettere le mani avanti: quando si viene a disegnare attraverso un linguaggio di programmazione, l’unica strada possibile è farlo attraverso la geometria. Per fortuna la geometria che useremo noi è quella che si studia alle elementari, il che dovrebbe facilitare di molto la comprensione. Comunque quello che facciamo in questa lezione non è complicato affatto, quindi procediamo.

La seconda riga dichiara una variabile di nome fps (che sta per frames per second, fotogrammi al secondo, di cui vedremo il significato tra pochissimo) e di tipo float. Questo tipo di dato, abbreviazione di floating point si riferisce ai cosiddetti numeri con virgola mobile cioè, senza tanti fronzoli, i numeri decimali, quelli con la virgola. Se avete sbirciato tra i tipi primitivi, avrete notato che in realtà ci sono due tipi che si riferiscono ai numeri con la virgola: i numeri a singola e doppia precisione. Per quello che ci interessa al momento, la cosa non ha alcuna rilevanza, valeva solo la pena menzionarlo perché float è il tipo a precisione singola e double è il tipo a precisione doppia, ma noi useremo quelli a precisione singola, primo perché non ci fa differenza, secondo perché leggere double in giro può creare attimi di panico finché la differenza non ci si è stampata a caldo nel cervello, e in effetti double non è che faccia pensare naturalmente a numeri con la virgola, mentre float sì, se ci ricordiamo che è l’abbreviazione di floating point, virgola mobile.

Procediamo e incontriamo la nostra amica setup() che fa un certo numero di cose: inizializza le coordinate del centro, e inizializza il numero di fotogrammi al secondo. Importante è ricordarsi che quando si lavora con i float dobbiamo specificare la parte decimale (quella dopo il punto, che in notazione scientifica è la nostra virgola) anche se è zero, e che alla fine dobbiamo aggiungere una f minuscola per differenziarli dai double che non vogliono niente. In questo caso avremmo potuto farne a meno perché per il compilatore [17] è chiaro che intendevamo usare un float, ma in alcuni casi potrebbe non esserlo e quindi tanto vale abituarsi a fare le cose per bene e usare la f minuscola ogni volta che ci va e non sbagliamo di sicuro [18]. Alla fine del blocco incontriamo due funzioni. La prima, size() [19], già la conosciamo e serve per ridimensionare la finestra grafica del nostro sketch, e come parametri mettiamo le coordinate del centro moltiplicate per 2, in modo che quello diventi proprio il centro. La seconda funzione, frameRate(), è nuova e serve a dire a Processing ogni quante volte vogliamo che la funzione draw() definita più sotto venga chiamata ogni secondo. Eh, Processing funziona così: una volta eseguita setup(), comincia a chiamare ripetutamente draw() — e ogni chiamata a questa funzione si chiama “fotogramma”, in Processing — in cui noi andremo a mettere tutte quelle istruzioni che hanno a che fare con il tempo che passa e con gli eventi che potrebbero verificarsi in futuro e che vorremo gestire [20].

Non ci resta che esaminare il contenuto di draw() che inizia con la dichiarazione della variabile locale [21] float di nome t che sta per tempo e che vedremo al momento opportuno, seguita dalla chiamata background(0, 0, 0) [22]. I parametri che questa funzione si aspetta sono le componenti codificate del colore di cui vogliamo colorare lo sfondo della finestra. In questo caso si aspetta una codifica RGB [23] e i tre zeri corrispondono al nero. In maniera analoga la successiva chiamata alla funzione fill() imposta il colore con cui il prossimo comando che disegna una forma riempirà la forma stessa. In questo caso ho passato la codifica RGB del bianco.

La quarta riga finalmente inizializza la variabile t con l’espressione frameCount/fps. frameCount è una variabile speciale fornitaci da Processing che ci dice a quale fotogramma siamo arrivati, e in pratica è un contatore che parte da 0 e cresce di 1 ogni volta che Processing chiama draw(). Sappiamo che in un secondo ci sono tanti fotogrammi quanti ne abbiamo indicati in fps e quindi in effetti la variabile t contiene, per ogni fotogramma, quanto tempo in secondi è passato dall’inizio dell’esecuzione. Questa cosa ci torna supercomoda immediatamente perché così possiamo applicare le formule di fisica che già conosciamo [24] per far compiere alla nostra ellisse (i cui raggi sono uguali e quindi è un cerchio) una rotazione al secondo ad una certa distanza dal centro che abbiamo specificato all’inizio del programma.

Provare per credere. Fico, eh? Ancora più fico sarebbe se ora provaste a smanacciare [25] un po’ i parametri, tanto per prenderci confidenza: per esempio potreste provare a far girare il cerchietto una volta ogni due secondi, o ogni mezzo, oppure a farlo girare in senso antiorario, o cambiate dimensioni, raggio di rotazione, colore del disco… insomma, un po’ di fantasia!

Ammetto che è stata una lezione lunga ma abbiamo imparato un’altra notevole fetta di cose. Inoltre so di aver tralasciato molte precisazioni che normalmente si fanno, tipo il perché e il percome i parametri godono di così tanta licenza espressiva tipo operazioni tra numeri e variabili e roba così, ma il motivo è semplice: probabilmente non ci avete nemmeno fatto caso, il che vuol dire che vi è sembrato naturale, e molte cose che in realtà sono ragionevolmente naturali spesso vengono fin troppo analizzate dai corsi di programmazione per principianti. Qua lo scopo è imparare le cose senza farsi venire troppi mal di testa, e quindi se sentite l’urgenza di avere spiegazioni su cose particolari sarò felice di darvele, basta che le chiediate nei commenti.

La prossima volta scopriremo il vero e unico motivo per cui abbiamo inventato i computer. Intanto voi smanacciate i parametri e state tranquilli: non si diventa ciechi [26].

  1. Quando si usa una funzione si dice che la si “chiama”, o “call” in inglese.
  2. A meno di catastrofi termonucleari nel frattempo.
  3. Non è del tutto esatto, ma queste cose lasciamole ai malintenzionati.
  4. Non è proprio così ma se cominciamo questo discorso vi ritrovate tre esami più vicini ad una laurea in ingegneria informatica.
  5. Dove tantissimo può variare tra 255 e 18.446.744.073.709.551.615, e in alcuni casi molto speciali anche oltre.
  6. Non c’è alcun motivo per cui l’iniziale è maiuscola ma ci sono alcune convenzioni che vedremo quando sarà ora, per il momento l’importante è che il nome contenga solo lettere, cifre e il carattere “_”, ma all’inizio non può esserci una cifra.
  7. Forse l’unica cosa che ho apprezzato davvero di Pascal.
  8. Sempre che chi li ha creati avesse un minimo di cognizione di causa…
  9. Ci sono casi in cui questo discorso non ha senso e non è possibile ma questi non sono casi che ci interessano.
  10. E qui sento già le prime urla di dolore e disperazione.
  11. Perdonate ma da questo momento comincerò a scrivere codice in inglese per semplicità e consuetudine.
  12. Esiste una versione analoga ma un po’ più complicata di cui parleremo quando ci servirà.
  13. Non è che fa finta di niente, ma qui ci avventuriamo in terreni perigliosi e quindi rivedremo questa cosa parecchio più in là.
  14. void significa nullo, vuoto, privo, e molte altre sfumature di niente.
  15. Altri linguaggi lo usano in altri contesti e con altri significati ma sono casi talmente speciali che a mala pena serve citarli.
  16. E ovviamente per “uscire a giocare un po’” non intendo prendere la palla e correre in giardino.
  17. Mi rendo conto che non ho mai detto cosa sia un compilatore, ma per il momento vi basti sapere che i computer non sanno leggere parole ma solo interpretare numeri, e il compilatore è quel programma che traduce le parole in numeri.
  18. Il fatto è che quando un computer sembra sbagliare, è estremamente più probabile, quasi certo, che l’errore l’ha compiuto un umano, e imparare a fare le cose con un certo metodo ci faciliterà nel momento in cui dovremo scovare i nostri o gli altrui errori.
  19. Quando ci riferiamo a funzioni in un discorso possiamo sempre omettere tipo ritornato e parametri, a meno che non siano fondamentali a capire il discorso, ma in ogni caso non scordiamo le parentesi che sono quelle che dicono “funzione!”.
  20. È importante capire che non tutti i programmi funzionano così: molti utili programmi di cui probabilmente non conosciamo l’esistenza partono, fanno le loro cose, e terminano, ma praticamente tutte le applicazioni a finestre a cui siamo abituati funziona in modo del tutto analogo a Processing, e dato che Processing è un ambiente per artisti audiovisuali, è giusto che lui si comporti così.
  21. Si dice “locale” per intendere “all’interno dello scope in cui è dichiarata” e la differenza è con lo scope “globale” che è quello che comprende tutto lo sketch: per esempio le tre variabili che abbiamo dichiarato all’inizio sono “globali” perché possono essere viste e modificate da qualunque punto dello sketch.
  22. Visto? Qui ha senso riportare anche la lista dei parametri.
  23. Niente paura: delle codifiche dei colori parleremo presto.
  24. Per i wannabe nerd: quando $t$ indica il tempo in secondi, la funzione $ \cos(2\pi t) $ compie un’oscillazione tra 1, -1 e di nuovo a 1 in un secondo. La funzione $\sin(2\pi t)$ si comporta in modo del tutto analogo solo che parte da 0, sale a 1, piomba a -1 e torna a 0 prima di ricominciare. Funzioni di questo genere si chiamano periodiche e il tempo che ci mettono a compiere il ciclo si chiama periodo. Ora potete andare a spaventare i vostri colleghi filosofi.
  25. Termine tecnico.
  26. Sempre che stiate seduti dritti sulla sedia e teniate il monitor a circa 80 centimetri dagli occhi.

Commenti

pikkio » 

Visto che l’esperimento sta avendo successo, ti suggerirei di farne una sezione dedicata del sito, separata rispetto al Blog. Ho in mente le serie “Dive into…” di Mark Pilgrim, hanno l’aria di un angolo speciale del web.

Andrea Franceschini » 

Avevo già in mente una cosa del genere, e prima o poi la farò, ma già scrivere una lezione mi prende due-tre giorni abbondanti, figurati tradurre in altre due lingue quelle che ho già scritto :)

Rispondi