Paradigmi di Programmazione

Cos'è un paradigma di programmazione ?  : origine del termine e perché si è reso necessario introdurlo nel contesto della programmazione
Principali paradigmi di programmazione   : programmazione procedurale e dichiarativa
Programmazione funzionale   : principi e trasparenza referenziale
Programmazione logica   : principi e  PROLOG
Integrazione e purezza dei paradigmi

1. Cos'è un paradigma di programmazione?

Il significato scientifico del termine "paradigma" è relativamente recente [Kuhn62] :  "un insieme di idee scientifiche collettivamente accettato per dare un senso al mondo dei fenomeni". Nel caso particolare della programmazione si è sentita la necessità di distinguere fra diversi modi di intendere la programmazione  quando è stato accettato il fatto che non esisteva un unico modello computazionale -il classico Von Neumann- cui riferirsi.

La questione si è posta già negli anni '60 con la programmazione funzionale, ma il contributo più decisivo in questo senso è stato dato probabilmente negli anni '70 dalla cosiddetta programmazione logica prima,  e dal boom di quella ad oggetti, dopo. Oggi i paradigmi di programmazione citati con una certa frequenza sono almeno una decina, anche se non sempre sono chiari i loro confini o la loro indipendenza. I paradigmi offrono una chiave di classificazione dei linguaggi: diremo che un linguaggio afferisce ad un paradigma se consente di scrivere i programmi in accordo ad esso.

2. Principali paradigmi di programmazione

Semplificando al massimo, possiamo limitarci a considerare due tipi di paradigmi di programmazione :

  1. quelli di tipo dichiarativo che privilegiano il punto di vista per cui i programmi devono essere delle specifiche eseguibili del problema: ci si preoccupa cioè di cosa risolvere;  il "come" risolverli viene in qualche modo scaricato sull'esecutore del linguaggio. In generale questo approccio comporta una certa inefficienza.

  2. chiarimento
  3. quelli di tipo procedurale che privilegiano il punto di vista per cui i programmi vengono considerati  alla stregua degli algoritmi : ci si preoccupa quindi direttamente del come risolvere il problema trascurando o sottointendendo le specifiche del problema, cioè il "cosa" .

I principali paradigmi di tipo dichiarativo sono quelli funzionale  e quello  logico,  basati rispettivamente sul concetto di espressione e di predicato, come vedremo tra breve. 

I principali paradigmi procedurali sono quello imperativo e ad oggetti

Il paradigma imperativo è quello classico in cui rientrano i linguaggi macchina dei   comuni microprocessori e i linguaggi ad al alto livello più vecchi e diffusi come FORTRAN, COBOL, BASIC, Pascal. Quello ad  oggetti è un'evoluzione di quello imperativo. Entrambi i paradigmi sono infatti basati sul concetto di stato e sulla possibilità di modificarlo attraverso l'esecuzione di comandi. Le differenze riguardano soprattutto la "distribuzione" e  le modalità per l'accesso dello stato complessivo .

3. Programmazione funzionale

Il paradigma di programmazione funzionale  trova le origini storiche nel lambda-calcolo, il  linguaggio introdotto negli anni '30 dal matematico Alonzo Church come formalismo universale (secondo appunto la nota Tesi di Church-Turing), e la cui caratteristica essenziale, ritrovabile anche in moderni linguaggi funzionali, è che tutto è  un'espressione, e come tale  denota un valore ottenibile da un processo di valutazione.

La parola "funzionale" si deve al fatto che ogni simbolo di  operazione che compare in un'espressione corrisponde ad una funzione, sia nel senso matematico del termine che in quello informatico ("sottoprogramma che restituisce un valore").  

Le espressioni e i sottoprogrammi funzionali sono del resto presenti anche nei primi linguaggi procedurali.

il cartello a sinistra intende simboleggiare il divieto dell'uso del := nella programmazione funzionale. Il cartello a destra denota il "via libera" alle funzioni (rappresentate da 'lambda')

Nei linguaggi funzionali esistono però dei meccanismi per cui è possibile rinunciare al concetto di comando e di effetto, così come ai concetti ad esso correlati:  procedura,   ciclo, assegnamento, sostituendoli con altri, in accordo alla seguente tabella:

IMPERATIVO FUNZIONALE
procedura funzione
ciclo ricorsione
test (if, case) espressioni condizionali, pattern matching
assegnamento legame (param. formale-param. attuale)
sequenzializzazione composizione di funzioni
confronto funzionale-imperativo

 

Il risultato è quello di avere  dei programmi che godono della seguente proprietà, nota come "sostituibilità di uguali con uguali" nel tradizionale ambito algebrico:

Trasparenza referenziale:

in una espressione H si possono sempre sostituire le  occorrenze di una sottoespressione e con il suo valore v senza che cambi il valore di H.

algebrica

 

Se gli array sono la struttura dati caratteristica della programmazione imperativa, le liste lo sono della programmazione funzionale. Le operazioni più caratteristiche (la sintassi è solo indicativa) sono:

elab. di liste
OPERAZIONE RESTITUISCE
Testa(L)    il primo elemento di una lista L (non vuota)
Coda(L)        la lista L privata del primo elemento
[ A | L]      la lista la cui testa è A e la cui coda è L
[ espr1, espr2, ..., esprn]          la lista i cui elementi sono i valori delle espressioni specificate

