Invito alla programmazione funzionale
1 Il paradigma funzionale e l'insegnamento
Il paradigma funzionale di programmazione è, a mio avviso, estremamente utile nell'insegnare la programmazione dei computer ai ragazzi e nel favorire lo sviluppo del pensiero computazionale. Questi sono a grandi linee le caratteristiche che stanno alla base.
In generale sono inspirati ad un approccio matematico al problema e la loro sintassi spesso ne rispecchia questi principi. A parte le differenze concettuali tra linguaggi puri ed impuri sostanzialmente la forma sintattica e concettuale è allineata alla funzione matematica. Siamo credo, quasi tutti d'accordo sull'importanza della matematica ad ogni livello di istruzione e formazione.
Il concetto è quindi quello di trasformazione piuttosto che computazione. Una funzione matematica non esegue calcoli ma solamente trasformazioni e mappature di valori, si limita quindi a applicare argomenti da un dominio all'altro: dominio e codominio.
Una funzione, che può essere anche chiamata operatore a questo punto, in un linguaggio funzionale agisce nella stessa maniera. È un operatore e gli operatori sono (generalmente) funzioni. Scrivere il segno +, significa invocare una funzione infissa (ma non sempre) che accoglierà due valori in entrata e restituirà un valore in uscita: mappa un dominio su un altro.
Scrivere, 1 + 2 è equivalente allo scrivere 1 somma 2 (se somma fosse una funzione infissa di quel tipo); ma anche + 1 2 o somma 1 2, come in alcuni linguaggi funzionali a notazione prefissa.
Anche se non si nota a prima vista questo porta a dei notevoli vantaggi.
Riflettiamo su come abitualmente sommiamo una lista di numeri: 1 + 2 + 3 + 4 + 5
ovvero (1 + 2) + 3 + 4 + 5
ovvero (3 + 3) + 4 + 5
ovvero ( 6 + 4) + 5
ovvero 10 + 5 => 15
. Questo meccanismo che i nostri ragazzi faticano a ricordare nella definizione matematica di proprietà associativa ma ignari applicano naturalmente è intrinseco (in una maniera o nell'altra) nei linguaggi funzionali. Il meccanismo chiamato currying (da Haskell Curry1) è alla base di molti linguaggi (anche se non di tutti, ma praticamente tutti hanno meccanismi simili) ed è utile per la composizione delle funzioni. Siamo abituati naturalmente alla composizione, siamo circondati di oggetti prefabbricati componibili, oggetti che hanno una funzione specifica che devono essere completati con altri. Un esempio banale, se noi abbiamo una funzione somma che prende due numeri potremmo derivarne una che somma 1 ad un qualsiasi numero: somma(a,b) = a + b
, somma_uno_ad_un_numero = somma 1
, somma_uno_ad_un_numero 3
restituirà 4
. In OCaml2 che è un linguaggio curryed della famiglia ML3 sarà:
(* La funzione somma due numeri *) let somma a b = a + b;; (* Restituisce una funzione che è parzialmente applicata *) let somma_uno_ad_un_numero = somma 1;; (* Risultato: 4 *) somma_uno_ad_un_numero 3;;
Questo meccanismo che pomposamente si chiama funzione parzialmente applicata in realtà non è così complesso e secondo me molto vicino ad alcuni nostri modi di pensare. Da qui alla composizione la strada è breve, immaginate come da operazioni generiche si possa arrivare a quelle specializzate e poi a comporle insieme come un flusso. Con i linguaggi funzionali, possiamo essere in grado di costruire flussi dichiarativi, sequenze logiche di azioni da intraprendere e descriverle in maniera pulita senza infarcire di test e condizioni.
alzato -> bagno -> denti -> viso -> colazione
Sequenze operative, come le facciamo ogni giorno. Non ci chiediamo prima di andare in bagno se ci siamo alzati e prima di lavarci i denti se siamo in bagno e prima di lavarci il viso se ci siamo lavati i denti e prima di colazione se ci siamo lavati il viso.
Perché voi vi lavate i denti prima di colazione?
alzato -> bagno -> viso -> colazione -> denti
È bastato spostare i denti in fondo alla catena. Si può obiettare che anche con i linguaggi imperativi si potrebbe fare una cosa simile ed è vero, ma forse sarebbe un poco più complesso (esistono linguaggi multiparadigma e imperativi con alcune estensioni funzionali).
Questo, spiegato così banalmente, è solo uno dei meccanismi interessanti e dei vantaggi.
A scuola ci dicono che non si può sommare le pere con le mele, bene… Il concetto di tipo di dati è fondamentale ed avere linguaggi che forzano a non mischiare pere e mele nella somma non può fare che bene. Abitua a pensare a cosa si sta facendo.
Il concetto di ricorsività, che detto così pare complesso, è insito in noi. Noi non pensiamo in termini di ciclo in sé ma lo pensiamo in quanto ricorsivo e cioè che succede e risuccede. Perché lo chiamiamo ciclo? Ad una domanda così non rispondereste perché ricorre?
I linguaggi funzionali hanno una particolare enfasi sulle liste, liste di cose. Noi abbiamo la lista della spesa o l'array della spesa? Operazioni come map non sono altro che un trasformare una lista in un'altra lista. L'operazione di fold, scoglio micidiale di chi si introduce in questa magia, non è altro che quello che facciamo quando smontiamo l'albero di Natale: prendiamo ogni palla dall'albero e l'avvolgiamo (fold) con la carta (una funzione) per metterla nella scatola (il valore restituito). È molto semplice e anche in maniera erronea perché poi ci sono problemi di tipo e via dicendo, ma l'esempio serve a far capire a grandi linee.
Con la funzione fold possiamo sommare un lista di numeri (in OCaml): List.fold_left (+) 0 [1;2;3;4;5];;
Il risultato è 15, potete vedere come + è una funzione, lo zero è il valore iniziale usato come accumulatore e poi la lista. Scritto può apparire complesso e fuorviare dal concetto che è in realtà semplice.
In Racket4 che è un dialetto LISP5 ed a notazione prefissa: (foldl + 0 '(1 2 3 4 5)).
Racket per esempio ha dei moduli (Pictures) per l'insegnamento (https://docs.racket-lang.org/quick/index.html).
Kojo6 è un ambiente per l'insegnamento basato su Scala7, un altro linguaggio funzionale.
I concetto poi di immutabilità dei valori, valori e variabili sono strettamente distinti e si incoraggia a non usare le variabili. Matematicamente non abbiamo variabili ma incognite e cioè valori non conosciuti che con opportune trasformazioni rendiamo evidenti.
Sono solo alcuni dei meccanismi che sono intrinseci nei funzionali ed era solo per spezzare una lancia a favore di un modo di pensare che viene erroneamente ritenuto complesso e che in realtà non è più complesso di altri.
Per concludere, anche la programmazione orientata agli oggetti (che nella sua base rientra nei linguaggi imperativi) ha i suoi indiscussi pregi ed infatti molti linguaggi funzionali oggi sono anche orientati agli oggetti come molti imperativi hanno estensioni funzionali, perché i problemi oggi sono innumerevoli e ogni approccio ha i suoi pregi e difetti.