Paradigmi di programmazione
Nelle lezioni precedenti abbiamo visto che programmare è un processo creativo, che coinvolge sia un procedimento induttivo (dal problema ad un modello) e deduttivo (da un modello ad un algoritmo). Abbiamo poi visto che lo sviluppo di una soluzione tiene conto del problema della complessità computazionale, che classifica i problemi in polinomiali e non polinomiali di cui abbiamo visto diversi esempi. Successivamente abbiamo approfondito diverse tecniche di programmazione avanzate che consentono di affrontare problemi computazionali complessi riducendo i tempi di elaborazione.
In questa lezione passiamo invece ad analizzare lo sviluppo vero e proprio.
La programmazione infatti, oltre che una attività creativa e progettuale, ha una parte tecnica che consiste nel trasformare un algoritmo in un insieme di istruzioni in un linguaggio di programmazione. Il programmatore quindi traduce l’algoritmo matematico in un insieme codificato di istruzioni e strutture dati che modellizza il problema. Accanto a questo però emergono altre problematiche di natura tecnica:
- la scrittura di istruzioni che permettano di ridurre gli errori e le ambiguità;
- ridurre la complessità del programma e delle strutture dati;
- scrivere codice che sia in parte riusabile per altri problemi simili;
- gestire la curva di apprendimento del linguaggio e delle tecnologie utilizzate.
Tutte queste attività rientrano nell’ambito di una vera sotto disciplina all’interno dell’informatica, chiamata ingegneria del software1 , che si occupa di definire metodologie, processi, e qualità del software. In questo senso nel corso della storia dell’informatica c’è stata una evoluzione nello sviluppo di tecniche di programmazione, contemporaneo e parallelo rispetto a quello dello sviluppo di algoritmi, che ha visto passare da una programmazione vicina al linguaggio della macchina, ai sistemi attuali che invece privilegiano l’astrazione ed un approccio più vicino al modello concettuale del problema da risolvere.
In particolare si sono sviluppati veri e propri paradigmi di programmazione, cioè modelli e concezioni generali su come deve essere sviluppato un programma, quali devono essere le caratteristiche di un linguaggio di programmazione, e come devono essere strutturati i dati che dovrà elaborare.
In questa lezione esploreremo i principali paradigmi della storia dell’informatica e la loro evoluzione.
Programmazione procedurale
Coi primi computer, ed in particolare con la macchina di Von Neumann (1947), i primi linguaggi di programmazione basati su operazioni basilari che doveva svolgere la macchina e che costituivano un paradigma di programmazione per cui un algoritmo veniva codificato in 3 istruzioni fondamentali:
- di assegnazione/calcolo
- salto condizionato (if)
- salto incondizionato (goto)
Erano previsti sottoprogrammi per funzioni ripetute ed era prevista una memoria unica suddivisa in uno heap (comune a tutti i sottoprogrammi) ed uno stack (separato per ogni sottoprogramma in esecuzione).
In questo paradigma i dati sono codificati come numeri binari (anche lettere, stringhe, numeri, ecc.) e non c’è nessuna astrazione. Il computer era infatti come una evoluzione della calcolatrice ed il programma era fatto per svolgere una procedura di elaborazione, con un input, uno stato iniziale, un output ed uno stato finale. Non era prevista interattività.
L’attività del programmatore quindi era quella di codificare il problema in un insieme di dati ed istruzioni basilari, tramite un enorme lavoro di astrazione da parte del programmatore, in genere un ingegnere o un matematico esperto.
La programmazione procedurale ha dato poi vita ai primi linguaggi di programmazione ad alto livello, come Fortran, Ada e Basic, che accorpavano molte istruzioni basilari in una sola, ma che riproducevano comunque sempre lo stesso modello.
Programmazione strutturata
A partire dagli anni 60 il paradigma procedurale venne messo in discussione sia per l’evoluzione tecnologica, che dava ai computer la possibilità di essere interattivi tramite terminali con tastiera e schermo, sia per la sua eccessiva astrazione nella predisposizione dei dati. Uno dei principali problemi era però nella complessità di gestione del flusso del programma, legato all’istruzione goto.
Nel 1966 Bohm-Jacopini dimostrano col loro celebre teorema che qualunque algoritmo non necessita del goto ma può essere implementato tramite queste tre istruzioni fondamentali:
- assegnazione/calcolo
- condizione
- ciclo
Dijkstra nel 1968 dimostrò poi che il goto non solo non era necessario, ma era proprio dannoso perché poteva permettere salti incondizionati in parti diverse del programma producendo quello che venne chiamato “spaghetti code”.
Qui un esempio di Spaghetti Code (calcolo del Pi greco) scritto in linguaggio Basic.
10 LET PI = 0
20 LET I = 0
30 LET FLAG = 1
40 LET MAX = 1000000
50 IF FLAG = 1 THEN GOTO 70
60 GOTO 90
70 LET PI = PI + 4 / (2 * I + 1)
80 GOTO 100
90 LET PI = PI - 4 / (2 * I + 1)
100 LET I = I + 1
110 IF FLAG = 1 THEN LET FLAG = 0 ELSE LET FLAG = 1
120 IF I < MAX THEN GOTO 50
130 PRINT "Valore approssimato di PI: "; PI
140 END
Come si vede il goto infatti può essere usato in modo indiscriminato per saltare da un punto qualsiasi del programma ad un altro. Questo comporta una eccessiva libertà al programmatore e quindi errori in esecuzione difficili da individuare.
Il paradigma che venne quindi proposto era quello di utilizzare le strutture algoritmiche di Bohm-Jacopini, chiamato della programmazione strutturata, che è alla base di una nuova generazione di linguaggi come il Pascal ma sopratuttto il C2, mentre altri (come Ada) si sono adattati al nuovo paradigma. Qui un esempio dello stesso codice in C/C++.
#include <iostream>
using namespace std;
double calcolaPi(int termini) {
double pi = 0.0;
for (int i = 0; i < termini; ++i) {
double termine = 4.0 / (2 * i + 1);
if (i % 2 == 0) {
pi += termine;
} else {
pi -= termine;
}
}
return pi;
}
int main() {
int n:
cout << "Inserisci il numero di iterazioni: ";
cin >> n;
double pi = calcolaPi(n);
cout << << pi << endl;
return 0;
}
La programmazione strutturata introduce al posto del sottoprogramma il concetto di funzione e inoltre permette di creare strutture dati più complesse, come array e struct semplificando il modello concettuale del programma con strutture matematiche di facile comprensione per tutti.
Con la programmazione strutturata viene introdotto il concetto di diagramma di flusso, uno strumento che permette di modellizzare algoritmi in modo visuale e rende più semplice la sua codifica in un programma.