Infine, i linguaggi funzionali, per dirsi veramente tali, devono prevedere le funzioni di ordine superiore:  accettare  funzioni come argomento, restituire funzioni come risultato e  incorporare le funzioni nelle strutture dati. Queste possibilità, previste con certe limitazioni anche in altri paradigmi, consentono spesso di ottenere del codice con sorprendenti caratteristiche di eleganza e di generalità.

Infine occorre tenere presente che i moderni linguaggi funzionali prevedono la lazy evaluation ("valutazione pigra" o non-strict) cioè la possibilità di ritardare la valutazione degli argomenti attuali di una funzione fino al momento in cui essi si rendono effettivamente necessari.

4. Programmazione logica

Il paradigma di programmazione logica  si basa sull'idea di considerare i predicati logici del primo ordine (ristretti ad una certa forma sintattica) come delle specifiche eseguibili. Negli anni 60'-70' si è scoperto che ciò può avvenire  in modo relativamente efficiente applicando un'unica regola di inferenza, nota come principio di risoluzione (Robinson, 1960), poco naturale per l'uomo, ma molto adatta per l'esecuzione automatica.   Questa regola di inferenza, nella versione ristretta al calcolo proposizionale, si enuncia come segue:

PRINCIPIO DI RISOLUZIONE (semplificato)

dalla coppia di clausole (genitori):   A  or B  ,   (not A) or C
si può dedurre la clausola (risolvente):    B or C

  

Nel caso della logica del primo ordine  A, B e C possono assumere  la forma sintattica di proposizioni atomiche :

nomepredicato(t1,t2, ..., tn)

dove t1, t2, ..., tn sono termini. Questi possono essere di tre tipi:

La presenza dei termini richiede, per applicare la regola di risoluzione,  l'intervento di un algoritmo di unificazione per tentare di rendere esattamente opposte due proposizioni della forma   p1(t1,t2 ...,  tk) e not p1(t1',t2', ..., tk') attraverso un'opportuna sostituzione delle variabili  in esse presenti.

I programmi logici (puri) sono insiemi  finiti di predicati del primo ordine, in una forma particolare, detta forma a clausole di Horn :

conclusione   [ se  premessa1premessa2, ...,  premessak ]

dove conclusionepremessa1premessa2, ...,  premessak sono proposizioni atomiche. Se manca la parte opzionale (quella con il se), allora  le clausole si chiamano "fatti"  altrimenti, si parla di "regole" .

Esempi di clausole

Oltre alle clausole programma (fatti e regole)  appena viste  è possibile specificare anche delle clausole goal o interrogazioni, nella forma:

prop1prop2, ...,  propn

La semantica di un goal è:

"cerca, se esistono, delle sostituzioni alle variabili presenti nel goal che rendano contemporaneamente vere tutte le sue proposizioni, usando delle dimostrazioni basate sulle clausole programma e sul principio di risoluzione"

Rispetto alle normali computazioni, il "calcolo logico", ha le   caratteristiche del cosiddetto non-determinismo:

Il linguaggio di programmazione logica più noto è certamente il PROLOG. Rispetto alla programmazione logica, la sua semantica è di fatto incompleta: vale a dire che, a causa di una rigida strategia deduttiva (la cui adozione è imposta da motivi di efficienza), l'interprete PROLOG non sempre riesce a trovare le dimostrazioni di un goal, anche quando esistono.

Il PROLOG è generalmente dotato di agevolazioni sintattiche per il trattamento di liste, con una sintassi molto simile a quella presentata nella sezione dedicata alla programmazione funzionale.

La programmazione logica ha vissuto negli anni '80 un entusiasmo rivelatosi eccessivo rispetto alle attese  e di fatto oggi sensibilmente ridimensionato. In molti casi si è scoperto che gli stessi risultati possono essere ottenuti in modo relativamente facile, e comunque molto più efficiente, con paradigmi più tradizionali. Tuttavia, ancor oggi, la programmazione logica trova molte applicazioni, spesso con sorprendente concisione ed eleganza, soprattutto in settori particolari dell'intelligenza artificiale come l'elaborazione simbolica, la deduzione e i sistemi esperti.

5. Integrazione e purezza dei paradigmi

Rispetto alla varietà  dei problemi e delle applicazioni concrete, ogni paradigma presenta propri vantaggi e svantaggi. L'adozione di un linguaggio che metta a disposizione in modo coerente un unico paradigma (linguaggio "puro") può comportare, in particolare, il ricorso a tecniche oscure e/o inefficienti per sopperire alla mancanza di  paradigmi diversi da quello ospitato.

In pratica si preferisce aggiungere caratteristiche "impure" a linguaggi già esistenti che consentano di ospitare, almeno parzialmente, dei  paradigmi "secondari" rispetto a quello principale. A volte lo stesso scopo viene raggiunto aggiungendo particolari librerie. Ad esempio alcuni linguaggi ad oggetti mettono a disposizione delle librerie a supporto della programmazione logica.

Un'altra soluzione, spesso adottata, è quella di interfacciarsi con altri linguaggi. Molti PROLOG prevedono, ad esempio, l'interfacciamento con routine scritte in C o in Pascal.

L'ideale sarebbe di disporre di linguaggi multiparadigma, cioè che consentano di applicare il paradigma più adatto al sotto-problema che interessa "cucendo" poi le soluzioni con appositi meccanismi di interfacciamento (integrazione) tra paradigmi.

esempio multiparadigma

Al momento esistono solo alcune proposte di ricerca (vedi ad es. il linguaggio L3P) e non sembra vicino il giorno in cui si potrà contare su  uno standard accettatato e sufficientemente diffuso.