Viaggio tra i paradigmi

Un’introduzione ai 3 principali paradigmi di programmazione

Annunci

Un paradigma di programmazione è un insieme di concetti che permette allo sviluppatore di modellare il problema da risolvere secondo un gruppo ben definito di regole.

I fondamentali paradigmi di programmazione sono:

  • La programmazione strutturata
  • La programmazione orientata agli oggetti
  • La programmazione funzionale

I 3 precedenti paradigmi sono stati utilizzati nell’ordine in cui sono scritti, ma, curiosamente sono stati inventati nell’ordine opposto.

Programmazione strutturata

La programmazione strutturata è stata per la prima volta teorizzata da Edsger Dijkstra, matematico e informatico olandese, uno dei padri della computer science.

Dijkstra era convinto che, per validare i programmi, fosse necessario dimostrarli matematicamente. Riuscì a dimostrare la validità di alcune strutture di controllo, come sequenze di operazioni, cicli e istruzioni condizionali.
Scoprì inoltre che era impossibile dimostrare la correttezza di programmi che contenessero salti incondizionati (goto), e, alla fine degli anni 60, pubblicò un articolo in cui metteva in evidenza i rischi dell’uso di tali costrutti.

Questo aprì una diatriba durata una decina d’anni, ma alla fine degli anni 70 fu pienamente accettato che i soli costrutti validi per il controllo di flusso fossero le istruzioni di tipo if … then … else e do … while.
Il famigerato goto venne messo all’angolo e non più permesso in molti linguaggi per un po’ si accettò che il goto fosse utilizzato nella gestione degli errori; poi con l’avvento delle eccezioni anche questo tipo di utilizzo fu abbandonato.

Dijkstra sosteneva inoltre che ogni funzione dovesse avere un solo punto di ingresso e un solo punto di uscita; in realtà negli ultimi decenni è stato in parte sdoganato il fatto di poter avere più di un punto di uscita: infatti in alcuni casi può essere più espressivo avere 2 o 3 punti di uscita, ma solo a patto che la funzione sia molto corta.

La dimostrabilità dei programmi invece non prese mai piede, per vari motivi:

  • Dopo Dijkstra non fu più portato avanti l’impianto matematico teorico che sarebbe servito per dare fondamento alla dimostrabilità dei programmi
  • Non tutti i programmatori sono dei matematici
  • I tempi per la dimostrazione di ogni pezzo di programma probabilmente non sarebbero compatibili con i tempi di consegna (ma questo non possiamo dirlo, infatti si sarebbero potuti creare degli strumenti automatici per la dimostrazione)

Ancora oggi che usiamo il paradigma ad oggetti o quello funzionale, il corpo degli algoritmi è scritto secondo le regole della programmazione strutturata, che quindi costituisce l’ossatura dei nostri programmi.
 

Programmazione ad oggetti

La programmazione ad oggetti viene spesso descritta ponendo l’accento sui dati e dicendo che permette di avere delle strutture che contengono sia i dati che le funzioni (metodi) che servono per manipolarli.
Questo in realtà è vero solo perché i linguaggi OO usano una notazione del tipo oggetto.funzione(), che però non è diverso da dire funzione(oggetto).
Una definizione che mi piace di più è quella che la programmazione ad oggetti permette di rappresentare in modo più chiaro le relazioni esistenti tra le entità del sistema (contenimento, specializzazione, uso).

Il paradigma è inoltre identificato con le 3 funzionalità principali, che sono:

  • Incapsulamento
  • Ereditarietà
  • Polimorfismo

In realtà l’incapsulamento, cioè la separazione tra l’interfaccia e l’implementazione, già era presente in C. Infatti tutto ciò che serviva al programma client per usare una libreria era il file .h da includere. Con i linguaggi ad oggetti l’incapsulamento diventa comunque più facile da ottenere.

L’ereditarietà era in qualche misura già possibile, perché si poteva utilizzare una struct al posto di un’altra, a patto che i campi comuni seguissero lo stesso ordine e fossero dello stesso tipo.
Sicuramente i linguaggi ad oggetti hanno reso più semplice e sicuro l’uso dell’ereditarietà, che prima doveva essere costruita e gestita manualmente.

Il polimorfismo, che forse costituisce l’apporto più importante della programmazione ad oggetti, ha permesso di fare in modo chiaro ed esplicito ciò che prima si poteva ottenere in modo più macchinoso, utilizzando i puntatori a funzione.

