|
|
![]() |
![]() |
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 |
![]() |
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.
Semplificando al massimo, possiamo limitarci a considerare due tipi di paradigmi di programmazione :
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.
![]() |
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 .
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.
|
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 |
![]() |
Il risultato è quello di avere dei programmi che godono della seguente proprietà, nota come "sostituibilità di uguali con uguali" nel tradizionale ambito algebrico:
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. |
![]() |
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:
![]() |
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à.
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:
variabili es. X,Y,Z, ...
costanti es. a, b, zero, vuoto, ...
termini composti es. f(a), g(b,X), h( f(a), g(Y,Y)) , succ(succ(zero)) , ...
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 premessa1, premessa2, ..., premessak ] |
dove conclusione , premessa1, premessa2, ..., premessak sono proposizioni atomiche. Se manca la parte opzionale (quella con il se), allora le clausole si chiamano "fatti" altrimenti, si parla di "regole" .
![]() |
Oltre alle clausole programma (fatti e regole) appena viste è possibile specificare anche delle clausole goal o interrogazioni, nella forma:
prop1, prop2, ..., 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:
può terminare con insuccesso
può concludersi con più di un caso di successo
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.
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.
![]() |
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.