Funzioni personalizzate

Iniziamo con un nuovo importante capitolo nel nostro percorso di introduzione alla programmazione con Processing. Se state seguendo questa serie di post dall’inizio, arrivati a questo punto vi sarete resi conto che negli esempi che ho proposto ho sempre cercato di seguire due regole auree della programmazione:

  1. scrivere il minor numero di righe di codice possibile
  2. scrivere codice semplice da leggere e interpretare.

Per quanto riguarda il primo punto, non penso che i programmatori siano pigri ma, al contrario, mi piace pensare che amino arrivare dritti al punto, senza perdersi in fronzoli o distrazioni.

In merito alla leggibilità, capirete quanto è importante scrivere bene il proprio codice quando vi capiterà di dover rimettere mano a dei progetti realizzati qualche mese o, addirittura, qualche anno prima.

Come anticipavo nell’incipit, con questo post cominceremo  un percorso che ci porterà a capire e a utilizzare la programmazione orientata agli oggetti (OOP), uno dei paradigmi fondamentali della programmazione moderna.

Un mondo di funzioni

Fin dal primo post abbiamo parlato di funzioni e, con il susseguirsi degli articoli, ne abbiamo utilizzate parecchie. Abbiamo imparato che queste funzioni ci permettono, ad esempio, di impostare la grandezza della finestra di lavoro oppure di disegnare forme geometriche.

Alcune di esse hanno bisogno di uno o più parametri per funzionare correttamente, penso, ad esempio, a fill() altre, invece, devono semplicemente essere richiamate e non necessitano di ulteriori dati come setup() o draw().

I creatori di Processing hanno già inserito all’interno del linguaggio una serie di funzioni per rendere il linguaggio semplice da utilizzare. Provate a pensare a quanto sarebbe difficile disegnare un cerchio senza avere a disposizione la funzione ellipse().

Come per le variabili built-in, le parole che identificano le funzioni già inserite all’interno di Processing sono riconoscibili perché vengono evidenziate automaticamente. Ovviamente è possibile creare delle funzioni personalizzate a patto che il nome scelto non sia già riservato da una funzione built-in.

Modularità e riusabilità

Il numero di righe di codice di un programma determina la sua complessità; cominciare a utilizzare funzioni personalizzate e, come vedremo tra qualche settimana, gli oggetti ci permette di rendere il codice modulare e riusabile.

Perché questo punto è molto importante? Un programma modulare è facile da debuggare e da modificare e/o riadattare alle proprie esigenze. Vi ricordate la differenza tra un programma con parametri hard-coded e lo stesso programma scritto con le variabili?

Il principio è lo stesso ed è come dividere il contenuto di un manuale in capitoli: grazie alla modularità delle funzioni possiamo trovare subito le porzioni di codice che ci interessa leggere o modificare.

In merito alla riusabilità: se scriviamo delle funzioni complesse, possiamo tranquillamente copiarle e incollarle in un nuovo progetto senza perdere tempo a riscriverle da capo con un notevole risparmio di tempo e risorse.

Return e void

Arriviamo al punto: come si scrive una funzione? Innanzitutto dobbiamo capire se la nostra funzione è pensata per restituire un valore oppure no. Facciamo un esempio per ciascun caso:

  1. Una funzione che, ogni volta che viene richiamata, mi disegni un fiore sullo schermo.
  2. Una funzione matematica che aggiunga 5 a ogni numero che viene utilizzato come parametro.

Nel primo caso la funzione non restituisce alcun valore: disegna semplicemente un fiore sullo schermo. In questa funzione potremmo comunque passare dei parametri come, ad esempio, la posizione iniziale del fiore ma non ci verrà restituito nessun valore né numerico né di testo dalla funzione stessa.

Nel secondo caso, invece, utilizziamo una funzione proprio per ricevere in cambio un numero.

Le due parole magiche che dovrò utilizzare in Processing sono voidreturn.