La programmazione strutturata ha “democratizzato” l’accesso alla programmazione a tutti, ed è diventato il paradigma fondamentale della programmazione e come tale è usato ancora oggi per lo sviluppo di software, soprattutto in progetti semplici. E’ supportato da quasi tutti i linguaggi di programmazione ed è ancora oggi la prima tecnica di programmazione che impara un programmatore grazie alla sua relativa semplicità.
Programmazione ad oggetti
L’informatica però a partire dagli anni ’70-’80 si evolve ulteriormente: compaiono le prime interfacce grafiche, cominciano ad essere sviluppati programmi molto complessi, spesso interattivi, che gestiscono grandi basi di dati formate da molte strutture dati collegate tra loro, compaiono i primi videogiochi e le prime applicazioni client-server, specie su Internet. La programmazione strutturata comincia ad evidenziare grossi limiti nel gestire applicazioni interattive, complesse e con database relazionali. Le funzioni infatti possono diventare numerose e complesse, così come la gestione dei dati ad esse collegate perché bisogna tenere traccia di chi fa cosa.
La prima soluzione adottata è stata la programmazione modulare, che consisteva nel suddividere un grosso programma in moduli, porzioni di programma autonome che svolgono un insieme di funzioni specifiche e che sono riutilizzabili anche in futuri sviluppi di altri programmi. I principali linguaggi di programmazione (C, Cobol, Pascal, ecc.) vennero quindi adattati alla programmazione modulare, altri vennero inventati appositamente (Modula-2).
Il vero salto evolutivo lo si ha avuto però con la programmazione orientata ad oggetti (OOP), che introduce un nuovo modo di progettare le applicazioni. Nella programmazione strutturata le strutture dati sono memorizzate in variabili e le funzioni sono elementi di programma che le gestiscono. Nella programmazione ad oggetti invece dati e funzioni vengono incapsulati in un solo “oggetto” che contiene sia i dati che le funzioni per utilizzarli. I dati sono chiamati “proprietà” dell’oggetto, e le funzioni sono dette “metodi“. Con gli oggetti l’obiettivo è quindi quello di collegare dati e sottoprogrammi in una unica entità autonoma che svolge una piccola porzione del programma in modo indipendente dalle altre con cui collabora.
La programmazione ad oggetti prevede quindi una rete di oggetti che rappresentano ognuno una porzione del problema, del modello e del programma.
La programmazione quindi non consiste più nel creare un algoritmo (scomposto in funzioni) che elabora dati ma nel partire invece dai dati stessi per creare una modello concettuale dove la realtà del problema è definita tramite una rete (grafo) di oggetti che interagiscono tra loro, tramite relazioni di utilizzo, ereditarietà, estensione, ecc. Questo modello è molto efficace perché rende molto più semplice progettare applicazioni grafiche ed interattive, modellizzare strutture dati composte da entità distinte tra loro, e creare strutture di oggetti riusabili in applicazioni differenti.