Quindi potremmo dire che la programmazione ad oggetti ha rivestito in modo più chiaro e sicuro una serie di operazioni già possibili in precedenza, ma che costavano molto dal punto di vista della chiarezza del codice, della possibilità di errori e della produttività:

  • Ha permesso di definire in modo più chiaro quali sono gli attori che fanno parte dell’applicazione e come interagiscono tra loro attraverso lo scambio di messaggi.
  • Ha reso più semplice, sicuro e controllato dal compilatore l’utilizzo dell’ereditarietà e del polimorfismo, permettendo a tutti di utilizzare queste funzionalità. Questo ha determinato un notevole aumento di produttività rispetto ai linguaggi precedenti.
  • Attraverso il polimorfismo e quel che ne consegue, ha permesso di introdurre il concetto di separazione e disaccoppiamento tra i componenti di un’applicazione, permettendoci di generalizzare certe parti di codice e di renderle indipendenti dai cambiamenti (o renderle almeno più resistenti).
  • Con la dependency injection ha permesso di inserire la dipendenza direttamente a run-time, consentendo di costruire applicazioni ancora più generiche; questo anche grazie a numerosi framework nati attorno a questi linguaggi.

Programmazione funzionale

La programmazione funzionale è stato il primo paradigma ad essere emerso (risale agli studi del matematico Alonzo Church negli anni 30), e l’ultimo ad essere adottato.
Questo stile di programmazione ha alcune caratteristiche che lo contraddistinguono dagli altri:

  • L’immutabilità delle variabili
  • La funzione come unità di base
  • La struttura dichiarativa

L’immutabilità (quasi totale) si attua nel fatto che se si vuole modificare il valore di una variabile, è necessario crearne una nuova con il nuovo valore.
L’immutabilità delle variabili in realtà non è assoluta: esistono infatti meccanismi per modificare il valore delle variabili, ma sono espliciti ed estremamente controllati, ad esempio con l’uso di una memoria transazionale (simile al funzionamento delle transazioni su un database).

L’immutabilità è il motivo per cui, negli ultimi anni, la programmazione funzionale è uscita dall’ambito prettamente accademico: infatti tutti i problemi che derivano dalla concorrenza e dal multi threading sono dovuti ai cambiamenti di stato delle variabili:

  • Se le variabili non sono protette, si possono verificare delle variazioni indesiderate (un thread modifica un valore, e il thread successivo trova quel valore modificato).
  • D’altra parte, se le variabili sono protette con dei lock, si può incorrere in condizioni di deadlock.

L’immutabilità ci ripara da questi effetti indesiderati.

Il prezzo da pagare è un cambiamento di prospettiva che di solito mette in difficoltà gli sviluppatori che utilizzano altri paradigmi:

  • In primo luogo l’unità fondamentale è la funzione, che riceve una serie di parametri in ingresso (che possono essere a loro volta funzioni) e che dà un risultato (anch’esso può essere una funzione). Queste funzioni possono essere assimilate alle funzioni nel senso matematico del termine, e, a parità di ingressi, generano sempre la stessa uscita (funzioni pure).
  • Il codice è poi scritto in maniera dichiarativa anziché imperativa (sequenza di comandi) come avviene nei linguaggi OO e in quelli procedurali.
  • L’iterazione è sostituita dalla ricorsione. Ottimizzazioni effettuate dal compilatore permettono in alcuni casi di risparmiare spazio sullo stack e di lo stesso codice macchina che si sarebbe ottenuto se si fosse usata un’iterazione

I linguaggi puramente funzionali non sono ancora molto diffusi. Essi hanno però influenzato gli altri linguaggi (per esempio le ultime versioni di Javascript, Python, C# hanno delle caratteristiche funzionali, come le espressioni lambda, ma non solo).
Inoltre si sono diffuse delle buone pratiche nella scrittura delle funzioni nei linguaggi non funzionali, come per esempio evitare di generare degli effetti collaterali, se non esplicitamente indicato.

Rispondi

Inserisci i tuoi dati qui sotto o clicca su un'icona per effettuare l'accesso:

Logo WordPress.com

Stai commentando usando il tuo account WordPress.com. Chiudi sessione /  Modifica )

Google+ photo

Stai commentando usando il tuo account Google+. Chiudi sessione /  Modifica )

Foto Twitter

Stai commentando usando il tuo account Twitter. Chiudi sessione /  Modifica )

Foto di Facebook

Stai commentando usando il tuo account Facebook. Chiudi sessione /  Modifica )

Connessione a %s...