La nostra prima funzione: Flower

void setup() {
  size(700, 700);
  background(255);  
}

void draw() {
  flower(width/2, height/2);
}

void flower(int posizioneX, int posizioneY) {
  noStroke();
  fill(0, 255, 0);
  rectMode(CENTER);
  rect(posizioneX, posizioneY+100, 25, 100);
  fill(255, 0, 255);
  ellipse(posizioneX-50, posizioneY, 70, 70);
  ellipse(posizioneX, posizioneY-50, 70, 70);
  ellipse(posizioneX+50, posizioneY, 70, 70);
  ellipse(posizioneX, posizioneY+50, 70, 70);
  fill(255, 255, 0);
  ellipse(posizioneX, posizioneY, 50, 50);
  noFill();
}

Ecco il risultato:Funzioni personalizzate: flower

La parte di codice che ci interessa parte dalla linea 10:

void flower(int posizioneX, int posizioneY) {

Sto dicendo a Processing di creare una funzione che non restituirà nessun parametro (void) di nome flower che accetta due parametri in input di tipo integer: posizioneX posizioneY. La parentesi graffa alla fine mi serve per aprire il blocco di codice della funzione.

Nelle linee successive disegno il fiore utilizzando più volte la funzione ellipse per i petali e il pistillo e un rect per il gambo. I parametri posizioneXposizioneY vengono richiamati per impostare correttamente la posizione del fiore nella finestra.

Con la parentesi graffa finale, chiudo il blocco di codice della funzione.

Una volta scritta la funzione, è sufficiente richiamarla in setup() o in draw() passandogli le due variabili di tipo integer che la funzione si aspetta di ricevere.

Ora se io volessi disegnare più fiori all’interno della finestra, sarebbe sufficiente richiamare la funzione flower il numero di volte necessario:

void setup() {
  size(700, 700);
  background(255);  
}

void draw() {
  flower(width/2, height/2);
  flower(100, 200);
  flower(500, 300);
  flower(100, 600);
}

void flower(int posizioneX, int posizioneY) {
  noStroke();
  fill(0, 255, 0);
  rectMode(CENTER);
  rect(posizioneX, posizioneY+100, 25, 100);
  fill(255, 0, 255);
  ellipse(posizioneX-50, posizioneY, 70, 70);
  ellipse(posizioneX, posizioneY-50, 70, 70);
  ellipse(posizioneX+50, posizioneY, 70, 70);
  ellipse(posizioneX, posizioneY+50, 70, 70);
  fill(255, 255, 0);
  ellipse(posizioneX, posizioneY, 50, 50);
  noFill();
}

Funzioni personalizzate in Processing

Se volessi creare un prato pieno di fiori, potrei inserire la funzione all’interno di un ciclo e, ovviamente, potrei aggiungere dei parametri ulteriori per creare, ad esempio, fiori di colore e grandezza diversi:

void setup() {
  size(700, 700);
  background(255);
  for(int i = 0; i < 20; i++) {
    flower(int(random(width)), int(random(height)), color(random(255), random(255), random(255)), int(random(60, 100)));
  }
}

void draw() {
  
}

void flower(int posizioneX, int posizioneY, color colore, int petali) {
  noStroke();
  fill(0, 255, 0);
  rectMode(CENTER);
  rect(posizioneX, posizioneY+100, 25, 100);
  fill(colore);
  ellipse(posizioneX-50, posizioneY, petali, petali);
  ellipse(posizioneX, posizioneY-50, petali, petali);
  ellipse(posizioneX+50, posizioneY, petali, petali);
  ellipse(posizioneX, posizioneY+50, petali, petali);
  fill(255, 255, 0);
  ellipse(posizioneX, posizioneY, 50, 50);
  noFill();
}

Un campo di fiori

Funzioni che restituiscono valori

Passiamo ora al secondo esempio:

void setup() {
  println(aggiungiCinque(10));
}

void draw() {
}

int aggiungiCinque(int numero) {
  return numero+5;
}

Abbiamo creato una funzione che restituisce un tipo di dato integer chiamata aggiungiCinque che accetta un parametro di tipo integer. Con la parola chiave return restituiamo il valore passato come parametro a cui abbiamo sommato 5;

Nella funzione setup() abbiamo chiamato la nostra funzione aggiungiCinque(10) passando il valore 10. Nella console, ci verrà stampato, correttamente, il valore 15.

È di fondamentale importanza indicare correttamente sia il tipo di valore che la funzione restituirà in output sia quello che può accettare in input. Se, infatti, passassimo il valore 10.5, un float, Processing ci restituirebbe un errore come nell'immagine:

Errore in input

Modificando la funzione per accettare un float ma lasciando la restituzione di un parametro integer otteniamo un altro tipo di errore sul return:

Errore sull'output

Loop II: for e nesting

Come accennato nel post precedente, esistono due tipologie di loop: quelli con while e quelli con for. Oggi ci concentreremo su questi ultimi che, come dicevo, sono quelli che preferisco utilizzare.

Ripassiamo velocemente come funziona il ciclo while: questa tipologia di loop esegue un blocco di codice finché la condizione prevista tra parentesi è true.

while(condizione) {
 // Blocco di codice
}

Il motivo per cui io non amo utilizzare il while nasce dal fatto che la variabile deve essere dichiarata e inizializzata all’esterno del loop, verificata nella condizione e incrementata all’interno del loop.

La stessa variabile viene quindi utilizzata in punti diversi del codice e questo, nel caso di programmi piuttosto complessi, può creare non poca confusione.

For

Per ovviare al problema sopra descritto, ci affidiamo ai cicli for dove l’inizializzazione, la verifica e l’incremento della variabile “contatore” avviene all’interno della condizione.

Riprendiamo l’esempio del post precedente e riscriviamolo utilizzando il for:

void setup() {
  size(700, 500);
  background(255);
  for(int y = 10; y < height; y+=10) {
    line(100, y, 600, y);
  }
}

void draw() {
}

Il codice ora risulta più pulito e più facile da leggere: in una sola riga abbiamo iniziato il ciclo, dichiarato e inizializzato la variabile contatore int y = 10; posto la condizione y < height; e, infine, incrementato la variabile di 10 a ogni ciclo: y += 10. È importante sottolineare che questi tre blocchi devono essere separati dal punto e virgola, come mostrato nell'esempio.

Loop in draw()

Prendiamo in considerazione la possibilità di inserire un ciclo all'interno della funzione draw() che, come abbiamo avuto modo di ripetere fino alla nausea, è essa stessa una funzione che si ripete costantemente.

void setup() {
  size(700, 500);
  background(255);
}

void draw() {
  for(int y = 10; y < height; y+=10) {
    line(100, y, 600, y);
  }
}

Il funzionamento di questo programma è più semplice di quello che si può pensare: nel primo ciclo di draw() il programma processerà per intero il loop for (quindi la variabile y partirà da un valore 10 e poi verrà aumentata di 10 fino al raggiungimento del valore 690). Quando la condizione diventa false e il loop for si conclude, il ciclo draw() aggiorna lo schermo visualizzando tutte le linee e poi riparte per il secondo ciclo. A quel punto y verrà reimpostata al suo valore iniziale 10 e ripeterà nuovamente l'interno processo. Alla velocità di 60fps, non si riesce a vedere nessun tipo di animazione; per farlo, è sufficiente aggiungere una variabile random:

void setup() {
  size(700, 500);
  background(255);
}

void draw() {
  for(int y = 10; y < height; y+=10) {
    stroke(random(255), 0, random(255));
    line(100, y, 600, y);
  }
}

Loop nesting

La parola inglese nesting significa annidamento; quando usato in programmazione e, in particolare, in questo contesto, intendiamo la possibilità di inserire un loop for all'interno di un altro loop. Se, ad esempio, volessimo disegnare una griglia di rettangoli a coprire l'intera larghezza e altezza della nostra finestra potremmo procedere così:

void setup() {
  size(700, 500);
  background(255);
}

void draw() {
  for(int x = 0; x <= width; x += 20) {
    rect(x, 0, 10, 10);
  }
}

Una serie di quadrati vengono disegnati per tutta la larghezza della finestra.

Processing loop nesting #1

Facciamo la stessa cosa in verticale. Per comodità ho lasciato anche il codice di prima, commentandolo:

void setup() {
  size(700, 500);
  background(255);
}

void draw() {
  /* Disegna i quadrati in orizzontale
  for(int x = 0; x <= width; x += 20) {
    rect(x, 0, 10, 10);
  }
  */
  for(int y = 0; y <= height; y += 20) {
    rect(0, y, 10, 10);
  }
}

Processing loop nesting 2

A questo punto uniamo le due cose:

void setup() {
  size(700, 500);
  background(255);
}

void draw() {
  for(int x = 0; x <= width; x += 20) {
    for(int y = 0; y <= height; y += 20) {
      rect(x, y, 10, 10);
    }
  }
}

Processing loop nesting 3

Per ogni quadrato sull'asse x viene disegnata per intero la colonna.

Per concludere il post di oggi, aggiungiamo un po' di psichedelia: copiando e incollando il codice qui sotto i colori cambieranno a ogni ciclo di draw().

void setup() {
  size(700, 500);
  background(255);
}

void draw() {
  for(int x = 5; x <= width; x += 20) {
    for(int y = 5; y <= height; y += 20) {
      fill(random(255), 0, random(255));
      rect(x, y, 10, 10);
    }
  }
}

Processing_loop_nesting_4

 

Loop: while

Grazie ai controlli condizionali, abbiamo imparato come risolvere un importante problema: fare in modo che il nostro programma rispetti una logica eseguendo alcune porzioni di codice solo al verificarsi di determinate condizioni. Ora, però, ci troviamo di fronte a un’altra questione importante per il nostro futuro da programmatori: siamo in grado di disegnare qualcosa sullo schermo una volta ma non sappiamo come ripetere, in modo semplice e veloce, più volte la stessa figura sullo schermo.

Ovviamente possiamo fare copia-incolla della nostra funzione, ad esempio ellipse(), modificare qualche parametro affinché le figure non si sovrappongano una all’altra ma questo non è certamente il modo migliore di procedere. Provate a pensare, infatti, cosa succederebbe se volessimo disegnare 500 cerchi: sarebbe complicato tenere il conto del numero esatto di copie. Se, poi, ci accorgessimo di voler fare una piccola modifica probabilmente ci passerebbe la voglia di cambiare 500 righe di codice.

Introduciamo oggi il concetto di loop.

Loop

Lo scopo del loop è, per l’appunto, racchiudere in un blocco di codice delle istruzioni che devono essere ripetute un esatto numero di volte. Esistono fondamentalmente due tipologie di loop: while e for. In questo post ci concentreremo di più sul primo anche se, lo ammetto, io preferisco di gran lunga utilizzare i cicli for. Nel prossimo post, quando li approfondiremo, scoprirete anche il perché.

While

Concettualmente i cicli di loop sono molto simili ai controlli condizionali:

if(condizione) {
 // Blocco di codice
}

Abbiamo imparato che, in un if, il blocco di codice viene eseguito quando la condizione inserita tra le parentesi è vera.

while(condizione) {
 // Blocco di codice
}

Nel loop while, il blocco di codice verrà eseguito finché la condizione inserita tra le parentesi è vera.

Esempio pratico

Come al solito facciamo subito un esempio e analizziamo il codice che ha generato l’immagine:

While loop in Processing

int y = 10;

void setup() {
  size(700, 500);
  background(255);
  while(y < height) {
    line(100, y, 600, y);
    y += 10;
  }
}

void draw() {
}

Lo scopo di questo programma è disegnare delle linee orizzontali distanziate di 10 pixel l'una dall'altra fino al raggiungimento dell'altezza della finestra.

Come potete vedere nel codice, all'inizio abbiamo dichiarato e inizializzato la variabile, chiamata y, che utilizzeremo per verificare la condizione all'interno del ciclo while. In setup() abbiamo impostato la grandezza della finestra e il colore dello sfondo e poi abbiamo inserito il nostro loop:

finché il valore della variabile y sarà inferiore all'altezza della finestra, disegna una linea orizzontale le cui coordinate saranno: 100, y, 600, y. Aumenta poi il valore di y di 10.

Quando il codice viene eseguito il programma lo analizza in questo modo:

  • Il valore iniziale di y è 10 (valore con cui abbiamo inizializzato la variabile all'inizio del programma), y è minore dell'altezza della finestra (500px)? Si, allora disegna una linea con le seguenti coordinate: 100, 10, 600, 10. y = 10+10;
  • y = 20. y è minore di 500? Si, allora disegna una linea con le coordinate: 100, 20, 600, 20. y = 20+10;
  • y = 30. y è minore di 500? Si, allora disegna una linea con le coordinate: 100, 30, 600, 30. y = 30+10;
  • y = 40. y è minore di 500? Si, allora disegna una linea con le coordinate: 100, 40, 600, 40. y = 40+10;
  • ...
  • y = 490. y è minore di 500? Si, allora disegna una linea con le coordinate: 100, 490, 600, 490. y = 490+10;
  • y = 500. y è minore di 500? No, allora esci dal ciclo while.

Provate a pensare a quanto tempo avremmo perso a disegnare quelle righe una alla volta e, soprattutto, nel caso in cui volessimo modificare la distanza tra le linee da 10 a 5 pixel, ci basta modificare un singolo parametro: y += 5.

Attenzione!

Ecco alcune cose importanti che dobbiamo tenere a mente quando utilizziamo un ciclo loop di tipo while:

  • Abbiamo bisogno di creare una variabile da utilizzare come "contatore" per verificare se la condizione è true.
  • Il valore della variabile contatore deve essere modificato all'interno del ciclo while.
  • Bisogna fare attenzione che ci sia una condizione di uscita dal loop altrimenti il ciclo si ripeterà all'infinito mandando in blocco il computer.

Controlli condizionali III: Variabili booleane

Nel primo post dedicato ai controlli condizionali ho fatto un accenno alle variabili di tipo booleano anche se non mi sono soffermato troppo su di esse. Per il momento sappiamo solo che possono assumere un valore true oppure false ma non abbiamo ancora imparato come dichiararle utilizzarle.

Ovviamente dobbiamo rispettare i principi che abbiamo già visto quando abbiamo parlato di variabili: in fase di dichiarazione dobbiamo assegnare un data type – boolean in questo caso – e un nome:

boolean var = true;

Ecco che abbiamo creato una variabile chiamata var di tipo booleano il cui suo valore iniziale sia true.

Come possiamo utilizzare questa variabile? Ecco un esempio pratico:

boolean var = true;
int ellipseX = 0;

void setup() {
  size(500, 500);
}

void draw() {
  background(0);
  ellipse(ellipseX, height/2, 50, 50);
  if(var) {
    ellipseX++;
  }
}

void mousePressed() {
  var = !var;
}

Questo programma è molto simile a un altro programma che abbiamo già visto ma c’è una differenza sostanziale: ora, infatti, abbiamo un maggior controllo sul nostro cerchio: quando clicchiamo il mouse all’interno della finestra, il cerchio si ferma.

All’interno del ciclo draw() aumentiamo il valore della variabile ellipseX – che determina la posizione del cerchio – solo nel caso in cui la variabile booleana nominata var sia true.

Nella funzione mousePressed() abbiamo usato l’operatore logico NOT che inverte il valore della variabile var. Se infatti partiamo dal presupposto che la variabile sia true, come previsto dall’inizializzazione, al click del mouse var diventerà NOT true quindi false. Viceversa, se la variabile ha un valore false al click viene assegnato un valore NOT false quindi true.

Mi rendo conto che questo passaggio possa essere un po’ ostico quindi, per aiutarvi, riscrivo la funzione mousePressed() in modo più semplice:

void mousePressed() {
 if(var) {
 var = false;
 } else {
 var = true;
 }
}

Con l’operatore NOT ho reso il mio codice più leggibile riducendo il numero di righe, da cinque a una soltanto, per cambiare il valore della variabile.

A questo punto vi propongo un altro esercizio: e se volessi fare in modo che il mio cerchio anziché fermarsi torni indietro al click del mouse? Buon lavoro!

Esercizio 2: I quattro quadranti

Nell’ultima lezione pubblicata abbiamo parlato di controlli condizionali e operatori logici. Ecco la soluzione all’esercizio dei “quattro quadranti”:

Soluzione all'esercizio proposto in questa lezione: http://blog.federicopepe.com/2015/10/controlli-condizionali-ii-operatori-logici/
Raw
 ex_1_Quattro_Quadranti.pde
/*
 *  Esercizio 2: I quattro quadranti
 *  by Federico Pepe
 *  http://blog.federicopepe.com/processing
 */
 
 void setup() {
   size(500, 500);
   stroke(255);
   fill(255);
 }
 
 void draw() {
   // Imposto il colore nero di background
   background(0);
   
   // Verifico la posizione X e Y del mouse e disegno il rettangolo
   // nel quadrante in cui si trova il mouse
   if(mouseX < width/2 && mouseY < height/2) {
     rect(0, 0, width/2, height/2);
   } else if(mouseX > width/2 && mouseY < height/2) {
     rect(width/2, 0, width/2, height/2);
   } else if(mouseX < width/2 && mouseY > height/2) {
     rect(0, height/2, width/2, height/2);
   } else {
     rect(width/2, height/2, width/2, height/2);
   }
   // Disegno le linee di riferimento
   line(width/2, 0, width/2, height);
   line(0, height/2, width, height/2);
 }

Le linee di codice sembrano molte ma, in realtà, la logica è piuttosto semplice. Nella parte di setup() imposto la grandezza della finestra e alcuni parametri di base: il colore delle linee di riferimento e il fill sarà bianco.

All’interno di draw() verifico la posizione X e Y del mouse all’interno di una serie di controlli condizionali. Traduco in pseudocodice la condizione che vado a controllare nel primo if: se la posizione X del mouse ha un valore minore della metà della larghezza della finestra e la posizione Y del mouse ha un valore minore della metà dell’altezza della finestra allora disegnerò un rettangolo il cui punto di partenza sia l’angolo in alto a sinistra, ovvero il punto con coordinate 0, 0 e le cui dimensioni siano la metà della finestra sia in larghezza che in altezza.

Negli else if successivi verifico le altre condizioni e disegno il quadrato bianco di conseguenza. Nell’ultimo else non occorre porre nessuna condizione perché è, per esclusione, l’ultima possibilità ovvero che il mouse si trovi nel quadrante in basso a destra.

Perché utilizzo delle variabili invece di parametri hard-coded? Avrei potuto benissimo scrivere:

if(mouseX < 250 && mouseY < 250)

In tal caso, però, il codice funzionerebbe correttamente solo nel caso in cui le dimensioni della mia finestra siano impostate, come nell’esempio, a 500 x 500 pixel. Con le variabili, posso modificare i parametri in size e avere un programma sempre funzionante.