Le funzioni sono uno strumento molto utile per racchiudere in un modulo separato una funzionalità specifica attribuendole un nome. Ad esempio, supponiamo che ci serva di individuare, più volte all'interno di un programma, il maggiore tra due numeri. Ovviamente non ha alcun senso ripetere, ogni volta che è necessario, il codice che serve allo scopo, senza contare il fatto che, se avessimo bisogno di correggere l'algoritmo, dovremmo cercare e correggere tutte le ripetizioni delle istruzioni! Ha molto più senso, invece, racchiudere l'algoritmo in una funzione separata con un nome opportuno.
float trovamax(float a, float b)
{
if (a > b) // se a > b
return a; // restituisci la variabile 'a'
else // altrimenti
return b; // restituisci la variabile 'b'
}
Le variabili a e b, definite a destra del nome della funzione, tra parentesi tonde, sono i parametri della funzione, ovvero i suoi dati di ingresso. Da notare che anche la funzione viene definita del tipo float perché restituisce a chi l'ha invocata un valore numerico con virgola.
Le funzioni sono utili anche soltanto per "chiudere" una parte di codice logicamente separata dal resto in una scatola con un nome ed avere un programma principale più pulito e ordinato.
Per esempio, nella struttura della modalità dinamica di Processing le due funzioni setup e draw non hanno parametri di ingresso e non restituiscono valori alla fine perché hanno l'unico scopo di separare la parte di programma che viene eseguita una sola volta da quella che viene eseguita 25-30 volte al secondo. Per questo motivo il tipo delle due funzioni è void e le parentesi tonde sono vuote
void setup()
{
}
void draw()
{
}
Se analizziamo il codice che abbiamo scritto alla fine della sezione precedente, troviamo facilmente che nella sezione draw un insieme di istruzioni si occupano della parte grafica
background(255);
fill(255, 0, 0);
ellipse(posx, posy, 10, 10); // disegno l'ellisse
textSize(15);
text("t: " + t, 10, 25); // visualizzo il valore del tempo
e altre istruzioni della parte di calcolo delle grandezze di stato.
posx = posx + vx * dt; // applico le equazioni del moto r.u.
posy = posy + vy * dt; // alla posizione
t = t + dt; // incremento il tempo
Possiamo, allora, pensare di usare due funzioni per incapsulare (come si dice in gergo informatico) queste due parti.
// dichiarazione delle variabili
float posx, posy; // due componenti per la posizione
float vx, vy; // due componenti per la velocità
float t, dt; // il tempo e l'incremento di tempo
void setup()
{
size(800, 600);
t = 0; // il contatore del tempo comincia da zero
dt = 1; // il delta-t viene inizializzato ad 1
posx = 100; // x e y della posizione iniziale
posy = 200;
vx = 1; // componenti x e y del vettore velocità
vy = 0.5;
}
void draw()
{
background(255);
grafica();
movimento();
}
void grafica()
{
fill(255, 0, 0);
ellipse(posx, posy, 10, 10); // disegno l'ellisse
textSize(15);
text("t: " + t, 10, 25); // visualizzo il valore del tempo
}
void movimento()
{
posx = posx + vx * dt; // applico le equazioni del moto r.u.
posy = posy + vy * dt; // alla posizione
t += dt; // incremento il tempo
}
Dal punto di vista funzionale non è cambiato nulla, ma, come si vede chiaramente, ora la funzione draw ha solo tre istruzioni: una per il colore dello sfondo e le altre due per richiamare le funzioni di disegno e movimento.
Prima di aggiungere le forze al nostro universo, proviamo a scrivere il codice per implementare il rimbalzo perfettamente elastico del punto materiale contro le pareti del canvas.
Dalla teoria (o dall'osservazione pratica) sappiamo che un urto perfettamente elastico conserva la componente tangenziale del moto e cambia segno alla componente normale alla parete.
Questa è una possibile implementazione:
// dichiarazione delle variabili
float x, y; // due componenti per la posizione
float vx, vy; // due componenti per la velocità
float t, dt; // il tempo e l'incremento di tempo
float r = 20; // raggio dell'oggetto
void setup()
{
size(800, 600);
t = 0; // il contatore del tempo comincia da zero
dt = 5; // il delta-t viene inizializzato ad 1
x = 100; // x e y della posizione iniziale
y = 200;
vx = 1; // componenti x e y del vettore velocità
vy = 0.3;
}
void draw()
{
//background(255);
grafica();
movimento();
rimbalzo();
}
void grafica()
{
fill(255, 0, 0);
ellipse(x, y, 2*r, 2*r); // disegno l'ellisse
textSize(15);
text("t: " + t, 10, 25); // visualizzo il valore del tempo
}
void movimento()
{
x = x + vx * dt; // applico le equazioni del moto r.u.
y = y + vy * dt; // alla posizione
t += dt; // incremento il tempo
}
void rimbalzo()
{
if (x + r > width) // sto urtando la parete destra
{
vx = -vx;
x = width - r;
}
if (x - r < 0) // sto urtando la parete sinistra
{
vx = -vx;
x = r;
}
if (y + r > height) // sto urtando la parete inferiore
{
vy = -vy;
y = height - r;
}
if (y - r < 0) // sto urtando la parete superiore
{
vy = -vy;
y = r;
}
}
Un po' di cose da notare.
L'uso delle funzioni rende molto più leggibile e gestibile il codice principale nella funzione draw. Se, ad esempio volessi disabilitare il rimbalzo nella simulazione, mi basterebbe commentare la riga di codice che chiama la funzione relativa.
L'uso delle funzioni separa logicamente e sintatticamente le parti di codice che si occupano di una determinata funzione. In questo modo è più facile correggere il codice o modificarlo per implementare nuove funzionalità.
Il fatto che la funzione di rimbalzo venga chiamata dopo quella di movimento non è casuale. In questo modo, la verifica sull'urto viene fatta dopo che è stata calcolata la posizione nuova e l'urto viene gestito correttamente. Se invertissimo le due funzioni, cosa succederebbe?