La stesura del codice sorgente di un applicativo può essere concepita seguendo diverse logiche
Queste logiche possono essere raggruppate in insiemi unitari, aventi rango di paradigmi.
Ma cosa è un paradigma?
Iniziamo con il definire il significato letterale della terminologia relativa ai paradigmi di programmazione.
Per paradigma si intende un modello canonico, avente valore di riferimento e di confronto.
Una volta definito quindi questo campione di riferimento, si possono far ricadere dentro di essi i relativi esemplari.
Analogamente, e per differenza, esemplari che non rispondono alle caratteristiche del modello devono forzosamente costituire un nuovo modello di riferimento. Questo può presentare tratti di continuità con l’esistente, o costituire una nuova branca, totalmente nuova.
Si noti la trasversalità di questa definizione; essa può essere applicata alla scienza informatica, così come alla classificazione di specie animali o vegetali. Vedremo a breve ulteriori analogie tra questi due mondi apparentemente non in contatto tra loro.
Paradigmi di programmazione: definizione
Un paradigma di programmazione è uno stile specifico, costituito da un insieme di regole, logiche, approcci, e funzionalità, al quale il programmatore fa riferimento nell’affrontare, e quindi risolvere, uno specifico problema di natura informatica.
In questo articolo, si preferisce ricorrere alla definizione di paradigma di programmazione usata come un sistema di classificazione degli approcci logici utilizzabili dai programmatori, piuttosto che come un modo per classificare i singoli linguaggi di programmazione1.
Questa scelta è supportata dal fatto che diversi linguaggi di programmazione non possono essere fatti ricadere rigidamente all’interno di un singolo paradigma, ma piuttosto, essi incorporano diverse funzionalità e logiche derivate da diversi paradigmi.
Ancora, sebbene un linguaggio di programmazione possa essere stato concepito come sommariamente aderente a un determinato paradigma, esso, allo stesso modo nel quale un linguaggio umano evolve nel tempo, può essere evoluto rispondendo anche a necessità o logiche afferenti paradigmi di programmazione differenti da quello prestabilito alla sua genesi.
Ad esempio, Python nasce come linguaggio orientato agli oggetti (OOP), ma può essere declinato anche secondo logiche rispondenti alla programmazione funzionale.
Rust è descritto come un linguaggio di programmazione multi-paradigma, che presenta uno stile tipicamente imperativo, ma declinabile anche secondo diversi paradigmi di programmazione funzionale o orientata agli oggetti.
I paradigmi di programmazione possono essere visti come un albero genealogico, dove un paradigma di programmazione ha origine da un suo successore e ne modifica o enfatizza specifiche caratteristiche.
In questo senso, come si ribadirà ulteriormente, è sconsigliato il seguire un approccio ermetico o contrapposto tra le diverse logiche e i diversi paradigmi di programmazione, avendo essi sovente una o più radici comuni.
Paradigmi di programmazione: la storia
La genesi del termine, e di conseguenza la sua adozione sistematica per definire diversi approcci logici alla programmazione, è piuttosto fluida.
Analizzando le vicende storiche, che risalgono ai primi anni ’60, si riscontrano diverse correnti di pensiero che possono essere raggruppate in veri e propri paradigmi.
Tuttavia il termine “Programming Paradigms” viene per la prima volta formalizzato solo nel 1978, nella lettura che Robert W. Floyd tiene alla Stanford University in occasione della cerimonia per il suo Turing Award.
Il testo integrale, disponibile a questo link, cita il termine “paradigms” 31 volte, e rimanda a precedenti importanti quali Dijkstra, Wirth, Kuhn.
Questo valida la tesi secondo la quale il termine “paradigma di programmazione” sia stato fatto coincidere in via retrospettiva con una o più correnti di pensiero e logica già in vigore prima dell’utilizzo formale del termine.
Lo stesso Floyd presenta come incipit la definizione di paradigma attinta dall’Oxford English Dictionary, come a voler suffragare l’utilizzo di un termine specifico per una categoria di eventi con caratteristiche comuni.
Una ulteriore opera citata nella genesi dei paradigmi di programmazione è Peter J. Landin, “The Next 700 Programming Languages”, pubblicato nel 1966 per conto di UNIVAC.
Il testo riporta un tentativo di classificazione di diversi linguaggi di programmazione e loro caratteristiche, secondo un framework denominato ISWIM (“If you See What I Mean”), con il tentativo di consolidare un metodo di classificazione comune.
A distanza di 50 anni si può dire che il tentativo sia riuscito in parte.
Tuttavia per il fine qui esposto, si precisa che l’opera non presenta alcuna ricorrenza del termine “paradigms” o “programming paradigms”, e pertanto a esso è possibile attribuire una influenza culturale per i posteri piuttosto che la paternità del termine.
Influenza nella cultura informatica
Come già precisato, è ragionevole ritenere siano state applicate logiche per risolvere problemi di natura informatica seguendo approcci tra loro omogenei. Senza che tuttavia questo esprimesse una specifica volontà di aderire a un paradigma di programmazione in senso stretto.
I primi scambi tra uomo e macchina avvennero mediante il linguaggio macchina dei singoli elaboratori informatici. Questi rispondevano in linea di principio a un paradigma imperativo.
L’aspettativa era appunto che il calcolatore eseguisse le funzioni richieste dall’operatore.
Una successiva e importante evoluzione si ha con la nascita di linguaggi di programmazione più evoluti e astratti (per l’epoca, per lo meno), come Assembly e Fortran. Entrambi furono basati su un paradigma di programmazione imperativa.
Si fornisce un’esemplificazione con due schematici esempi in Assembly e Fortran.
Assembly
global _start _start: mov eax, [var_name] add eax, [var_name] mov [sum], eax ...
Fortran
PROGRAM HELLO PRINT '(A)', 'Hello, world' STOP END
Con l’evolvere della potenza di calcolo, della cultura associata al mondo dell’informatica, e alla necessità di creare e governare strutture sempre più complesse, è divenuto sempre più importante approcciare la creazione e utilizzo dei linguaggi di programmazione secondo regole comuni e condivise.
Proprio su questi presupposti si sono formalizzate le logiche che hanno dato vita ai paradigmi di programmazione di uso corrente.
I principali paradigmi di programmazione
Programmazione imperativa
Il paradigma della programmazione imperativa è, sia storicamente che concettualmente, il paradigma di programmazione più rilevante, e con un valore fondante.
Anche intuitivamente, si può comprendere come sia naturale fornire al computer comandi da eseguire, come veri e propri ordini.
“Print
“, “Run
“, “Do
“, “Go
“, “End
“, sono tutti termini spesso riscontrati in questo tipo di paradigma.
Dal punto di vista storico esso deriva dalla struttura e logica dei calcolatori, basata su:
- Memoria.
- Unità di calcolo e logica (all’interno della CPU).
- Unità di controllo e processi (all’interno della CPU).
- Sistemi I/O.
Il paradigma imperativo si concentra sulla gestione delle variabili e del loro stato, anche in relazione alla loro puntuale allocazione di memoria e persistenza all’interno di un applicativo.
Si struttura come una sequenza di comandi e istruzioni, assimilabili ad un flusso, con i dovuti bivi per gestire casi specifici.
Una ulteriore evoluzione della programmazione imperativa è data dalla programmazione procedurale, che prevede l’aggiunta di procedure e subroutines.
La programmazione imperativa richiede tipicamente un profondo grado di controllo sulla macchina. Se questo permette di sfruttare al massimo le prestazioni del sistema, d’altra parte rende il codice complesso e di difficile interpretazione, portabilità e manutenzione.
Si fornisce un esempio di programmazione imperativa in Java.
public class SumNum {
public static void main(String[] args) {
int somma = 0;
for (int i = 1; i <= 10; i++) {
somma += i;
}
System.out.println("The sum is" + somma);
}
}
Programmazione Dichiarativa
La programmazione dichiarativa si basa e focalizza sul risultato da ottenere, senza esplicitare direttamente la sequenza puntuale di operazioni necessarie per ottenerlo. In questo è vista a ragione l’antitesi della programmazione imperativa, la quale come esplicitato invece interviene a livello atomico nella creazione del processo.
Nella programmazione dichiarativa, la complessità viene nascosta agli occhi dell’utente, lasciando al programma computazionale l’onere di eseguire tale attività.
Un esempio di programmazione dichiarativa, per altro di sostanziale rilevanza e vastissima diffusione è SQL.
SELECT Name FROM Customers WHERE Country='Germany' AND (City='Berlin' OR City='München');
Anche Python, usato come wrapper di librerie di machine learning e deep learning, spesso scritte in Cython, C++ o anche C, richiede un utilizzo dichiarativo. Questo connubio consente di ottimizzare l’efficacia e prestazioni di linguaggi compilati con l’espressività di un linguaggio di alto livello come Python.
I principi fondanti della programmazione dichiarativa sono:
- Dichiarazione degli intenti.
- Immutabilità delle risultanze (solo le funzioni possono modificare cambiamenti di stato delle variabili).
- Alto livello di astrazione.
- Corrispondenza con processi logico-matematico.
Programmazione Funzionale
La programmazione funzionale è un paradigma di programmazione che, come dice il nome stesso, ruota attorno alla costruzione e applicazione di funzioni.
Nella programmazione funzionale, le funzioni sono trattate come cittadini di prima classe2, il che significa che possono essere legate a nomi (compresi gli identificatori locali), passate come argomenti e restituite da altre funzioni, proprio come qualsiasi altro tipo di dati.
La programmazione funzionale trova le sue fondamenta nel calcolo lambda, sviluppato da Alonzo Church nel 1930.
Le caratteristiche principali della programmazione funzionale sono:
- Funzioni pure: le funzioni sono deterministiche e non modificano lo stato esterno o variabili globali.
- Immutabilità del dato.
- Ricorsività.
- Funzioni trattate come first-class citizens2.
- Parallelismo e multi-threading.
- Assenza di loop espliciti (sostituiti da ricorsività e funzioni specifiche come map o reduce).
Alcuni esempi celebri di programmi basati sul paradigma funzionale sono Haskell, Scala, Lisp.
Qui viene riportato un esempio per individuare il fattoriale di un qualsiasi numero fornito come input; nella sua essenzialità è spesso considerato l'”hello world” di Haskell.
factorial :: Integer -> Integer factorial 1 = 1 factorial n = n * factorial (n - 1) main :: IO () main = do let n = 5 print $ factorial n 120
Programmazione Orientata agli Oggetti
La programmazione orientata agli oggetti (OOP) è un paradigma di programmazione oggi largamente diffuso nella creazione di applicativi software aventi un alto grado di astrazione e non strettamente legati alle performance o all’hardware sul quale essi vengono eseguito.
La programmazione orientata agli oggetti si basa si basa sul concetto di “oggetti”.
Gli oggetti sono istanze di classi, e una classe è una struttura che definisce gli attributi (dati) e i metodi (comportamenti) che gli oggetti di quella classe possiedono.
La programmazione a oggetti si basa su quattro concetti fondanti:
- Incapsulamento.
- Astrazione.
- Ereditarietà.
- Polimorfismo.
Una panoramica dettagliata della programmazione orientata agli oggetti è disponibile a questo link [Aziona: OOP 101: Introduzione all’Object-Oriented Programming].
Uno dei vantaggi principali della OOP è la capacità di modellare problemi complessi in modi organizzati e riutilizzabili, con un forte grado di astrazione rispetto al sottostante, permettendo agli sviluppatori di concentrarsi sulla logica risolutiva piuttosto che sui dettagli implementativi.
Volendo seguire un approccio strettamente formale, OOP può essere anche considerato un subset del paradigma di programmazione imperativo, con l’aggiunta della logica degli oggetti.
Programmazione Logica
La programmazione logica è un paradigma di programmazione che, intuitivamente, è fortemente basato sulla logica matematica e sul concetto di inferenza.
In questo paradigma, i programmi sono scritti in termini di relazioni logiche e regole che vengono utilizzate per dedurre informazioni o risolvere problemi.
La programmazione logica è alla base del linguaggio di programmazione Prolog.
Per le sue caratteristiche intrinseche, il paradigma della programmazione logica è usato per aggredire processi che richiedono analisi logica e inferenze, come l’elaborazione del linguaggio naturale, e diverse applicazioni dell’intelligenza artificiale e apprendimento automatico (Machine Learning).
La programmazione logica si basa sulle seguenti caratteristiche:
- Predicati e assiomi: definizione delle relazioni tra oggetti mediante affermazioni logiche (predicati), collegati mediante regole o assiomi.
- Unificazione: il processo per individuare istanze che soddisfino più predicati.
- Backtracking: il processo è percorribile in entrambi i sensi, e gli step possono essere percorsi a ritroso.
- Ricorsività.
- Ambito di conoscenza: gli assiomi o regole sono forniti da un esperto di dominio, in quanto le verità vanno accertate mediante la conoscenza delle relazioni.
Alcune applicazioni note della programmazione logica sono la Torre di Hanoi e il dilemma del Commesso Viaggiatore (TSP).
Conclusioni
É evidente che ciascuno dei seguenti paradigmi di programmazione meriterebbe, da solo, un articolo a sé stante. Pertanto non va ricercata esaustività nella presente panoramica.
É importante ricordare che i paradigmi di programmazione non sono strumenti, non possono essere propriamente “usati”. Essi sono modelli che possono eventualmente essere presi come riferimenti.
Altrettanto rilevante è notare che nel contesto informatico attuale tracciare confini netti può risultare se non difficoltoso, foriero di errori e incomprensioni. I paradigmi di programmazione stessa possono essere tra loro nidificati, collegati, e gerarchicamente associati in modi e sensi non univoci.
Le definizioni stesse in capo ai singoli paradigmi sono mutevoli e oggetti di costanti revisioni, in parallelo all’evoluzione di macchine, linguaggi, approcci e metodologie.
I paradigmi di programmazione sono essenziali per la scrittura di buon codice e per aderire a logiche univoche e integre, da utilizzare come binari all’interno del quale sviluppare le nostre applicazioni.
Note
[1] Sul tema si legga: ” Harper, Robert (1 May 2017). “What, if anything, is a programming-paradigm?”, e Krishnamurthi, Shriram (November 2008). “Teaching programming languages in a post-linnaean age”.
[2] Il termine “cittadino di prima classe” va qui inteso nel contesto specifico della computer science. Il cittadino di prima classe (“first class citizen”) è un’entità che supporta tutte le operazioni generalmente disponibili per le altre entità. Queste operazioni includono il passaggio come argomento, il ritorno da una funzione e l’assegnazione a una variabile.