Per la modellizzazione è introdotto un vero e proprio linguaggio grafico, UML, che come si vede dalla figura permette di modellizzare schematicamente oggetti ma anche il comportamento dinamico delle applicazioni.
Con la programmazione ad oggetti sono stati introdotti nuovi linguaggi, come C++. ed altri successivi. Ma è in particolare Java che è diventato il linguaggio di riferimento intorno al quale si è evoluto questo paradigma di programmazione, con l’introduzione di nuovi concetti, come la programmazione generica e le interfacce. Altri linguaggi hanno invece preferito avere un approccio multiparadigma, come Javascript, Python, Kotlin, Swift dove la programmazione ad oggetti è possibile ma opzionale.
Oggi la maggior parte dei linguaggi prevede una qualche forma di programmazione ad oggetti e questo paradigma è il più diffuso oggi. Non va comunque pensato come alternativo alla programmazione strutturata ma come una sua estensione, specie per progetti complessi.
Programmazione funzionale
La programmazione ad oggetti è efficace per risolvere molti problemi ma non è l’unica evoluzione della programmazione strutturata. Se la programmazione ad oggetti si concentra nei dati e nella loro incapsulazione in oggetti, un modello alternativo si è invece concentrato sul vero e proprio flusso dell’algoritmo, cioè sulle azioni (o funzioni) di trasformazione, a partire da input che tramite un insieme di funzioni collegate tra loro produce un output di una elaborazione. Questo modello viene chiamato programmazione funzionale e supera il concetto di diagramma di flusso perché non prevede esplicitamente nè condizioni nè cicli.
In questo modello il programma è costituito da una funzione che a sua volta è composta da una sequenza o una combinazione di funzioni dove ciascuna svolge una porzione dell’algoritmo di calcolo e che restituisce alla funzione successiva una elaborazione parziale, dove il dato non viene modificato ma viene elaborato e ne viene generata una nuova copia modificata per la funzione successiva della sequenza. Il programma quindi è la somma di tutte le operazioni definite dalle funzioni secondo una determinata sequenza che infine genera un risultato di output.
Qui un semplice esempio col calcolo del Pi greco col paradigma funzionale:
const n = 10000;
const value = Array
.from({length: n})
.map((_, i) => {
return (i % 2 === 0 ? 1 : -1) * (4 / (2 * i + 1))
})
.reduce((sum, element) => sum+=element, 0);
La funzione from genera un array di dimensione n che viene passato alla funzione successiva, map, che a sua volta inserisce nell’array per ogni elemento alla posizione i il valore calcolato. Come si può vedere non esiste alcun ciclo for, ci pensa la funzione map ad eseguire l’operazione su ogni elemento dell’array e a produrre un nuovo array, sempre di n elementi alla funzione successiva. Infine c’è l’istruzione reduce, che esegue internamente un ciclo su ogni elemento e lo somma alla variabile sum.

