Molti programmatori (me compreso) vengono da linguaggi in cui è stato sempre promosso l’uso della programmazione in stile imperativo come Java, C# o C++ e il codice scritto in stile funzionale può apparire ad alcuni “esoterico”. Ma da un pò di tempo a questa parte la programmazione funzionale sta prendendo sempre più piede e molti dei linguaggi tra cui gli appena citati, si sono evoluti introducendo nuovi costrutti e funzionalità che permettono di adottare il functional style.
La programmazione funzionale non è niente di nuovo, linguaggi che esistono da tantissimo tempo, come ad esempio il LISP (che personalmente ho utilizzato in un unica occasione, per l’esame di AI all’Università), hanno adottato da sempre questo stile di programmazione. Ma allora perchè la programmazione funzionale è diventata così di moda?
Sicuramente uno dei motivi è stato l’avvento dei processori multi-core che hanno trasformato la programmazione multi-thread da una cosa più simile al multitasking su processori a singolo core ad una cosa più complessa su processori multi-core e quindi ad elaborazione parallela reale.
La programmazione multi-thread su multi-core, è un cosa abbastanza complicata usando l’imperative style, in quanto bisogna occuparsi in maniera esplicita della concorrenza e delle risorse condivise, mentre la programmazione funzionale ci viene in aiuto in tal senso in quanto è intrinsecamente thread-safe e vedremo più avanti nell’articolo il perchè.
Vediamo invece adesso un esempio per chiarire meglio i due stili programmazione. Proponiamo un semplice problema che cercheremo di risolvere in Java usando i due stili imperativo e funzionale evidenziando le differenze tra i due approcci.
Indice
Imperative Style vs Functional Style
Supponiamo di avere un array di interi e di voler trovare il massimo valore tra quei numeri in lista che siano multipli di 10.
Imperative Style
In maniera imperativa avremo una cosa del tipo:
public class Esempio { // Approccio Imperativo public static Integer maxImperative(int[] numbers) { Integer max = null; if(numbers.length > 0) { max = numbers[0]; for(int e : numbers) { if(e % 10 ==0 && e > max) max = e; } } return max; } public static void main(String[] args) { int[] values = new int[] {2, 5, 6, 10, 8, 30, 150, 11, 100, 247, 45, 40, 132}; System.out.println(Esempio.maxImperative(values)); } }
Come possiamo vedere nel metodo maxImperative, in perfetto imperative style siamo noi a dire COME fare per calcolare il max tra i multipli di 10, iterando nella lista, verificando le condizioni con l’ if ecc… La procedura sembra abbastanza chiara, la maggior parte dei programmatori avrebbe fatto più o meno così, ma potrebbero esserci dei problemi con questa implementazione, vediamo quali:
- Variabili Mutabili – Lo stile imperativo implica spesso l’uso di variabili mutabili. In questo stile, generalmente inizializziamo le variabili con un valore predefinito o iniziale e mentre iteriamo, spesso modifichiamo queste variabili. Nell’esempio precedente, max viene modificata più volte; ma cosa succede in un ambiente multi-thread in cui più thread accedono e modificano tali variabili? Il problema và gestito esplicitamente dal programmatore e in presenza di codice di una certa complessità la cosa non è proprio semplice.
- Uso di variabili spazzatura – Quando si programma in modo imperativo, spesso creiamo variabili inutili. Questi sono segnaposti temporanei, variabili che ci servono per uno scopo, come appoggio per qualche valore intermedio del nostro algoritmo, o per memorizzare un risultato, come può essere la variabile max in questo esempio. E più variabili temporanee vengono create più “spazzatura” c’è da raccogliere!
Ma vediamo adesso l’approccio funzionale allo stesso problema, creando il nuovo metodo maxFunctional :
Functional Style
// Approccio Funzionale public static Integer maxFunctional(int[] numbers) { OptionalInt max = Arrays.stream(numbers) .filter(number -> number % 10 == 0) .max(); // qui usiamo gli Optionals per verifcare se c'è un risultato return max.isPresent() ? max.getAsInt() : null; } public static void main(String[] args) { int[] values = new int[] {2, 5, 6, 10, 8, 30, 150, 11, 100, 247, 45, 40, 132}; System.out.println(Esempio.maxFunctional(values)); }
L’approccio funzionale è di tipo dichiarativo. Nello stile dichiarativo ci si concentra sul COSA abbiamo bisogno e non sul COME ottenerlo (come si fà nella programmazione imperativa) e si delegano i dettagli a librerie di funzioni. Ne risulta uno stile conciso, espressivo, che non utilizza variabili spazzatura e non ha alcuna mutazione di variabili esplicita (nella nostra funzione max viene modificata una sola volta). In un ambiente multi-thread usando un codice con queste caratteristiche è molto più semplice gestire la concorrenza.
Ma il functional style nel nostro esempio entra in gioco con l’argomento passato al metodo filter:
Questa chiamata di funzione è molto diversa da quella a cui siamo abituati a vedere nelle versioni precedenti di linguaggi come Java e C. Invece di prendere valori o oggetti come argomento, questa funzione accetta come argomento una funzione anonima (una espressione lambda). Ciò rende questa funzione “funzionale nello stile”.
Nello stile funzionale si usano quelle che si chiamano funzioni di ordine superiore (high order function).
Le funzioni di ordine superiore possono:
- ricevere funzioni come argomento, allo stesso modo di tipi primitivi e oggetti.
- creare funzioni, allo stesso modo di come creiamo tipi primitivi e oggetti all’interno delle funzioni.
- restituire funzioni, allo stesso modo di come possiamo restituire tipi primitivi e oggetti dalle funzioni.
Lo stile funzionale ha tutti i vantaggi dello stile dichiarativo e possiamo usare la scomposizione in funzioni come strumento di progettazione. Nella programmazione OO usiamo gli oggetti per modellare la separazione delle responsabilità. Con lo stile funzionale, deleghiamo diverse responsabilità a diverse funzioni.
Ad esempio, la funzione filter vista sopra, si assume la responsabilità di eliminare elementi che non soddisfano un particolare criterio e delega ad un’altra funzione, quella che riceve come argomento, la responsabilità di decidere quali elementi scegliere effettivamente, in questo caso i numeri multipli di 10.
Conclusioni
Lo stile imperativo ha un vantaggio significativo: quasi ogni programmatore lo conosce. Oggi è lo standard di fatto. La maggior parte delle istituzioni educative insegna questo stile e i programmatori acquisiscono un’esperienza significativa con questo stile nei loro progetti.
La maggior parte delle persone sa lavorare con cicli, condizioni e task, mentre non molti programmatori sono familiari o a proprio agio con lo stile funzionale. Sembra strano, sembra complesso a prima vista, sembra difficile da debugare ed Inoltre, queste nuove funzioni, non ci suonano familiari come può essere un ciclo for o una condizione if.
È nella natura umana associare il non familiare allo strano e al complesso. Lo stile funzionale potrebbe non essere familiare per chi non l’ha mai usato, ma è sicuramente più conciso e ha una complessità relativamente minore rispetto allo stile imperativo.
Più iniziamo a leggere e scrivere il codice in stile funzionale, tanto più velocemente quello strano codice diventa una nuova sintassi comoda e familiare. Ci si accorgerà che in generale il codice in stile funzionale è molto più facile da leggere e richiede meno tempo e fatica, rispetto al “prolisso” codice in stile imperativo. Questo è particolarmente vero quando bisogna leggere il codice scritto da altre persone.
Impariamo quindi ad usare il functional style! 😉