Cosa sono le funzioni? Come si usano?

Un buon programmatore non può non sapere cosa sono le funzioni: sono alla base del ri-utilizzo del codice. Non solo: sono un elemento fondamentale della scrittura di codice, senza le quali si impiegherebbe molto più tempo e si commetterebbero molti più errori. Cosa sono, quindi, queste funzioni? E come si usano?

Le funzioni sono delle parti di programma che possono essere richiamate per essere eseguite, che possono accettare valori in ingresso e che possono restituire degli output.

Le funzioni, quindi, sono dei raggruppamenti di frammenti di codice autonomi che – partendo da eventuali input – possono restituire eventuali output, eseguendo operazioni più o meno complesse.

Grazie alla loro struttura ci permettono di comporre codice in modo più conciso, preciso, semplice ed elegante. Tuttavia, rappresentano spesso un argomento ostico e difficile da comprendere: il modo migliore per capirne il funzionamento è sperimentarle, toccarle con mano ed esercitarsi, finché è necessario.

Com’è composta una funzione?

Ad oggi ci sono tanti, tantissimi, linguaggi di programmazione diversi: alcuni sono più performanti, altri sono più semplici, altri ancora sono indipendenti dalla piattaforma hardware. Ciò nonostante, nella maggioranza dei casi la struttura di una funzione – anche detta metodo, per gli amici che utilizzano Java, C# & Co. – non muta eccessivamente. Infatti, solitamente, una subroutine:

  • Deve essere definita
  • È vincolata da un nome
  • Deve avere una signature
  • Può avere dei valori di input
  • Può avere dei valori di output

Definizione di una funzione e scelta del nome

Il primo passo per utilizzare una funzione è definirla: significa darle una forma, assegnarle un nome e dei parametri e scriverne il contenuto. È indispensabile, in questa prima ma fondamentale tappa, sapere cosa deve fare – e con quali dati.

La definizione varia da linguaggio a linguaggio, tuttavia è spesso introdotta dalle parole chiave funcfunctionfun. E poi? È necessario sceglierne il nome: deve essere esplicativo e, alla sola lettura, dovrebbe permettere di capire cosa fa la funzione; nei progetti a lungo termine è importantissimo seguire questa prassi: permette una manutenzione più agevole.

La signature

Ogni funzione è unica: ciò che contraddistingue da tutte le altre è la sua firma, la cosiddetta signature. Questa è formata dal nome che le abbiamo assegnato, dai parametri in ingresso e – se stiamo usando un linguaggio tipizzato – dal tipo dei parametri. La signature, quindi, rappresenta l’identificativo del nostro sottoprogramma.

Parametri in ingresso, valori in uscita

Le routine sono “pezzi di programma” richiamabili: spesso sono sfruttate per raggruppare elaborazioni più o meno complesse di dati; per questo motivo, è fondamentale che abbiano la possibilità di ricevere degli input – detti parametri in ingresso – e di presentare degli output.

Il passaggio di questi parametri avviene in due modalità differenti: per valore o per riferimento. Il sistema varia sia in base al linguaggio che al tipo di dato. Ma andiamo per passi: cosa significa passare un parametro per valore o per riferimento?

Tutto è dovuto al modo di operare della RAM (Random Access Memory): le informazioni si trovano nelle variabili, dei contenitori che permettono di memorizzare dei valori. Una parte della RAM è organizzata a stack, a pila: significa che le informazioni vengono salvate una dopo l’altra, tutte in fila. Tuttavia, è presente un’altra area all’interno della nostra memoria ad accesso casuale: lo heap, il “mucchio”; qui i dati vengono salvati “alla rinfusa” e l’unico modo per ritrovarli è tramite una referenza,  che è collocata in una variabile referenza (nello stack). Questa seconda metodologia di salvataggio è particolarmente vantaggiosa quando si lavora con gli oggetti, che spesso sono di grandi dimensioni.

Entriamo nel dettaglio

Ecco quindi che puoi capire la differenza tra passaggio per valore e passaggio per riferimento: nel primo caso il dato si trova nello stack e, quando lo passi come parametro, ne viene fatta una copia all’interno della funzione; nel secondo caso, invece, viene inviata soltanto la referenza dell’oggetto – e non tutta l’istanza. Ti starai chiedendo cosa cambia all’atto pratico: che tu ci creda o no, si tratta di una divergenza notevole; infatti, nella prima situazione, se modifichi il valore di input, il cambiamento sarà limitato alla copia che è stata effettuata in fase di passaggio del dato; nel secondo contesto, invece, qualsiasi modifica si ripercuoterà sull’oggetto passato: questo, infatti, ha ora due referenze nello stack (quella originale e quella creata dopo aver chiamato la funzione), ma “esiste” una sola entità all’interno della RAM. È come avere due porte d’ingresso nella stessa casa: puoi entrare sia da una che dall’altra, ma l’abitazione rimane sempre la medesima.

Esempio di funzione

Gli esempi che ti propongo di seguito sono scritti in Java, ma non si discostano eccessivamente da altri linguaggi.

public String toString() {
    String string = this.nome + " " + this.cognome; return string;
}

public int somma (int a, int b) {
    int somma = a + b;
    return somma;
}

public void saluta (String nome) {
    System.out.println("Ciao, " + nome);
}

Come puoi notare, si tratta di frammenti di codice molto semplici e che, se richiamati, svolgono compiti ben definiti e concisi. Se volessi, potresti ampliarle richiamando, dall’interno, altre funzioni.

La ricorsività

Una pratica che permette di astrarre la scrittura di un algoritmo è la ricorsività: significa che una funzione, durante l’esecuzione, invoca sé stessa. Sostanzialmente, equivale a dire che un elefante è un elefante. Nonostante possa sembrare un paradosso, si tratta di una pratica diffusa ma certamente non semplice: è necessaria molta esperienza per poter realizzare funzioni ricorsive efficaci

Conclusione

Siamo giunti alla conclusione di questa sezione, fondamentale per poter proseguire la guida di Arduino.

Sapere come dar forma ad una funzione, come gestirne i parametri in ingresso e come descriverne il comportamento è molto importante: è lo strumento che ti dà la possibilità di creare programmi efficienti e facilmente mantenibili, senza creare confusione.

A questo punto, non ti resta che sperimentare le funzioni: prova a scriverne di semplici e poi realizzane sempre di più complesse. Sei pronto?