Questo meccanismo, detto pipeline, permette di comporre una operazione complessa come un insieme di operazioni più semplici. Allo stesso tempo però è bene osservare che le singole operazioni non sono composte da istruzioni o comandi che scompongono esplicitamente una struttura dati (ad esempio un array) ma agiscono in modo implicito su tutto l’array. E’ il linguaggio di programmazione che si occupa di eseguire internamente i cicli, creare condizioni, assegnare variabili locali, ecc. La programmazione funzionale si basa sul fatto che anziché fare più azioni all’interno dello stesso ciclo, rendendolo complesso in termini computazionali, sia più utile fare più fasi con una singola operazione (e quindi svolgere più cicli in sequenza). Come abbiamo visto con l’esempio sopra indicato vengono svolti 3 cicli più semplici (creazione, generazione e somma) anzichè un unico grande ciclo complesso dove ad ogni iterazione si svolgono le 3 operazioni.
In programmazione funzionale i cicli e condizioni sono nascosti o eliminati e si lavora direttamente ad alto livello su intere strutture dati complesse anziché su singoli elementi. Non solo ma ogni funzione non memorizza internamente dati, non conserva un suo stato interno (come gli oggetti), ma agisce come “puro flusso” di dati.
Questa metodologia di programmazione semplifica l’analisi e la modellizzazione del problema e la sua traduzione in codice, specie per le applicazioni di calcolo (intelligenza artificiale, videogiochi) ed interattive (web e mobile).3 Questo procedere per flussi sull’intero insieme di dati, con singole elaborazioni separate, è particolarmente adatto proprio alle tecnologie e le problematiche legate ai computer moderni, che possono parallelizzare le operazioni tramite cpu multiprocessore, e possono lavorare in tempo reale su eventi utente o di sistema (come avviene su pc, smartphone e server di rete).
Questo modello appare inizialmente complesso e di difficile comprensione per i programmatori che vengono dalla programmazione strutturata (chiamata anche imperativa), per questa ragione è restata per decenni un modello di nicchia. Ha trovato invece successo negli ultimi anni a causa dello sviluppo di tecnologie come lo sviluppo web e mobile, come anche l’analisi dati (big data) e soprattutto l’intelligenza artificiale, in particolare le reti neurali.
Per questo tutte le ultime versioni dei linguaggi ad oggetti hanno introdotto caratteristiche funzionali come in Java (versione 8) e C# (con LINQ), Python, Javascript, e infine Kotlin, Swift, mentre si sono sviluppati linguaggi specificatamente funzionali, come F# e Scala.
Conclusioni
La metodologia di programmazione è uno strumento che ci offre un modo per generalizzare un problema (tramite procedure, strutture dati, moduli o trasformazioni) e costruire il programma (tramite procedure, funzioni, oggetti o flussi). La metodologia è chiamata anche paradigma, perché dietro ad essa non c’è solo una guida a risolvere determinati problemi, ma c’è una intera visione “filosofica” di cosa si intende per fare software, come deve funzionare, ed è influenzato da molti aspetti, non solo tecnologici, ma culturali ed appunto filosofici.
E’ importante capire che è il paradigma di programmazione a guidare la creazione e l’utilizzo di un linguaggio di programmazione. Chi infatti progetta un linguaggio di programmazione lo fa sulla base di un paradigma di programmazione in modo da agevolare lo sviluppo di applicazioni secondo quel modo di concepire i problemi e le soluzioni, e questo condiziona anche il modo in cui si scrive software ed il modo in cui si questo verrà organizzato. La scelta del linguaggio è quindi fatta a valle di uno specifico paradigma, in quanto il linguaggio ne dipende fortemente.
E’ inoltre fondamentale capire che i paradigmi non sono verità immutabili ma sono frutto di una evoluzione storica, che dipende dallo sviluppo dell’informatica come disciplina come evoluzione o messa in discussione di paradigmi precedenti, e sono il frutto di una visione culturale, dell’esperienza accumulata e dalle tecnologie che si sono evolute nel tempo. Pertanto vanno visti in ottica evolutiva e mai come qualcosa di definitivo: anche oggi sono costantemente oggetto di revisione, evoluzione e modifica.
- Nel corso degli ultimi decenni l’insieme di queste conoscenze sono diventate una vera e propria disciplina, chiamata ingegneria del software. ↩︎
- in realtà il C consente l’uso di goto, anche se deprecato. ↩︎
- La programmazione funzionale si adatta poi particolarmente al calcolo parallelo. Infatti ogni fase dell’elaborazione può essere demandata (in architetture multicore come i modern pc e smartphone) a processori differenti nella stessa elaborazione e quindi ottimizzare al massimo le prestazioni di un sistema direttamente a runtime, senza una progettazione preliminare da parte del programmatore. ↩︎