ALGORITMI, MACCHINE,
GRAMMATICHE
MARCELLO FRIXIONE
Dip. di Scienze della Comunicazione,
Università di Salerno
[email protected]
2008
Algoritmi, Macchine, Grammatiche
1
1. Il concetto di algoritmo
1.1 Che cos’è un algoritmo
Gli algoritmi sono metodi per la soluzione di problemi. Possiamo caratterizzare un
problema mediante i dati di cui si dispone all’inizio e dei risultati che si vogliono
ottenere: risolvere un problema significa ottenere in uscita i risultati desiderati a partire
da un certo insieme di dati presi in ingresso. I dati in ingresso vengono anche detti
(valori in) input e i risultati in uscita (valori in) output. Possiamo assumere che ciascun
problema consista di un insieme di casi particolari, o istanze. Ogni istanza di un
problema è caratterizzata da un insieme specifico di dati in ingresso e da un determinato
risultato. Supponiamo ad esempio che il problema generale consista nel calcolare la
lunghezza dell’ipotenusa di un triangolo rettangolo date le lunghezze dei due cateti. In
questo caso le istanze del problema corrispondono agli specifici triangoli di cui
calcolare l’ipotenusa. Le informazioni in ingresso (i dati in input) sono le lunghezze dei
cateti; il risultato che ci si attende in uscita (l’output) è la lunghezza dell’ipotenusa.
Un algoritmo per risolvere un problema è un metodo che consente di calcolare il
risultato desiderato a partire dai dati di partenza. Cioè, a partire dai dati in input,
consente di calcolare l’output corrispondente. Il comportamento di un algoritmo può
essere schematizzato come nella fig. 1-1.
ALGORITMO
Dati in Ingresso
(Input)
Risultati in Uscita
(Output)
Figura 1-1
Affinché un metodo per la soluzione di un problema costituisca un algoritmo deve
essere totalmente esplicito: vanno specificati in maniera precisa e particolareggiata tutti
i passi del procedimento da eseguire per ottenere i risultati in uscita a partire dai dati in
ingresso. Nel caso del triangolo rettangolo un possibile algoritmo è costituito dalla
seguente lista di istruzioni:
•
•
•
•
•
•
•
si prenda in input la lunghezza A del primo cateto
si prenda in input la lunghezza B del secondo cateto
si calcoli il quadrato di A
si calcoli il quadrato di B
si sommino i due quadrati
si estragga la radice quadrata del valore così ottenuto
si produca in output quest’ultimo valore
In generale, un algoritmo è un procedimento di calcolo costituito da un insieme di
istruzioni. Tali istruzioni fanno uso di un insieme finito di operazioni elementari, le
quali si possono assumere come note e primitive (nell’esempio precedente sono state
assunte come note le operazioni di elevamento al quadrato, addizione ed estrazione di
radice quadrata). Le istruzioni devono essere tali che, per poterle applicare, basti saper
Algoritmi, Macchine, Grammatiche
2
eseguire le operazioni elementari. Inoltre, affinché un procedimento sia un algoritmo,
deve godere delle seguenti proprietà:
• L’insieme delle istruzioni di cui è composto deve essere finito.
• Se la soluzione esiste, deve poter essere ottenuta mediante un numero finito di
applicazioni delle istruzioni.
• All’inizio del calcolo, e ogni qual volta sia stata eseguita un’istruzione, si deve
sempre sapere in maniera precisa quale istruzione va eseguita al passo successivo, e
quindi non devono esserci due istruzioni diverse che possono essere applicate nello
stesso momento. In altri termini, in ogni fase del calcolo non deve mai accadere che,
per sapere quale istruzione si deve eseguire, ci si debba basare sull’intuizione, o si
debba tirare a indovinare. Un procedimento che goda di questa proprietà è detto
deterministico.
• Infine, deve essere sempre chiaro se si è giunti o meno al termine del procedimento,
e se sono stati ottenuti i risultati desiderati.
Quindi gli algoritmi (che sono detti anche metodi effettivi) sono procedimenti
deterministici che consentono di risolvere determinati problemi senza far ricorso ad
alcuna forma di creatività o di inventiva. Per eseguire un algoritmo è sufficiente
applicare le istruzioni passo dopo passo, badando solo a non commettere sviste.
Dal fatto che un algoritmo è un processo deterministico consegue che, una volta
fissati i dati, il risultato ottenuto è sempre lo stesso. Non può succedere che, eseguendo
più volte lo stesso algoritmo con lo stesso input, vengano prodotti output diversi.
Si noti che, pur essendo un algoritmo caratterizzato da un insieme finito di istruzioni,
le possibili istanze del problema che esso risolve sono, di norma, infinite. Ad esempio,
il precedente algoritmo calcola la lunghezza dell’ipotenusa per ogni coppia A, B di
lunghezze dei cateti, ed esiste un numero infinito di tali coppie.
Esempi di algoritmi possono essere tratti dalle matematiche elementari. Sono
algoritmi, ad esempio, i procedimenti che consentono di eseguire le quattro operazioni
aritmetiche. In questo caso l’input è costituito dai due numeri su cui operare e l’output
dal risultato dell’operazione. Come pure è un algoritmo il procedimento euclideo per la
ricerca del massimo comun divisore di due numeri naturali non nulli. In logica, il
metodo delle tavole di verità è un algoritmo che permette di stabilire se una formula del
linguaggio proposizionale è o meno una tautologia. In questo caso l’input è costituito da
una formula proposizionale F, l’output è una risposta del tipo sì/no: sì se F è una
tautologia, no in caso contrario.
Vi sono algoritmi che operano su dati di tipo numerico e algoritmi che elaborano
altri tipi di dati. Vediamo un esempio di questo secondo tipo. Si dice che una parola è
palìndroma (oppure che essa è un palìndromo) se può essere letta indifferentemente da
sinistra a destra o viceversa. Ad esempio, ossesso è un palindromo. Il problema che
vogliamo risolvere consiste nello stabilire se, data una certa parola, essa è palindroma o
meno. L’input del problema è costituito dalla parola di cui vogliamo sapere se è
palindroma. L’output è una risposta di tipo sì o no: sì se la parola presa in input è
palindroma, no se non lo è.
Un possibile algoritmo per questo compito si comporta nel modo seguente. Si inizia
confrontando la prima e l’ultima lettera della parola:
ossesso
Algoritmi, Macchine, Grammatiche
3
Se la prima lettera è diversa dall’ultima, il procedimento termina e la risposta è
negativa: non si tratta di un palindromo. Altrimenti si cancellano la prima e l’ultima
lettera, e si ripete il procedimento dall’inizio con le lettere rimaste:
ssess
Se, dopo un certo numero di iterazioni del procedimento, non è rimasta alcuna
lettera, allora il procedimento termina e la risposta è positiva: la parola di partenza era
effettivamente un palindromo (se la parola presa in input è palindroma, ed è composta
da un numero dispari di lettere – come è appunto il caso di ossesso – allora nell’ultima
iterazione la prima e l’ultima lettera coincidono e vengono cancellate).
Un altro esempio di algoritmo che elabora dati di tipo non necessariamente numerico
riguarda la ricerca di un dato elemento in un elenco ordinato. Si supponga di voler
controllare se un certo nome N figura o meno in un elenco di nomi L ordinato
alfabeticamente. In questo caso l’input del problema è costituito dal nome N e
dall’elenco L, mentre l’output consiste anche qui in una risposta del tipo sì o no: sì se il
nome cercato è presente in L, no in caso contrario. Un algoritmo ovvio per risolvere
questo problema consiste nello scorrere tutto l’elenco partendo dall’inizio fino a che
non si trova N, oppure si arriva alla fine dell’elenco senza averlo trovato. Ovviamente
questo metodo, che viene detto ricerca di tipo sequenziale, funziona, ma è
estremamente inefficiente: se, ad esempio, N compare all’ultimo posto nell’elenco, per
trovarlo è necessario controllare tutti i nomi presenti.
Dato però che, per ipotesi, l’elenco L è ordinato, si può progettare un algoritmo più
efficiente basandosi su una tecnica che gli informatici chiamano ricerca binaria. Il
principio è il seguente. Si confronta il nome N con il nome che si trova a metà
dell’elenco. (Ovviamente, se l’elenco è composto da un numero pari di nomi, bisogna
precisare cosa si deve intendere per nome che si trova a metà dell’elenco. Assumiamo
che, se L ha n elementi, l’elemento a metà di L sia quello in posizione quoz(n, 2) + 1,
dove quoz è il quoziente della divisione intera tra numeri naturali.) Così, se L ha 10
elementi, il nome a metà di L è quello nella sesta posizione. A questo punto si possono
dare tre possibilità:
(a) il nome a metà dell’elenco coincide con N
(b) il nome a metà dell’elenco segue N in ordine alfabetico
(c) il nome a metà dell’elenco precede N in ordine alfabetico
Se si è verificato il caso (a) la ricerca ha avuto successo, e il procedimento può
terminare.
Se invece si è verificato il caso (b), allora se N figura nell’elenco, deve trovarsi nella
sua metà iniziale. Ripetiamo quindi il procedimento prendendo ora in considerazione la
prima metà dell’elenco. Vale a dire, da questo punto in poi chiamiamo L la metà iniziale
dell’elenco, e procediamo come prima: confrontiamo il nome cercato con il nome che si
trova a metà di L, a quel punto si danno di nuovo tre possibilità, e così via.
Analogamente, se si è verificato il caso (c), il procedimento va eseguito sulla
seconda metà dell’elenco.
È evidente che, se il nome N è nell’elenco, ripetendo questo procedimento un
numero finito di volte, esso prima o poi verrà trovato. Altrimenti, se N non è
nell’elenco, il procedimento avrà comunque termine: a un certo punto, dopo avere
Algoritmi, Macchine, Grammatiche
4
ripetuto il procedimento un numero finito di volte, il pezzo di elenco L che dovremmo
prendere in considerazione sarà vuoto, e il calcolo avrà termine.
È anche evidente che questo secondo algoritmo è molto più “intelligente” di quello
basato sulla ricerca sequenziale. Esso infatti, in media, per ottenere il risultato richiede
un numero molto minore di passi di calcolo1.
Gli algoritmi di questi due esempi forniscono entrambi una risposta di tipo sì o no
(gli algoritmi con questa caratteristica vengono detti algoritmi di decisione). Non è
necessario però che le cose stiano in questo modo. È facile ad esempio immaginare una
variante del secondo problema, che consiste nel cercare il numero di telefono di una
data persona nella guida telefonica. In questo caso l’input è costituito dalla guida (cioè,
da un elenco di nomi ordinati alfabeticamente, a ciascuno dei quali è associato il
rispettivo numero telefonico) e dal nome della persona di cui si vuole il numero;
l’output è dato dal numero di telefono corrispondente (o da una risposta di fallimento se
il nome non è sulla guida).
Sin dall’antichità sono stati sviluppati algoritmi per risolvere svariati tipi di
problemi. Tuttavia, soltanto nel corso del ventesimo secolo la nozione stessa di
algoritmo è diventata uno specifico oggetto di ricerca. Ciò è avvenuto con la nascita di
una nuova disciplina, detta teoria della computabilità, o teoria della computabilità
effettiva, o anche teoria della ricorsività. La nozione di algoritmo presentata sopra ha
un carattere intuitivo, non è basata su una definizione rigorosa di tipo matematico. La
teoria della computabilità è stata sviluppata a partire dagli anni intorno al 1930, ed è
stata motivata dell’esigenza di fornire un equivalente rigoroso del concetto intuitivo di
algoritmo, al fine di indagare le possibilità ed i limiti dei metodi effettivi.
Per le caratteristiche di determinismo e finitezza che abbiamo enunciato, ogni
algoritmo si presta, almeno in linea di principio, ad essere automatizzato, ad essere cioè
eseguito da una macchina opportunamente progettata. Con lo sviluppo dei calcolatori
digitali la teoria della computabilità effettiva ha dunque assunto lo statuto di teoria dei
fondamenti per l’informatica, e svolge un ruolo importante nelle riflessioni teoriche
relative a tutte le discipline che a qualche titolo sono collegate all’informatica.
Nella parte restante di questa sezione continueremo ad occuparci della nozione
intuitiva di algoritmo, ed introdurremo alcune definizioni che ci saranno utili nel
seguito.
1.2 Diagrammi di flusso
Non appena si abbia a che fare con algoritmi un po’ più complicati di quelli visti nel
paragrafo precedente, una descrizione a parole come quelle impiegate fino ad ora
diventa scomoda e di difficile comprensione. Un modo più perspicuo e sintetico per
rappresentare algoritmi è costituito dai cosiddetti diagrammi di flusso (in inglese flow
chart), talvolta chiamati anche diagrammi a blocchi. I diagrammi di flusso utilizzano
una notazione di tipo grafico. La fig. 1-2 mostra un possibile diagramma di flusso per
l’algoritmo descritto nel paragrafo precedente, che calcola la lunghezza dell’ipotenusa
di un triangolo rettangolo a partire da quelle dei due cateti.
1
Per la precisione, con il primo algoritmo, se L ha n elementi, nel caso peggiore saranno necessarie n
operazioni di confronto. Con il secondo algoritmo, nel caso peggiore il numero delle operazioni di
confronto necessarie risulterà dell’ordine di log2 n.
Algoritmi, Macchine, Grammatiche
5
INIZIO
Prendi in input i
valori di A e di B
Calcola il valore di A 2 + B2
e metti il risultato in C
Calcola il valore di C
e metti il risultato in D
Produci in output
il valore di D
FINE
Figura
Figura1-2
<xxx>
La lettura di un diagramma come questo è intuitiva. In generale, i diagrammi di
flusso sono dei grafi orientati i cui nodi rappresentano le istruzioni da eseguire. La
forma di ciascun nodo indica il tipo di istruzione corrispondente. Gli archi che li
collegano rappresentano l’ordine in cui tali istruzioni devono essere effettuate. Essi
rappresentano appunto il flusso delle informazioni durante l’esecuzione dell’algoritmo.
Vediamo nei particolari i tipi di nodi che compongono il diagramma. In ogni
diagramma di flusso vi è un nodo inizio e un nodo fine, raffigurati entrambi mediante
delle ellissi (fig. 1-3). Essi indicano rispettivamente da dove si deve partire per iniziare
il calcolo e quando si è giunti al termine dell’esecuzione dell'algoritmo2.
INIZIO
FINE
Figura 1-3
I nodi la cui forma è raffigurata in fig. 1-4 a) e b) rappresentano rispettivamente
operazioni di input e di output.
2
In un diagramma di flusso può esserci più di un nodo fine (anche se, in questi casi, è sempre possibile
scrivere un altro diagramma di flusso equivalente in cui il nodo fine compare una sola volta). Non può
comparire invece più di un nodo inizio, poiché altrimenti non sarebbe più stabilito univocamente da dove
si deve iniziare il calcolo, e si perderebbe così la caratteristica del determinismo.
Algoritmi, Macchine, Grammatiche
6
a)
b)
Figura 1-4
I valori presi in input e i risultati delle elaborazioni compiute durante il calcolo
vengono immagazzinati in variabili. Ad esempio, l’algoritmo della fig. 1-2 utilizza le
variabili A, B, C e D. Le variabili impiegate nei diagrammi di flusso vanno intese come
locazioni di memoria, come celle in cui sono depositati dei dati, e il cui contenuto può
essere modificato nel corso del calcolo. Le istruzioni che portano a modificare il valore
di una variabile vengono rappresentate mediante nodi di forma rettangolare, come
quello della fig. 1-5.
Figura 1-5
Un ulteriore tipo di nodi impiegato nei diagrammi di flusso, che non compare
nell’algoritmo della fig. 1-2, è costituito dai test. Graficamente i test sono rappresentati
per mezzo di rombi, come nella fig. 1-6. All’interno del rombo è scritta un’espressione
che viene detta la condizione del test. Affinché un’espressione possa fungere da
condizione deve poter assumere un valore di verità, vero o falso. Se la condizione di un
test è vera, allora nell’esecuzione dell’algoritmo si segue la freccia contrassegnata con
‘SI’. Se la condizione è falsa, allora si segue la freccia contrassegnata con ‘NO’.
SI
NO
condizione
Figura 1-6
Come esempio di impiego del test, presentiamo un semplice algoritmo che, preso in
input un numero, produce in output il suo valore assoluto (fig. 1-7). L’algoritmo
memorizza nella variabile A il numero preso in input; dopo di che controlla se il valore
di A è maggiore o uguale a 0. In caso affermativo assegna il valore di A alla variabile B.
Altrimenti assegna a B il valore di A cambiato di segno. Dopo di che produce in output
il valore di B, e il calcolo termina.
Algoritmi, Macchine, Grammatiche
7
INIZIO
Prendi in input il
valore di A
SI
NO
A≥ 0?
Assegna a B
il valore di A
Assegna a B
il valore di − A
Produci in output il
valore di B
FINE
Figura 1-7
Questi sono gli elementi di base che impiegheremo per la costruzione dei diagrammi
di flusso. Per ora non ci interessa stabilire con precisione quali operazioni si possano
utilizzare all’interno dei blocchi. Basta che si tratti di operazioni che, intuitivamente,
siano effettuabili in modo algoritmico (come è il caso delle usuali operazioni
aritmetiche). In tal caso è evidente che l’intero procedimento descritto da un diagramma
di flusso è a sua volta un algoritmo.
Nei diagrammi di flusso i test possono essere impiegati per la definizione di cicli,
mediante i quali una stessa istruzione, o un gruppo di istruzioni, possono essere ripetuti
più volte. La fig. 1-8 mostra due tipici esempi di strutture cicliche. Nel ciclo di fig. 1-8
a) per prima cosa viene controllato il valore della condizione; se essa è vera, allora
vengono eseguite le istruzioni istruzione1,…, istruzionen. Dopo di che, si torna a
controllare la condizione, e fino a che essa resta vera le istruzioni vengono ripetute. Il
ciclo termina la prima volta che la condizione diventa falsa.
Nel ciclo di fig. 1-8 b) per prima cosa vengono eseguite le istruzioni istruzione1,…,
istruzionen; dopo di che viene controllata la condizione del test; se essa risulta falsa,
allora istruzione1,…, istruzionen vengono eseguite di nuovo, e ciò si ripete fino a
quando la condizione diventa vera. Il ciclo termina la prima volta che la condizione
diventa vera.
a)
b)
istruzione 1
NO
condizione
SI
istruzione 1
istruzione n
condizione
istruzione n
NO
SI
Algoritmi, Macchine, Grammatiche
8
Figura 1-8
Vediamo ora un esempio di diagramma di flusso che rappresenta un algoritmo basato
su di un ciclo come quello della fig. 1-8 a). Assumendo come nota l’operazione di
addizione, l’algoritmo della fig. 1-9 prende in input due numeri naturali e ne calcola il
prodotto.
INIZIO
Prendi in
input i valori
di M e di N
Poni 1 il
valore di C
Poni 0 il
valore di P
NO
C ≤ N?
Produci in output
il valore di P
FINE
SI
Somma al
valore di P il
valore di M
Incrementa di
1 il valore di C
Figura 1-9
Questo algoritmo prende in input i due fattori da moltiplicare, e li memorizza nelle
variabili M e N. Dopo di che calcola il prodotto di M per N sommando M a se stesso
per N volte. Ciò si ottiene mediante un ciclo e, per fare sì che esso venga ripetuto N
volte, viene impiegata un’altra variabile, che abbiamo chiamato C. Prima del ciclo il
valore di C è posto uguale a 1. A ogni iterazione del ciclo C viene incrementata di 1;
quando il valore di C supera quello di N il ciclo viene fatto terminare, ed è prodotto il
valore in output. In informatica una variabile usata come C viene detta contatore. Per
calcolare il risultato si è usata la variabile P. All’inizio il valore di P viene posto uguale
a 0. Ad ogni iterazione, al valore di P è sommato il valore di M, di modo che alla fine in
P si ottiene il valore di M sommato a se stesso N volte.
L’algoritmo che verifica se una parola è palindroma, illustrato nel paragrafo
precedente, può essere espresso mediante un diagramma di flusso basato su un ciclo
(fig. 1-10).
Algoritmi, Macchine, Grammatiche
9
INIZIO
Prendi in input il
valore di
PAROLA
PAROLA
ha almeno un carattere
e
primo carattere di PAROLA
=
ultimo carattere di PAROLA?
NO
SI
Cancella da PAROLA il
primo e l’ultimo carattere
SI
NO
E’ rimasto qualche carattere
in PAROLA?
Produci in output la
risposta “si tratta di
un palindromo”
Produci in output la
risposta “non si tratta di
un palindromo”
FINE
Figura 1-10
Per specificare in tutti i particolari gli algoritmi come questo che operano su dati non
numerici (al fine, ad esempio, di implementarli mediante un linguaggio di
programmazione), bisognerebbe precisare come devono essere rappresentati i dati da
elaborare (cosa che invece può essere data per scontata nel caso di algoritmi che
elaborano dati numerici). In questo caso, ad esempio, andrebbe specificato come va
rappresentata la parola di cui si vuole controllare se è palindroma; nel caso
dell’algoritmo per la ricerca di un nome in un elenco (si veda il paragrafo precedente)
andrebbe specificato come vanno rappresentati i nomi e l’elenco ordinato. Si dovrebbe
cioè (secondo la terminologia informatica) precisare su quali strutture dati l’algoritmo
opera. Questi aspetti, che sono centrali dal punto di vista informatico, non sono rilevanti
per i nostri scopi, per cui nel seguito li tralasceremo.
Algoritmi, Macchine, Grammatiche
10
1.3 Algoritmi che non sempre producono risultati
Ci sono algoritmi che, per alcuni dei possibili input, non producono in output alcun
risultato. Un semplice esempio è costituito dall’algoritmo di fig. 1-11, il quale esegue la
sottrazione tra due numeri naturali. Essa è definita soltanto se il minuendo è maggiore o
uguale al sottraendo. Pertanto l’algoritmo di fig. 1-11 si comporta come segue: prende
in input due numeri naturali A e B; dopo di che, se A è maggiore o uguale a B, produce
come output la differenza tra A e B; altrimenti termina senza produrre risultato.
INIZIO
Prendi in input
i valori di A e di B
NO
SI
A ≥ B?
Calcola il valore di A – B
e ponilo in C
Produci in output
il valore di C
FINE
Figura 1-11
L’algoritmo di fig. 1-11 in alcuni casi non produce risultati. Tuttavia, per ogni coppia
di numeri presi in input, dà sempre origine a un calcolo che termina. Vi sono algoritmi
che, per alcuni input, non producono alcun risultato in quanto danno origine a un
calcolo che non termina. Si considerino ad esempio i diagrammi della fig. 1-12.
a)
b)
INIZIO
INIZIO
Prendi in input
il valore di I
Prendi in input
il valore di I
I ≠ 100?
NO
Aumenta di 1
il valore di I
SI
NO
Aumenta di 1
il valore di I
I = 100?
SI
Produci in output
il valore di I
Produci in output
il valore di I
FINE
FINE
Figura 1-12
Nel caso dell’algoritmo di fig. 1-12 a), supponiamo che venga dato in input alla
variabile I un qualsiasi numero maggiore di 100. La condizione risulterà vera, quindi si
inizierà ad eseguire il ciclo. Verrà incrementato di 1 il valore di I; si tornerà quindi a
verificare la condizione, che risulterà ancora vera. Poiché ad ogni iterazione il valore di
Algoritmi, Macchine, Grammatiche
11
I è destinato a crescere, la condizione del ciclo non diventerà mai falsa, e il ciclo in
linea di principio è destinato a continuare all’infinito. Considerazioni analoghe valgono
nel caso dell’algoritmo di fig. 1-12 b): se il valore preso in input è maggiore di 100, la
condizione resterà sempre falsa, e il ciclo non terminerà mai.
Quindi, per alcuni valori in input, cicli come questi possono dare luogo a un calcolo
che non termina. In gergo informatico, si dice che in questi casi un algoritmo va in loop
(in inglese “loop” significa “cappio”, “occhiello”).
Un altro esempio di algoritmo che in alcuni casi va in loop è dato dal diagramma di
flusso di fig. 1-13. Esso prende in input un numero naturale x e, se x è un quadrato
perfetto, ne calcola la radice quadrata3. Dopo aver preso in input il valore di x,
l’algoritmo pone a zero il valore di una variabile y, e controlla se x è uguale a y2. Nel
caso il risultato di questo test sia positivo, il calcolo è terminato: y è la radice quadrata
di x, e il suo valore viene prodotto in output. Altrimenti il valore di y viene incrementato
di uno, e si torna a controllare se x è uguale a y2. È facile constatare che, se x è un
quadrato perfetto, allora prima o poi il calcolo termina, e viene prodotta in output la
radice quadrata di x. Altrimenti, se x non è un quadrato perfetto, il test y2 = x sarà
sempre falso, e il calcolo andrà avanti all’infinito senza produrre alcun risultato.
INIZIO
Assumi in
input x
Poni y = 0
FINE
Produci in
output il
valore di y
SI
y2 = x ?
NO
Incrementa
y di 1
Figura 1-13
1.4 Numeri naturali e codifiche dei dati
Spesso quando si studia la nozione di algoritmo ci si concentra su algoritmi che
elaborano numeri naturali. Ciò potrebbe sembrare riduttivo: abbiamo visto che esistono
algoritmi definiti su enti assai diversi dai numeri naturali. Vi sono algoritmi che
stabiliscono se un certo oggetto appartiene o meno a un dato insieme, o se gode o non
gode di una certa proprietà. Vi sono algoritmi che eseguono manipolazioni di vario
genere sulle espressioni di un linguaggio o di un sistema formale. In informatica poi
vengono impiegati algoritmi per elaborare dati della natura più diversa, dai testi alle
immagini, dai suoni ai filmati.
Tuttavia il fatto di concentrarsi su algoritmi che elaborano numeri naturali non
comporta una perdita di generalità. Infatti, esistono varie tecniche mediante le quali dati
3
Il calcolo viene effettuato in modo molto inefficiente, ma ciò, in questa sede, non è rilevante.
Algoritmi, Macchine, Grammatiche
12
di tipo diverso possono essere codificati mediante numeri naturali, per cui algoritmi
come quelli sopra citati vengono ricondotti ad algoritmi che elaborano dati di tipo
numerico. Vediamo alcuni esempi ispirati all'informatica.
È noto che nella memoria di un calcolatore tutti i dati sono rappresentati sotto forma
di sequenze di bit, ossia di cifre 0 e 1. Tali sequenze possono essere interpretate come la
rappresentazione di numeri naturali espressi in notazione binaria (bit è infatti la
contrazione di binary digit, ossia, appunto, cifra binaria). In questo modo tutte le
manipolazioni che un calcolatore esegue sui dati possono essere lette in termini
aritmetici, come calcoli di tipo algoritmico che operano su (insiemi di) numeri naturali.
Esistono molteplici tecniche per codificare i dati sotto forma di sequenze di bit.
Vediamo sinteticamente un paio di esempi tra i più semplici.
Consideriamo un tipo di dati molto diversi da quelli visti sino ad ora, ossia le
immagini. In informatica sono state sviluppate svariate tecniche per codificare immagini
in modo che possano essere elaborate con un calcolatore. Vediamo a grandi linee come
funziona il metodo più semplice per ottenere la codifica binaria di un’immagine. Si
supponga di avere a che fare con un disegno in bianco e nero. Si immagini di
sovrapporre al disegno una griglia abbastanza fitta da riuscire a rappresentare la figura
con il grado di dettaglio desiderato. A questo punto si assegni ad ogni cella della griglia
il valore nero se la porzione corrispondente del disegno è prevalentemente nera, il
valore bianco se la porzione corrispondente del disegno è prevalentemente bianca. Si
otterrà così un’immagine come quella di fig. 1-14.
Figura 1-14
Se ne ingrandiamo un dettaglio, ad esempio la punta di uno dei baffi di Miomao,
otterremo un particolare come quello di fig. 1-15, in cui sono evidenti le celle bianche e
nere che compongono la griglia (che in questo caso è di dimensioni 266 × 398).
Figura 1-15
Algoritmi, Macchine, Grammatiche
13
Ovviamente, quanto più fitta è la griglia, tanto migliore sarà la qualità
dell’immagine. A titolo di esempio, riportiamo nella fig. 1-16 cinque versioni della
stessa immagine, ottenute rispettivamente con griglie di dimensioni 20 × 30, 40 × 60, 50
× 75, 80 × 120 e 120 × 180.
Figura 1-16
A questo punto è facile passare a una codifica mediante cifre binarie. Basta
rappresentare, ad esempio, ogni cella bianca con 0, e ogni cella nera con 1. In questo
modo il disegno di partenza viene codificato con una sequenza di bit. Con buona
approssimazione, questo è il tipo di procedimento che uno scanner esegue quando
acquisisce un’immagine.
In informatica un’immagine codificata mediante questa tecnica viene chiamata
bitmap (ossia, “mappa di bit”), e ognuna delle celle che la compongono è detta pixel
(pixel sta per picture element). Anche immagini più ricche, ad esempio a colori o con
vari toni di grigio, possono essere codificate sotto forma di bitmap. In questi casi ogni
cella dovrà ammettere più di due valori (in particolare, sarà necessario un valore diverso
per ogni possibile colore o tono di grigio). Sarà quindi necessaria più di una cifra
binaria per rappresentare lo stato di ciascuna cella, ma, nella sostanza, la tecnica rimarrà
immutata.
Grazie a codifiche di questo tipo le elaborazioni eseguite sulle immagini (come
aumentarne il contrasto, passare da un’immagine al suo negativo, passare da
un’immagine a colori ad una con toni di grigio, eccetera) possono essere viste come
procedure che alla codifica dell’immagine iniziale presa come input associano come
output la codifica dell’immagine elaborata. Ad esempio, sostituendo nella codifica di
fig. 1-14 ciascuno 0 con 1 e ciascun 1 con 0 si ottiene il negativo dell’immagine di
partenza (fig. 1-17).
Algoritmi, Macchine, Grammatiche
14
Figura 1-17
Tecniche analoghe si possono impiegare per rappresentare altri tipi di dati.
Consideriamo ad esempio i suoni. Supponiamo di voler rappresentare un’onda sonora,
come quella mostrata nella fig. 1-18. L’asse delle ascisse rappresenta la dimensione del
tempo. In estrema sintesi, si può impiegare una tecnica di questo tipo. Si suddivide
l’asse delle ascisse x in intervalli che possono essere scelti anche molto piccoli. Dopo di
che, per ciascuno di questi intervalli i, si calcola il valore medio delle ordinate dei punti
che hanno ascissa in i, e lo si codifica con un numero naturale. Tale operazione viene
detta campionamento. L’onda sonora di partenza è ora rappresentata sotto forma di un
insieme ordinato finito di numeri naturali. La rappresentazione sarà tanto più fedele
quanto più piccoli sono gli intervalli della suddivisione.
Figura 1-18
I numeri naturali possono dunque essere visti come un mezzo per rappresentare dati
generici di tipo discreto, e in questo modo saranno intesi nel seguito di questo testo. Si
noti tra l’altro che nell’ultimo esempio abbiamo codificato con numeri naturali i numeri
decimali che sono le ordinate della curva di fig. 1-18. Come noto i numeri reali sono
rappresentati da numeri decimali finiti o periodici (se sono razionali), o infiniti e non
Algoritmi, Macchine, Grammatiche
15
periodici (se sono irrazionali). In un calcolatore digitale in ogni caso essi vengono
approssimati con numeri decimali finiti, i quali possono essere codificati mediante
numeri naturali.
Analogico e digitale: il regolo calcolatore
In un calcolo di tipo analogico i dati vengono rappresentati per mezzo di
grandezze fisiche che variano in modo continuo (ad esempio grandezze elettriche
come la corrente o il voltaggio, oppure grandezze geometrico-meccaniche, come la
rotazione o lo spostamento reciproco di determinate componenti). I calcoli vengono
effettuati agendo fisicamente su tali grandezze. I calcoli analogici si contrappongono
ai calcoli di tipo digitale, in cui i dati vengono codificati mediante un insieme discreto
di simboli, e le computazioni consistono di manipolazioni definite su tali codifiche
simboliche. Le codifiche descritte nel testo sono tutte di tipo digitale.
La distinzione analogico/digitale riguarda il modo in cui vengono rappresentati ed
elaborati i dati, e non è una distinzione tra tipi diversi di hardware. Così vi sono
calcolatori elettronici analogici (in cui ad esempio i dati sono rappresentati in termini
di voltaggio), come pure calcolatori meccanici sia analogici, sia digitali.
In questi appunti ci occuperemo esclusivamente di calcoli digitali. Tuttavia, per
meglio chiarire la distinzione, esaminiamo qui un semplice dispositivo di calcolo
analogico. Si tratta del regolo calcolatore, inventato nel XVII secolo dal matematico
inglese Edmund Gunter. Probabilmente si tratta del calcolatore analogico che
storicamente ha avuto la maggiore diffusione. Vediamo in sintesi come funziona.
Affianchiamo due righelli, in maniera che siano liberi di scorrere l’uno rispetto
all’altro verso destra e verso sinistra. Segniamo su ciascuno di essi delle tacche a
distanze uguali, e associamo a ogni tacca una potenza di 2:
20
21
22
23
24
25
26
27
28
|
|
|
|
|
|
|
|
|
|
20
|
21
|
22
|
23
|
24
|
25
|
26
|
27
|
28
In maniera equivalente, possiamo indicare sui righelli i valori corrispondenti:
1
2
4
8
16
32
|
|
|
|
|
|
|
1
|
2
|
4
|
8
|
16
|
32
64 128 256
|
|
|
|
|
|
64 128 256
A questo punto i righelli possono essere usati per moltiplicare tra loro le potenze
di 2. Supponiamo di voler moltiplicare 8 per 16. Si fa scorrere verso destra il righello
inferiore in modo che la tacca del numero 1 si venga a trovare in corrispondenza della
tacca del numero 8 sul righello superiore:
Algoritmi, Macchine, Grammatiche
16
fattore
1
|
2
|
4
|
8
|
|
1
16
|
|
2
32
|
|
4
prodotto
64 128 256
|
|
|
|
|
|
|
|
|
8 16 32 64 128 256
fattore
Ora, per ottenere il prodotto di 8 e di 16, basta andare a leggere sul righello
superiore il numero che corrisponde a 16 sul righello in basso; si ottiene così che
8 ⋅ 16 = 128. Infatti, la distanza d = 3 tra la tacca 1 e la tacca 8 sommata alla distanza
d’ = 4 tra la tacca 1 e la tacca 16 è uguale alla distanza d” = 7 tra la tacca 1 e la tacca
128. Questo accade perché, per come abbiamo segnato le tacche, d è tale che 2d = 8,
ossia d è il logaritmo in base 2 di 8 (in simboli, d = log2 8), d’ è il logaritmo in base 2
di 16, e, di conseguenza, d” = d + d’ = 7 = log2128 in quanto 128 = 8 ⋅ 16 = 23 ⋅ 24 =
23+4.
In generale, il logaritmo in base 2 di un numero n (log2 n) è quel numero d tale
che 2d = n. Dati due numeri n e n’, se d = log2 n e d’ = log2 n’, allora d + d’ = log2(n ⋅
n’). Infatti n ⋅ n’ = 2d ⋅ 2d’ = 2d+d’. Su questo si basa il funzionamento del regolo.
Si può infatti generalizzare il procedimento dei righelli scorrevoli in modo da
moltiplicare numeri qualsiasi. A tal fine bisogna completare i righelli aggiungendo le
tacche dei numeri che non sono potenze di due. Ad esempio si dovrà aggiungere la
tacca del 3 tra il 2 e il 4, le tacche del 5, del 6 e del 7 tra il 4 e l’8, e così via. Tutto
questo facendo in modo che la distanza della tacca di ciascun numero n da 1 sia
uguale al logaritmo in base 2 di n. Il logaritmo di un numero che non sia una potenza
di due è un numero reale non razionale. Ad esempio, log2 3 = 1,58496250072…, per
cui la tacca del 3 dovrà essere segnata a una distanza d = 1,58496250072… dalla
tacca dell’1. O ancora, log2 7 = 2,80735492205…, per cui la tacca del 7 dovrà essere
segnata a una distanza d’ = 2,80735492205… dalla tacca dell’1. E così via. In questo
modo, sommando le distanze d e d’, si ottiene d” = 4,39231742277…, che è il
logaritmo in base 2 di 21 (infatti 21 è il prodotto di 3 e di 7). La scala delle tacche sui
righelli del regolo avrà l’aspetto seguente (in questo caso tra le tacche 1 e 2 e tra le
tacche 2 e 3 sono indicati anche alcuni valori decimali):
10
Riportiamo qui di seguito il particolare di un regolo reale:
Algoritmi, Macchine, Grammatiche
17
Il regolo è un calcolatore analogico in cui la grandezza fisica che viene
utilizzata per il calcolo è lo spostamento reciproco dei due righelli. Essa viene usata
per rappresentare i numeri da moltiplicare. Si tratta di una grandezza continua poiché
il procedimento che abbiamo descritto non funziona soltanto per le tacche che sono
segnate esplicitamente sui righelli (che saranno comunque un insieme discreto).
Facendo scorrere in modo continuo i righelli l’uno rispetto all’altro, ogni posizione è
“significativa”, nel senso che, scelte tre distanze qualunque d, d’ e d”, se d” = d + d’,
allora 2d ⋅ 2d’ = 2d”, a prescindere dal fatto che nei punti corrispondenti sia segnata sui
righelli una tacca o meno. In altri termini, in linea di principio le tacche sui righelli
potrebbero essere fitte quanto si vuole. Ovviamente dal punto di vista pratico tale
possibilità di principio è limitata dalla precisione con cui è possibile effettuare le
misurazioni. Questo è un problema generale di tutti i calcolatori di tipo analogico, che
non trova corrispettivo nel calcolo digitale. Le limitazioni nella precisione delle
misurazioni comportano che, in generale, i calcoli di tipo digitale consentano di
ottenere una precisione maggiore di quelli analogici.
1.5 Funzioni e algoritmi
1.5.1 Il concetto di funzione
Come vedremo nel prossimo sottoparagrafo, il concetto di algoritmo si collega in
modo naturale al concetto matematico di funzione. Introduciamo dunque gli aspetti
essenziali della nozione di funzione. In generale, una funzione ϕ è una corrispondenza
tra due insiemi (li indichiamo con D e C) che, ad ogni elemento di D preso come
argomento, associa come valore uno ed un solo elemento di C. In altri termini, per ogni
argomento x ∈ D, esiste uno ed un solo valore y ∈ C che ϕ fa corrispondere a x, e si
scrive ϕ(x) = y (oppure y = ϕ(x))4.
L’insieme D in cui una funzione ϕ assume i suoi argomenti si dice il dominio di ϕ;
l’insieme C in cui una funzione ϕ assume i suoi valori si dice il codominio di ϕ. Per
indicare che una funzione ϕ ha dominio D e codominio C, si scrive ϕ: D → C (e si dice
che ϕ è di tipo D → C). La situazione è schematizzata in fig. 1-19.
ϕ :D→ C
C
D
ϕ
DOMINIO
CODOMINIO
∈ è il simbolo di appartenenza insiemistica: la scrittura a ∈ I significa che l'elemento a appartiene
all'insieme I.
4
Algoritmi, Macchine, Grammatiche
18
Figura 1-19
Il concetto di funzione è molto generale in quanto non solo dominio e codominio
possono essere due insiemi qualunque, ma anche la corrispondenza può essere di natura
qualsiasi. Ad esempio, dato un insieme D di persone, si ottiene una funzione di dominio
D facendo corrispondere ad ogni persona in D la sua altezza; in tal caso il codominio C
è costituito da numeri decimali finiti che esprimono le misure delle altezze rispetto ad
un’unità di misura (solitamente il metro). Si ottengono altre funzioni considerando
come varia la temperatura corporea di una persona al trascorrere del tempo, o la
quantità di un bene prodotta al variare dell’anno di produzione, eccetera.
Se D è l’insieme delle regioni italiane e C l’insieme delle città italiane, si ottiene una
funzione ϕ di dominio D e codominio C se si fa corrispondere a ciascuna regione il suo
capoluogo (ϕ(Piemonte) = Torino, ϕ(Liguria) = Genova, e così via); non si ottiene una
funzione di dominio D se come corrispondenza si assume quella determinata da “essere
capoluogo di provincia”, poiché, in tal caso, non si associa a ciascuna regione una ed
una sola città (le province del Piemonte sono otto, quelle della Liguria quattro, e così
via). Così, se D è un insieme di persone e si associa a ciascuna di esse sua madre, la
corrispondenza individua una funzione (in quanto ciascun individuo ha una ed una sola
madre)5, mentre, se D è un insieme di donne e si associano a ciascuna di esse i suoi figli,
in generale non si ha una funzione (in quanto una donna può non avere figli o averne
più di uno).
In matematica si è interessati per lo più a funzioni in cui dominio e codominio sono
insiemi di numeri. In tal caso la funzione ϕ viene spesso individuata fornendo
un’espressione algebrica ϕ(x) contenente la lettera x: se a x si sostituisce un numero a
del dominio e si eseguono i calcoli indicati nell’espressione ϕ(x), si determina il valore
ϕ(a) della funzione che corrisponde all’argomento a.
Ad esempio, se assumiamo
come dominio e codominio l’insieme R dei numeri reali, allora con l’espressione:
ϕ(x) = x2 + 4
(*)
si definisce una funzione ϕ di tipo R → R. Infatti, per ogni elemento di R che si
sostituisce a x in (*), ϕ(x) assume come valore uno ed un solo elemento di R. Ad
esempio: ϕ(3) = 32 + 4 = 13, ϕ(1,75) = 1,752 + 4 = 7,0625, ϕ( ) = e così via. Se
assumiamo che a x si sostituiscano numeri naturali (ossia che il dominio sia N), allora
(*) definisce una funzione di tipo N → N in quanto anche i valori sono in N.
Altri semplici esempi di funzioni definite in questo modo sono:
ϕ1(x) = 2x
ϕ2(x) = 4
ϕ3(x) =
5
Naturalmente, occorre che al codominio C appartengano tutte le madri degli individui di D.
Algoritmi, Macchine, Grammatiche
19
La funzione ϕ1 raddoppia il numero preso come argomento. La funzione ϕ2 è una
funzione costante: essa associa a ciascun elemento del dominio sempre lo stesso valore
nel codominio (in questo caso il numero 4). Il valore della funzione ϕ3 è 0 per tutti gli
argomenti strettamente maggiori di 2, ed è 1 per tutti gli argomenti minori o uguali a 2.
In realtà, come si è detto, ciascuna di esse individua funzioni diverse al variare del
dominio; ad esempio, può essere vista sia come una funzione di tipo N → N, sia come
di tipo R → R.
Finora abbiamo visto esempi di funzioni ad un solo argomento. Si possono però
considerare funzioni a più argomenti. In particolare, potremo avere funzioni
ϕ(x1, x2,…, xn) con un numero n qualsiasi di argomenti. Ad esempio, l’addizione
(ϕ(x , x ) = x + x ) e la moltiplicazione (ϕ(x , x ) = x .x ) di due numeri sono funzioni a
1
2
1
2
1
2
1 2
due argomenti. L’addizione aritmetica (cioè l’addizione definita sui numeri naturali) è
una funzione che prende come argomenti coppie di numeri naturali e produce come
valore un numero naturale: data come argomento la coppia (4, 3) l’addizione produce
come somma 7, data come argomento la coppia (123, 256), l’addizione produce come
somma 379, e così via.
In generale, sia ϕ(x1, x2) un’espressione con due argomenti che prende il suo primo
argomento x1 nell’insieme D1, il suo secondo argomento x2 nell’insieme D2, e il cui
valore appartiene all’insieme C. Allora ϕ(x1, x2) definisce una funzione ϕ di codominio
C, e il cui dominio è costituito dall’insieme delle coppie ordinate (x1, x2) tali che x1 ∈
D1 e x2 ∈ D2. Tale insieme viene detto il prodotto cartesiano di D1 e D2, e viene
indicato con D1 × D2. In tal caso ϕ è una funzione di tipo D1 × D2 → C.
Nel caso di una funzione aritmetica a due argomenti come l’addizione aritmetica, il
dominio è costituito dall’insieme di tutte le coppie ordinate (x1, x2) tali che x1, x2 ∈ N,
vale a dire dal prodotto cartesiano di N per se stesso N × N, che si indica anche con N2.
L’addizione aritmetica è quindi una funzione di tipo N2 → N.
Generalizzando, un’espressione ϕ(x1, x2,…, xn) con n argomenti tale che, se x1 ∈ D1,
x2 ∈ D2,…, xn ∈ Dn allora ϕ(x1, x2,…, xn) ∈ C, individua una funzione ϕ di tipo
D1 × D2 × … × Dn → C, che ha come dominio l’insieme delle n-ple ordinate
D1 × D2 × … × Dn, vale a dire il prodotto cartesiano dei vari Di (con 1 ≤ i ≤ n), e come
codominio C.
I connettivi vero-funzionali della logica proposizionale (congiunzione,
disgiunzione, negazione, e così via) possono essere visti come funzioni di tipo
opportuno. Consideriamo ad esempio la congiunzione ∧ (“e”). Essa prende come
argomenti due proposizioni qualsiasi A e B, ciascuna delle quali può assumere uno ed
uno solo dei due valori di verità V (vero) oppure F (falso), e produce come valore la
proposizione A ∧ B, il cui valore di verità è determinato sulla base dei valori di verità di
A e di B come riportato nella seguente tabella (A ∧ B è vera se e solo se A e B sono
entrambe vere):
Algoritmi, Macchine, Grammatiche
20
A
B
A∧B
V
V
V
V
F
F
F
V
F
F
F
F
Pertanto la congiunzione si comporta come una funzione che ha come dominio
l’insieme {V, F}2 (cioè l’insieme delle coppie ordinate (v1, v2) dove vi, con i = 1, 2,
sono valori di verità) e come codominio l’insieme {V, F}, ossia una funzione di tipo
{V, F}2 → {V, F}.
Così la negazione ¬ (“non”), che è il connettivo a un argomento caratterizzato dalla
seguente tabella:
A
¬A
V
F
F
V
può essere visto come la funzione η di tipo {V, F} → {V, F} tale che η(V) = F e
η(F) = V.
1.5.2 Funzioni calcolate da algoritmi
Ricolleghiamoci ora alla nozione di algoritmo. Il problema che viene risolto da un
algoritmo può essere visto come una funzione, in cui i dati in ingresso corrispondono
agli argomenti, e il risultato in uscita corrisponde al valore. Dato che per ciascun input
un algoritmo produce al più un solo output (e questo è garantito dall’assunzione di
determinismo alla base della definizione di algoritmo), la corrispondenza tra input e
output può essere caratterizzata in termini funzionali: l’algoritmo calcola una funzione
dall’insieme degli input a quello degli output. Questa analogia può essere visualizzata
come in fig. 1-20.
Figura 1-20
Queste considerazioni conducono alle seguenti definizioni:
Definizione 1. Dato un algoritmo A, si dice che esso calcola una funzione ϕ: D → C se
e solo se, per ogni x ∈ D, ϕ(x) = y se e soltanto se A con input x produce come output y.
Algoritmi, Macchine, Grammatiche
21
Definizione 2. Si dice che una funzione è calcolabile (computabile) in modo
algoritmico, o calcolabile in modo effettivo, o effettivamente calcolabile
(computabile), o più semplicemente calcolabile (computabile), se e solo se esiste un
algoritmo che la calcola.
Le funzioni aritmetiche che abbiamo menzionato in precedenza (addizione,
moltiplicazione), come pure tutte quelle che si incontrano comunemente nella pratica
matematica, sono calcolabili. Uno dei risultati più importanti della teoria della
computabilità, sul quale torneremo nel seguito, è che esistono funzioni non calcolabili,
ossia i cui valori non sono calcolabili mediante alcun algoritmo.
Una funzione effettivamente calcolabile non deve però essere confusa con un
algoritmo che ne calcola i valori. Nonostante l’analogia evidenziata nella fig. 1-20,
funzioni e algoritmi sono entità concettualmente distinte, e le funzioni sono
generalmente definite in maniera indipendente dagli eventuali algoritmi che ne
calcolano i valori.
Ciò segue dal fatto che, data una funzione effettivamente calcolabile, esistono più
algoritmi per calcolarne i valori. Si considerino ad esempio i due algoritmi per la ricerca
di un elemento in un elenco ordinato descritti nel par. 1.1. Essi "fanno la stessa cosa".
Ossia, usando una terminologia più rigorosa, possiamo dire che calcolano la stessa
funzione (si tratta di una funzione che ha come dominio un insieme che include gli
oggetti presenti nell'elenco e come codominio un insieme composto da due elementi,
che stanno per le risposte "sì" e "no"). O ancora, si consideri la funzione addizione. Essa
può calcolata con molti algoritmi diversi. Uno è quello che si impara alle scuole
elementari, basato sull'impiego della notazione decimale. Supponiamo però di
rappresentare i numeri utilizzando una notazione binaria. Un algoritmo che somma due
numeri in notazione binaria è necessariamente diverso da uno basato sulla notazione
decimale. Si possono sviluppare anche algoritmi che sommano numeri rappresentati
utilizzando le cifre romane. Si tratta di algoritmi diversi che calcolano tutti la stessa
funzione, l'addizione. In generale, si può mostrare che per ogni funzione calcolabile
esistono in linea di principio infiniti algoritmi che la calcolano.
Dal fatto che una stessa funzione può essere calcolata da più algoritmi diversi segue
immediatamente che una funzione calcolabile non può essere identificata con un
algoritmo che la calcola. Una funzione infatti viene definita in maniera del tutto
indipendente rispetto ai metodi per computarla.
Algoritmi, Macchine, Grammatiche
22
ESERCIZI RELATIVI ALLA PRIMA PARTE
Esercizio 1.1. Una sequenza di parentesi si dice bilanciata se, per ogni parentesi aperta,
esiste una parentesi chiusa corrispondente. Ad esempio, le due sequenze di parentesi
seguenti sono bilanciate:
(( ( ) (( )) ))
( ) ((( ) ( )) ( ))
mentre le seguenti non lo sono:
)) (( ))
(( ) ( )))
Descrivere a parole un algoritmo che, presa in input una sequenza di parentesi, controlli
se essa è bilanciata o meno.
Esercizio 1.2. Rappresentare mediante un diagramma di flusso un algoritmo per la
ricerca sequenziale di un nome in un elenco ordinato.
Esercizio 1.3. Rappresentare mediante un diagramma di flusso l’algoritmo per la ricerca
binaria di un nome in un elenco ordinato, che abbiamo presentato nel paragrafo 1.1.
Esercizio 1.4. Rappresentare mediante un diagramma di flusso l’algoritmo
dell’esercizio 1.1 che stabilisce se una sequenza di parentesi è bilanciata.
Esercizio 1.5. L’algoritmo della fig. 1-9 impiega un ciclo come quello della fig. 1-8 a).
Formulare un algoritmo che produca gli stessi risultati impiegando una struttura come
quella della fig. 1-8 b).
Esercizio 1.6. Formulare un algoritmo che, assunto come noto il prodotto, prenda in
input due numeri naturali m ed n e produca in output mn.
Esercizio 1.7. Si assuma come nota l’operazione mod, tale che X mod Y sia il resto
della divisione di X per Y. Si rappresenti quindi mediante un diagramma di flusso un
algoritmo che, preso in input un numero naturale N, produca in output 1 se N è primo, 0
se N non è primo.
Esercizio 1.8. Modificare l’algoritmo della fig. 1-11 in modo che produca sempre un
risultato: se A < B, produca come output 0.
Esercizio 1.9. Determinare due algoritmi che vadano in loop per qualsiasi input, uno
basato su un ciclo come quello della fig. 1-8 a), e l’altro basato su un ciclo come quello
della fig. 1-8 b).
Esercizio 1.10. È possibile fare in modo che l’algoritmo della fig. 1-13 dia luogo a un
calcolo che termina per qualunque input?
Esercizio 1.11. Stabilire se il seguente algoritmo:
Algoritmi, Macchine, Grammatiche
23
INIZIO
Prendi in input
il valore di N
NO
SI
N < 200?
Aumenta di 1
il valore di N
NO
N = 100?
SI
Produci in output
il valore di N
FINE
(a) termina per qualunque valore di N
(b) non termina per alcun valore di N
(c) termina se e solo se N > 200 oppure N < 100
(d) termina se e solo se N ≥ 200 oppure N ≤ 100
(e) termina se e solo se N ≥ 200 oppure N < 100
(f) termina se N > 200
(g) termina solo se N > 200
Algoritmi, Macchine, Grammatiche
24
Esercizio 1.12. Determinare per quali valori di input i seguenti algoritmi terminano.
(a)
(b)
INIZIO
INIZIO
Prendi in input
il valore di N
Prendi in input
il valore di N
NO
NO
SI
N < 100?
SI
N ≤ 100?
Aumenta di 1
il valore di N
Aumenta di 1
il valore di N
NO
NO
N = 100?
N = 100?
SI
SI
Produci in output
il valore di N
Produci in output
il valore di N
FINE
FINE
(c)
(d)
INIZIO
INIZIO
Prendi in input
il valore di N
Prendi in input
il valore di N
NO
SI
Aumenta di 1
il valore di N
NO
N ≠ 100?
N < 100?
SI
N < 100?
NO
SI
Aumenta di 1
il valore di N
Poni a 0
il valore di N
Produci in output
il valore di N
FINE
Produci in output
il valore di N
FINE
Algoritmi, Macchine, Grammatiche
25
Esercizio 1.13. Stabilire quali dei seguenti algoritmi generano un calcolo che termina
sempre.
(a)
(b)
(c)
INIZIO
INIZIO
INIZIO
Prendi in input
il valore di V
Prendi in input
il valore di V
Prendi in input
il valore di V
Poni a 1
il valore di U
Poni a 1
il valore di U
Poni a 1
il valore di U
Poni il valore di U
uguale al valore di V
Aumenta di 1
il valore di V
Aumenta di 1
il valore di V
NO
Poni il valore di U
uguale al valore di V
Aumenta di 1
il valore di V
U = V?
SI
NO
NO
U = V?
Produci in output
il valore di U
U = V?
SI
SI
Produci in output
il valore di U
Produci in output
il valore di U
FINE
FINE
(d)
FINE
(e)
(f)
INIZIO
INIZIO
INIZIO
Poni a 1
il valore di U
Prendi in input
il valore di U
Prendi in input
il valore di U
NO
U < 100?
Poni a 100
il valore di v
NO
SI
U ≥ 100?
SI
Aumenta di 1
il valore di U
NO
U ≠ V?
U < 100?
NO
Aumenta di 1
il valore di U
SI
SI
Produci in output
il valore di U
Aumenta di 1
il valore di U
Aumenta di 1
il valore di U
Produci in output
il valore di U
U = V?
SI
FINE
NO
Aumenta di 1
il valore di U
FINE
Produci in output
il valore di U
FINE
Esercizio 1.14. Sia D un insieme di stati. Stabilire se si ottiene una funzione di dominio
D associando a ciascun elemento di D:
a) gli stati ad esso confinanti
b) la sua capitale
c) le sue città con più di 100.000 abitanti
Algoritmi, Macchine, Grammatiche
26
d) il numero delle sue città con più di 100.000 abitanti
e) il numero 5
Esercizio 1.15. Stabilire quali delle seguenti espressioni algebriche definiscono una
funzione ϕ di tipo N → N:
a) ϕ(x) = x + 27
b) ϕ(x) = x – 10
c) ϕ(x) =
d) ϕ(x) =
e) ϕ(x) =
f) ϕ(x) =
Esercizio 1.16. Come il precedente Esercizio 2.3, ma con ϕ: R → R.
Algoritmi, Macchine, Grammatiche
27
SOLUZIONI DI ALCUNI ESERCIZI
RELATIVI ALLA PRIMA PARTE
Esercizio 1.1. Un possibile algoritmo per questo compito è il seguente:
- Prendi in input una sequenza S di parentesi.
(*) - Se S è vuota fermati: S è bilanciata.
- Altrimenti vai avanti fino a che non trovi una “)” oppure fino a che arrivi in
fondo a S.
- Se sei arrivato in fondo a S senza trovare “)”, allora fermati: S non è bilanciata.
- Altrimenti cancella “)” e torna indietro di uno.
- Se trovi una “(“ cancellala e torna a (*). Altrimenti fermati: S non è bilanciata.
Esercizio 1.2. Un possibile diagramma per il compito specificato è il seguente:
INIZIO
Prendi in input
l’elenco L e il
nome N
Metti in M il primo
nome di L
SI
M=N?
Produci in output la
risposta “N è in L”
FINE
Produci in output la
risposta
“N non è in L”
FINE
NO
Cancella il primo
elemento di L
L è vuoto ?
SI
NO
Algoritmi, Macchine, Grammatiche
28
Esercizio 1.3. Un possibile diagramma per il compito specificato è il seguente:
INIZIO
Prendi in input
l’elenco L e il
nome N
Metti in M il nome che si
trova a metà di L
(ossia, se L ha n elementi,
M sarà il nome di posizione
quoz(n, 2) + 1)
SI
Produci in output la
risposta “N è in L”
M = N?
FINE
NO
SI
N precede M in
ordine alfabetico ?
D’ora in avanti sia L la
parte dell’elenco che
precede M
NO
D’ora in avanti sia L la
parte dell’elenco che
segue M
L è vuoto ?
SI
Produci in output la
risposta
“N non è in L”
FINE
NO
Algoritmi, Macchine, Grammatiche
29
Esercizio 1.4. Un possibile diagramma per il compito specificato è il seguente:
INIZIO
Prendi in input la
sequenza S
SI
S è vuota ?
Produci in output la
risposta “La
sequenza è
bilanciata”
FINE
Produci in output la
risposta “La
sequenza non è
bilanciata”
FINE
Produci in output la
risposta
“La sequenza non è
bilanciata”
FINE
NO
Vai avanti fino a che trovi una “)”
oppure arrivi in fondo a S
Sei arrivato in fondo a S
senza trovare “)”?
SI
NO
Cancella “)” e torna
indietro di uno
Hai trovato
una “(“?
NO
SI
Cancella la “(“
Algoritmi, Macchine, Grammatiche
30
Esercizio 1.5. Un possibile algoritmo con le caratteristiche richieste è il seguente:
INIZIO
Prendi in
input i valori
di M e di N
Poni a 0 il
valore di P
Poni a 0 il
valore di C
SI
N = 0?
Produci in output
il valore di P
FINE
Produci in output
il valore di P
FINE
NO
Somma al
valore di P il
valore di M
Incrementa di
1 il valore di C
C ≥ N?
SI
NO
Esercizio 1.6. Una maniera per ottenere l’algoritmo voluto consiste nel modificare
l’algoritmo di fig. 1-9 del testo, in modo che, all’inizio del calcolo, il valore di P venga
posto uguale a 1 anziché a 0, e che l’istruzione Somma al valore di P il valore di M
all’interno del ciclo diventi Moltiplica il valore di P per il valore di M.
Esercizio 1.14. Corrispondono a funzioni b), d), e).
Esercizio 1.15. a) e f).
Esercizio 1.16. Tutte tranne c).
Algoritmi, Macchine, Grammatiche
31
2. Macchine di Turing e teoria della computabilità
2.1. Verso una caratterizzazione rigorosa del concetto di algoritmo
Durante tutta la storia delle matematiche sono stati sviluppati algoritmi per risolvere
classi sempre più estese di problemi. Tuttavia è soltanto in anni recenti che il concetto
stesso di algoritmo è stato fatto oggetto diretto di ricerca matematica. Ciò è avvenuto
attorno agli anni '30 del ‘900, nel contesto delle ricerche sui fondamenti della
matematica. Le ricerche precedenti si erano basate su di una nozione di algoritmo del
tutto intuitiva, non specificata in modo rigoroso. Tale nozione intuitiva fu del tutto
sufficiente fin tanto che lo scopo che ci si proponeva era quello di individuare algoritmi
che risolvessero problemi, o classi di problemi determinate. Ma con le ricerche sui
fondamenti della matematica avvenne un radicale cambiamento di prospettiva. Furono
poste domande di tipo nuovo, che non riguardavano più la possibilità di individuare
algoritmi specifici, ma che concernevano l'intera classe dei procedimenti di tipo
algoritmico. Soprattutto nel contesto del progetto fondazionalista proposto da David
Hilbert, diventava fondamentale rispondere alla domanda se esistessero problemi
matematici che non ammettono neppure in linea di principio di essere risolti mediante
alcun algoritmo. Tutto ciò era a sua volta strettamente legato allo studio delle proprietà
dei sistemi formali della logica matematica. Nel momento in cui la ricerca si indirizzò
allo studio delle proprietà della classe di tutti i procedimenti algoritmici, tale classe
dovette essere caratterizzata in maniera rigorosa, e non fu più sufficiente la tradizionale
definizione informale ed intuitiva. Nacque così quel settore della logica matematica che
è stato detto in seguito teoria della computabilità (o della calcolabilità) effettiva
(oppure anche teoria della ricorsività), in cui vengono indagati concetti quali quello di
algoritmo e di funzione computabile in modo algoritmico.
Durante gli anni '30 numerosi tra i maggiori logici del periodo, tra i quali Gödel,
Church, Post, Kleene e Turing, affrontarono, da punti di vista differenti, il problema di
individuare una definizione rigorosa della nozione di algoritmo. In queste pagine verrà
presentato uno di questi approcci, quello seguito dal logico inglese Alan Turing, che,
rispetto agli studi coevi sulla computabilità, presenta il vantaggio di affrontare il
problema in maniera diretta, analizzando il comportamento di un soggetto umano
computante, senza presupporre altre nozioni o strumenti formali elaborati nell'ambito
della ricerca logico-matematica. Inoltre, Turing ha formulato la sua proposta nei termini
di una particolare classe di macchine astratte, che possono essere considerate modelli
idealizzati dei calcolatori reali. Infine, parte dell'interesse per il lavoro di Turing risiede
nelle implicazioni avute in altri ambiti disciplinari, quali la filosofia della mente,
l'intelligenza artificiale e le scienze cognitive.
Come già abbiamo anticipato, per le caratteristiche di determinismo e di finitezza
che abbiamo enunciato, ogni algoritmo si presta, almeno in linea di principio, ad essere
automatizzato, ad essere eseguito cioè da una macchina opportunamente progettata. Con
lo sviluppo dell'informatica, la teoria della computabilità ha dunque assunto, in un certo
senso, il ruolo di "teoria dei fondamenti" per questa disciplina.
2.2. Le macchine di Turing
Turing affrontò il problema di fornire un equivalente rigoroso del concetto intuitivo
di algoritmo definendo un modello dell'attività di un essere umano che stia eseguendo
un calcolo di tipo algoritmico. Egli elaborò tale modello nella forma di una classe di
Algoritmi, Macchine, Grammatiche
32
dispositivi computazionali, di macchine calcolatrici astratte, che in seguito furono dette
appunto macchine di Turing (d'ora in avanti MT). Le MT sono macchine astratte nel
senso che, nel caratterizzarle, non vengono presi in considerazione quei vincoli che
sono fondamentali se si intende progettare una macchina calcolatrice reale (ad esempio,
le dimensioni della memoria, i tempi del calcolo, e così via), e soprattutto nel senso che
esse sono definite a prescindere dalla loro realizzazione fisica (cioè, dal tipo di
hardware utilizzato). Vale a dire, che cosa sia una MT dipende esclusivamente dalle
relazioni funzionali che sussistono tra le sue parti, e non dal fatto di poter essere
costruita con particolari dispositivi materiali.
Seguiamo l'analisi del processo di calcolo come viene condotta da Turing stesso
nell'articolo "On computable numbers, with an application to the
Entscheidungsproblem" (Turing 1936-37), dove il concetto di MT viene formulato per
la prima volta. Un calcolo, osserva Turing, consiste nell'operare su di un certo insieme
di simboli scritti su di un supporto fisico, ad esempio un foglio di carta. Turing
argomenta che il fatto che abitualmente venga usato un supporto bidimensionale è
inessenziale, e che si può quindi assumere, senza nulla perdere in generalità, che la
nostra macchina calcolatrice utilizzi per la "scrittura" un nastro monodimensionale di
lunghezza virtualmente illimitata in entrambe le direzioni (tuttavia, come vedremo, in
ogni fase del calcolo la macchina potrà disporre soltanto di una porzione finita di esso).
Tale nastro sia inoltre suddiviso in celle, in "quadretti", "come un quaderno di
aritmetica per bambini", ciascuna delle quali potrà ospitare un solo simbolo alla volta
(fig. 2.1).
Quanto ai simboli da utilizzare per il calcolo, ogni macchina potrà disporre soltanto
di un insieme finito di essi, che chiameremo l'alfabeto di quella macchina. Il fatto che
l'alfabeto di cui si può disporre sia finito non costituisce comunque una grave
limitazione. È infatti sempre possibile rappresentare un nuovo simbolo mediante una
sequenza finita di simboli dell'alfabeto, ed avere così la possibilità di esprimere un
numero virtualmente infinito di simboli (come avviene usualmente nella numerazione
decimale mediante cifre arabe). Sia dunque Σ ≡ {s1, s2, ... , sn} l'alfabeto di una generica
MT. Ogni cella del nastro potrà contenere uno di tali simboli, oppure, in alternativa,
restare vuota (indicheremo con s0 la cella vuota).
sj
∆q
nastro
testina di lettura/scrittura
i
Fig. 2.1
Vi è senza dubbio un limite al numero di simboli che un essere umano può
osservare senza spostare lo sguardo sul foglio su cui sta lavorando. Secondo Turing si
può quindi assumere senza perdita di generalità che la macchina possa esaminare
soltanto una cella alla volta, ed "osservare" ad ogni passo al più un singolo simbolo. A
tal fine la macchina sarà dotata di una testina di lettura, che sarà collocata, in ogni fase
del calcolo, su di una singola cella (fig. 2.1). Essa, per poter accedere alle altre celle del
nastro, dovrà spostarsi verso destra o verso sinistra. Chi sta eseguendo un calcolo ha poi
la possibilità di scrivere nuovi simboli, di cancellare quelli già scritti o di sostituirli con
altri. La testina eseguirà anche tali compiti di cancellazione e di scrittura. Anche in
questo caso però essa potrà agire soltanto sulla cella "osservata", e, per accedere ad altre
Algoritmi, Macchine, Grammatiche
33
celle, dovrà prima spostarsi lungo il nastro. Poiché ogni cella può contenere un solo
simbolo, scrivendo un nuovo simbolo in una cella il simbolo eventualmente presente in
essa si deve ritenere cancellato.
Nell'eseguire un calcolo, un essere umano tiene conto delle operazioni già eseguite e
dei simboli osservati in precedenza mediante la propria memoria, cambiando cioè il
proprio "stato mentale". Al fine di simulare ciò, supporremo che una macchina possa
assumere, in dipendenza dagli eventi precedenti del processo di calcolo, un certo
numero di stati interni (uno e non più di uno alla volta), che corrispondano agli "stati
mentali" dell'essere umano. Tali stati saranno in numero finito, poiché (usando la parole
dello stesso Turing) "se ammettessimo un'infinità di stati mentali, alcuni di essi
sarebbero 'arbitrariamente prossimi', e sarebbero quindi confusi". Il limitarsi ad un
numero finito di stati non costituisce tuttavia un vincolo, in quanto "l'uso di stati mentali
più complicati può essere evitato scrivendo più simboli sul nastro". Siano allora
q0, q1, ... , qm gli stati che una generica MT può assumere. Nella rappresentazione
grafica indicheremo sotto la testina di lettura/scrittura lo stato della macchina nella fase
di calcolo rappresentata. Definiamo configurazione di una MT in una data fase di
calcolo la coppia costituita dallo stato interno che essa presenta in quel momento e dal
simbolo osservato dalla testina (la configurazione della macchina rappresentata nella
fig. 2.1 è dunque (qi, sj)).
Una MT può dunque eseguire operazioni consistenti in spostamenti della testina
lungo il nastro, scrittura e cancellazione di simboli, mutamenti dello stato interno.
Scomponiamo tali operazioni in un numero di operazioni atomiche, tali da non poter
essere ulteriormente scomposte in operazioni più semplici. Nel tipo di macchina
descritto ogni operazione può essere scomposta in un numero finito delle operazioni
seguenti: (1) sostituzione del simbolo osservato con un altro simbolo (eventualmente
con s0; in tal caso si ha la cancellazione del simbolo osservato), e/o (2) spostamento
della testina su di una delle celle immediatamente attigue del nastro. Ognuno di tali atti
può inoltre comportare (3) un cambiamento dello stato interno della macchina. Nella
sua forma più generale, ogni operazione atomica dovrà quindi consistere di un
operazione di scrittura e/o di uno spostamento atomico, ed eventualmente di un
mutamento di stato. Indicheremo d'ora in avanti rispettivamente con le lettere S, D e C il
fatto che una macchina debba eseguire uno spostamento di una cella verso sinistra, di
una cella verso destra, oppure non debba eseguire alcuno spostamento (dove C sta per
"centro"). Grazie a ciò potremo rappresentare ogni operazione atomica mediante una
terna, il primo elemento della quale starà ad indicare il simbolo che deve essere scritto
sulla cella osservata, il secondo quale spostamento deve essere eseguito (S, D o C), il
terzo infine lo stato che la macchina deve assumere alla fine dell'operazione. Ad
esempio, la terna:
si S qj
significa che la macchina deve scrivere il simbolo si sulla cella osservata, spostarsi di
una cella a sinistra, ed assumere infine lo stato qj. Invece la terna:
s0 C qp
significa che la macchina deve cancellare il simbolo osservato, non eseguire alcun
movimento ed assumere lo stato qp.
Algoritmi, Macchine, Grammatiche
34
Ogni singola MT è "attrezzata" per eseguire un tipo di calcolo specifico, dispone
cioè di una serie di regole, di istruzioni, che le permettano di eseguire il compito per il
quale è stata progettata. In un calcolo algoritmico ogni passo deve essere
completamente determinato dalla situazione precedente. Nel caso di un calcolatore
umano, ogni sua mossa deve dipendere esclusivamente dal ricordo delle operazioni già
eseguite e dai simboli che egli può osservare. Analogamente, in una MT, poiché in ogni
fase del calcolo la macchina "sa" soltanto in quale stato si trova e quale è il simbolo
sulla cella osservata del nastro (cioè sa quale è la sua configurazione corrente), e poiché
ogni operazione può essere scomposta in operazioni atomiche, allora ogni generica
istruzione avrà la forma seguente:
<configurazione> → <azione atomica>.
In altri termini, ogni istruzione deve specificare quale operazione atomica deve
essere eseguita a partire da una determinata configurazione. Un esempio di istruzione è:
qi sj → sj' D qi',
che deve essere interpretata come segue: qualora la macchina si trovi nello stato qi ed il
simbolo osservato sia sj, allora il simbolo sj' dovrà essere scritto sul nastro al posto di sj,
la testina dovrà spostarsi di una cella verso destra e la macchina dovrà assumere lo stato
qi'. In generale, poiché ogni configurazione è rappresentabile mediante una coppia, ed
ogni operazione atomica mediante una terna, un’istruzione (in cui di solito il simbolo
"→ " viene omesso e considerato sottinteso) avrà la forma di una quintupla, i primi due
elementi della quale (uno stato interno ed un simbolo dell'alfabeto) indicano la
configurazione di partenza, mentre gli ultimi tre elementi specificano l'operazione che
deve essere eseguita. Le istruzioni di cui dispone ogni singola MT per eseguire il
calcolo per il quale è stata progettata avranno quindi la forma di un opportuno insieme
finito di quintuple (che verrà detto la tavola di quella MT). Una volta fissato l'alfabeto,
ciò che caratterizza ogni singola MT rispetto a tutte le altre è appunto la tavola delle sue
quintuple. Affinché un insieme di quintuple costituisca la tavola di una MT è
indispensabile che venga rispettata la seguente condizione: poiché il calcolo deve essere
deterministico, a partire da una singola configurazione non devono essere applicabili
istruzioni diverse. Ciò corrisponde alla condizione che, nella tavola di una macchina,
non possano comparire più quintuple con i primi due elementi uguali.
Affinché il calcolo possa terminare, è necessario che ad alcune delle configurazioni
possibili non corrisponda alcuna quintupla, altrimenti, qualunque fosse il risultato di
una mossa, esisterebbe sempre un'altra mossa che ad essa dovrebbe far seguito.
Chiameremo tali configurazioni configurazioni finali. Data una MT, è sempre possibile
costruirne un'altra che esegua lo stesso calcolo, per la quale esista uno specifico stato
interno che compare in tutte e sole le configurazioni finali. Chiameremo tale stato stato
finale, e stabiliremo convenzionalmente di riservare ad esso il simbolo q0. Data una MT
generica, per trasformarla in una che abbia q0 come stato finale si proceda nel modo
seguente. Sia (qn, sm) una generica configurazione finale della macchina di partenza. La
tavola della nuova macchina si ottiene aggiungendo tutte le quintuple del tipo:
qn sm sm C q0.
Algoritmi, Macchine, Grammatiche
35
In tutti i casi in cui la macchina di partenza giungeva in uno stato finale, la nuova
macchina farà un'ulteriore mossa, assumendo lo stato q0 (e lasciando inalterato tutto il
resto).
I dati vengono forniti a una MT sotto forma di una sequenza finita di simboli
dell'alfabeto scritti sul nastro prima dell'inizio del calcolo. Chiameremo input tale
sequenza di simboli. Il risultato è costituito da ciò che è scritto sul nastro al momento
della fermata, e ciò costituisce l'output del calcolo. Stabiliamo convenzionalmente che
all'inizio del calcolo la testina debba essere collocata in posizione standard, vale a dire
in corrispondenza del primo simbolo a sinistra dell'input; inoltre lo stato interno della
macchina debba essere q1.
Vediamo un semplice esempio di MT. Si consideri l'alfabeto Σ ≡ {|}, composto
come unico simbolo da una barra verticale. Definiamo una macchina che, presa come
input una successione di barre consecutive, restituisca come output tale successione
aumentata di un elemento. A tal fine è sufficiente disporre del solo stato interno q1
(oltre allo stato finale q0); la tavola della macchina sarà la seguente:
q1
q1
|
s0
|
|
D
C
q1
q0.
Secondo le convenzioni stabilite, alla partenza la testina deve essere collocata sul
primo simbolo a sinistra dell'input, e lo stato di partenza deve essere q1 (fig. 2.2).
∆q
1
Fig. 2.2
Fintanto che la testina trova celle segnate con | allora, in virtù della prima quintupla,
viene riscritto | sulla cella osservata (cioè, vengono lasciate le cose come stanno), e la
testina si sposta a destra di una cella mantenendo lo stato q1 (fig. 2.3).
∆q
1
∆q
1
................
∆q
1
Fig. 2.3
Quando la testina incontra una cella vuota viene attivata la seconda quintupla, in
virtù della quale la macchina deve segnare con una barra la cella osservata, non eseguire
alcuno spostamento, ed assumere lo stato finale q0 (fig. 2.4).
Algoritmi, Macchine, Grammatiche
36
∆q
0
Fig. 2.4
Sin qui abbiamo considerato MT che eseguono calcoli su dati generici. Vediamo ora
come si possano codificare i numeri naturali in modo da definire MT che calcolino
funzioni aritmetiche. Utilizziamo come alfabeto Σ ≡ {|}. I numeri naturali vengono
codificati come segue. Al numero 0 viene fatta corrispondere la sequenza composta da
una sola barra. In generale, ogni numero n viene codificato da una sequenza di n + 1
barre. Una n-pla di numeri naturali (k1, ... , kn) viene codificata sul nastro scrivendo la
sequenza di barre corrispondente ad ogni ki (con 1 ≤ i ≤ n), e lasciando una cella vuota
come separatore tra ognuna di tali sequenze. Ad esempio, la terna (4, 1, 0) viene
codificata come in fig. 2.5.
Diremo che una macchina Mϕ computa una funzione aritmetica ϕ ad n argomenti
(con n ≥ 1) sse quanto segue vale per ogni n-pla (x1, ... , xn) di numeri naturali. Sia
(x1, ... , xn) codificata nel modo sopra descritto e collocata in posizione standard rispetto
alla testina (essendo vuota ogni altra cella del nastro). Allora ϕ(x1, ... , xn) = y sse, al
termine del calcolo, l'output di Mϕ è costituito dalla codifica di y.
Fig. 2.5
Diremo che una funzione ϕ è T-computabile sse esiste una MT Mϕ che la computa.
2.3. Esempi di macchine di Turing
In questo paragrafo presentiamo alcuni esempi di MT che eseguono semplici
calcoli. Eccetto i casi in cui sia indicato esplicitamente, ognuna delle macchine ha Σ ≡
{|}, e, al momento dell'avvio, la testina deve essere collocata sulla prima cella a sinistra
dell'input. Lo stato iniziale è q1.
1. MT che esegue l'addizione di due numeri. Input: codifica di una coppia di numeri
naturali.
q1
q2
q3
|
|
|
|
|
s0
D
D
S
q1
q2
q4
q1
q2
q4
s0
s0
|
|
s0
s0
D
S
C
q2
q3
q0
La macchina riempie con una barra la cella vuota che separa i due numeri dell'input,
dopo di che cancella le due barre finali del secondo numero.
2. MT che raddoppia il numero di | consecutive che le viene dato in input. Input:
sequenza di |.
Algoritmi, Macchine, Grammatiche
37
q1
q2
q3
q5
q6
q7
|
s0
s0
|
|
|
s0
s0
|
|
|
|
D
D
D
S
S
S
q2
q3
q4
q5
q7
q7
q2
q3
q4
q5
q6
q7
|
|
s0
s0
s0
s0
|
|
|
s0
s0
s0
D
D
S
S
C
D
q2
q3
q5
q6
q0 (*)
q1
La macchina cancella la prima barra a destra dell'input, dopo di che si colloca a
destra dell'input (lasciando una cella vuota come separatore), e stampa due barre. Torna
quindi indietro, e ripete l'operazione sino a che tutte le barre dell'input sono state
cancellate.
3. MT che calcola la funzione ϕ(x)=2x. Input: codifica di un numero naturale. La
tavola è la stessa della macchina precedente, in cui la quintupla (*) è stata sostituita
dalle tre quintuple seguenti:
q6
q8
s0
|
s0
s0
D
C
q8
q0
q8
s0
s0
D
q8
Poiché la codifica di un numero n è costituita da n+1 barre, la codifica di 2n è
costituita da 2n+1 = 2(n+1)-1 barre. Quindi questa macchina, dopo avere raddoppiato il
numero delle barre in input, cancella una barra e si ferma.
4. MT che calcola la funzione il cui valore è 1 se l'argomento è un numero pari, 0 se
l'argomento è dispari. Input: codifica di un numero naturale.
q1
q1
q3
|
s0
s0
s0
|
|
D
C
C
q2
q0
q0
q2
q2
|
s0
s0
|
D
D
q1
q3
La macchina cancella successivamente tutte le barre dell'input, assumendo
alternativamente gli stati q1 e q2. Se, quando l'intero input è stato cancellato, lo stato è
q1 (il che accade se l'input era la codifica di un numero dispari), la macchina stampa una
barra (la codifica di 0). Altrimenti, se lo stato è q2 (se cioè l'input era pari), stampa due
barre (la codifica di 1). Dopo di che si ferma.
5. MT che calcola la differenza tra due numeri naturali. Input: codifica di una coppia
di numeri naturali, di cui il primo maggiore o uguale del secondo. All'inizio del calcolo
la testina deve essere collocata sulla prima cella a destra dell'input.
q1
q2
q3
q4
|
|
s0
|
s0
|
s0
s0
S
S
S
D
q2
q3
q4
q5
Algoritmi, Macchine, Grammatiche
q2
q3
q4
q5
s0
|
s0
s0
s0
|
s0
s0
C
S
S
D
q0
q3
q4
q5
38
q5
q6
|
s0
|
s0
D
S
q6
q1
q6
|
|
D
q6
La macchina cancella una barra dalla codifica del secondo numero in input. Dopo di
che cancella alternativamente una barra dalla codifica del secondo e del primo numero
in input, sino a che la codifica del secondo numero non è stata cancellata
completamente.
6. MT che controlla se una sequenza di parentesi è bilanciata, se cioè, per ogni
parentesi aperta, esiste una parentesi chiusa corrispondente. Σ ≡ { ( , ) , X }. Input: una
sequenza di "(" e ")" (senza celle vuote in mezzo). Output: una sequenza di sole X se le
parentesi dell'input erano bene accoppiate; altrimenti, una sequenza di simboli di Σ
comprendente "(" oppure ")".
q1
q1
q2
q3
q2
(
)
(
)
s0
(
)
X
X
s0
D
S
D
D
C
q1
q2
q3
q1
q0
q1
q2
q3
q1
X
X
X
s0
X
X
X
s0
D
S
D
C
q1
q2
q3
q0
La macchina percorre l'input da sinistra verso destra mantenendosi nello stato q1
finché non trova una ")"; allora passa in q2 e torna in dietro fino alla prima "(" che
incontra, che sostituisce con una X; assume quindi lo stato q3 e torna a destra, a
sostituire con X anche la ")" precedentemente individuata; dopo di che, torna in q1 e
ripete da capo l'operazione; si ferma non appena la testina incontra una cella vuota.
2.4. La tesi di Church
Nel 1936 il logico americano Alonzo Church, in seguito alle sue ricerche sulla
computabilità effettiva, propose di identificare la classe delle funzioni calcolabili
mediante un algoritmo (o funzioni effettivamente calcolabili) con una particolare classe
di funzioni aritmetiche, detta in seguito classe delle funzioni ricorsive generali (Church
1936). Tale identificazione divenne nota col nome di Tesi di Church. È possibile
dimostrare l'equivalenza tra la classe delle funzioni ricorsive generali e la classe delle
funzioni T-computabili, in quanto ogni funzione T-computabile è ricorsiva generale, e
viceversa (Turing 1937). La Tesi di Church può quindi essere formulata come segue:
una funzione è effettivamente calcolabile sse è T-computabile.
Che ogni funzione ricorsiva generale (o T-computabile) sia effettivamente
computabile segue direttamente e in modo ovvio dalla definizione di T-computabilità e
di MT. Ciò che invece è interessante nella Tesi di Church è l'implicazione inversa,
secondo la quale ogni procedimento algoritmico è riconducibile alla ricorsività
generale. Algoritmo e funzione computabile in modo effettivo sono concetti intuitivi,
non specificati in modo rigoroso, per cui non è possibile una dimostrazione formale di
equivalenza con il concetto di funzione ricorsiva generale. La Tesi di Church non è
dunque una congettura che, in linea di principio, potrebbe un giorno diventare un
Algoritmi, Macchine, Grammatiche
39
teorema. Tuttavia, la nozione intuitiva di funzione computabile in modo effettivo è
contraddistinta da un insieme di caratteristiche (quali determinismo, finitezza di calcolo,
eccetera) che possiamo considerare in larga misura "oggettive". Questo fa sì che sia
praticamente sempre possibile una valutazione concorde nel decidere se un dato
procedimento di calcolo debba essere considerato algoritmico o meno. Quindi, almeno
in linea di principio, è ammissibile che venga "scoperto" un controesempio alla Tesi di
Church; è ammissibile cioè che venga individuata una funzione effettivamente
calcolabile secondo questi parametri intuitivi, la quale non sia allo stesso tempo
ricorsiva generale. In questo paragrafo esporremo le ragioni per cui si ritiene
improbabile che un evento del genere si verifichi.
Prima di procedere, è opportuno un chiarimento. Le funzioni ricorsive generali (o Tcomputabili) sono esclusivamente funzioni aritmetiche. Ciò potrebbe sembrare troppo
restrittivo, in quanto esistono algoritmi definiti su oggetti diversi dai numeri naturali. Vi
sono algoritmi che stabiliscono se un certo oggetto matematico appartiene a un dato
insieme o meno. Ve ne sono altri che eseguono operazioni simboliche sulle espressioni
di un sistema formale, stabilendo ad esempio se una data formula gode o meno di una
certe proprietà. In logica matematica tuttavia sono state sviluppate tecniche mediante le
quali algoritmi di tipo diverso, quali quelli sopra citati, possono essere ricondotti a
funzioni aritmetiche. I numeri naturali infatti possono essere utilizzati per rappresentare,
mediante opportune codifiche, dati o informazioni di varia natura, purché di tipo
discreto (si ricordi a questo proposito il par. 1.4). Nel caso delle MT, si può dimostrare
che ogni macchina con un alfabeto Σ di simboli finito può essere trasformata in una
macchina equivalente che calcola una funzione aritmetica T-computabile come definita
nel par. 2.
Seguendo in parte l'analisi del logico S.C. Kleene (1952, §62), raccoglieremo gli
argomenti a favore della Tesi di Church in due gruppi, (a) e (b).
(a) Il primo gruppo di argomenti poggia su quella che si può chiamare evidenza
euristica. Rientra in questo gruppo la constatazione che, per ogni singola funzione
calcolabile che sia stata esaminata, è sempre stato possibile dimostrare la sua
appartenenza alla classe delle funzioni ricorsive generali. Analogamente, si è dimostrato
che le operazioni note per definire funzioni effettivamente calcolabili a partire da altre
funzioni effettivamente calcolabili conservano la ricorsività generale. Tale indagine è
stata condotta per un grande numero di funzioni, di classi di funzioni e di operazioni.
Infine, i vari metodi tentati per costruire funzioni effettivamente calcolabili che non
fossero ricorsive generali hanno condotto tutti al fallimento, nel senso che le funzioni
ottenute erano tutte a loro volta ricorsive generali, oppure non erano calcolabili in modo
effettivo.
(b) Nel secondo gruppo di argomenti viene considerata l'equivalenza delle diverse
formulazioni proposte. Abbiamo accennato al fatto che numerosi studiosi hanno
lavorato ad una definizione rigorosa del concetto di algoritmo. Ebbene, tutti i tentativi
che furono elaborati per caratterizzare in modo rigoroso la classe di tutte le funzioni
effettivamente computabili si rivelarono equivalenti, nel senso che la classe di funzioni
ottenuta era sempre la classe delle funzioni ricorsive generali. Ciò che è particolarmente
rilevante ai fini di una "corroborazione" della Tesi di Church è la diversità degli
strumenti e dei concetti impiegati nelle diverse formulazioni. In molti casi tali
formulazioni traggono la loro origine da concetti matematici preesistenti. Nel caso della
ricorsività generale di Herbrand-Gödel si prendono le mosse dal concetto di sistema di
equazioni, nella λ-ricorsività di Church (1936) si parte dall'idea di un calcolo di sole
funzioni, il λ-calcolo. Schönfinkel (1924) e Curry (1929, 1930, 1932) elaborarono il
Algoritmi, Macchine, Grammatiche
40
cosiddetto calcolo dei combinatori. Ad E. Post (1943, 1946) è dovuto l'approccio basato
sui sistemi normali o canonici. Negli anni cinquanta, il logico sovietico A. A. Markov
(1951, 1954) propose un'ulteriore formulazione tramite quelli che vennero poi detti
appunto algoritmi di Markov. Tale indipendenza dalla formulazione utilizzata è
ovviamente un forte elemento a favore della Tesi di Church.
Un posto a sé merita l'apporto all'evidenza della Tesi di Church fornito dall'analisi
del concetto di calcolo algoritmico compiuta da Turing. Il concetto di macchina di
Turing si distingue dalla maggior parte degli approcci sopra elencati in quanto non si
tratta di un concetto matematico elaborato per ragioni diverse e proposto in un secondo
tempo come formulazione rigorosa del concetto di algoritmo, quanto piuttosto di un
tentativo diretto di costruire un modello dell'attività di un essere umano che esegue un
calcolo di tipo deterministico. Storicamente, fu proprio l'analisi di Turing ad aumentare
notevolmente il convincimento della correttezza della Tesi di Church.
Questo tipo di approccio al problema della computabilità effettiva ha condotto
alcuni studiosi a considerare la Tesi di Church come una sorta di "legge empirica"
piuttosto che come un enunciato a carattere logico-formale". Il logico Emil Post, il
quale, nel 1936, propose un concetto di macchina calcolatrice in parte analogo a quello
sviluppato da Turing, sottolineava il suo disaccordo da chi tendeva ad identificare la
Tesi di Church con un assioma o una mera definizione. Essa dovrebbe piuttosto essere
considerata, afferma Post, una ipotesi di lavoro, che, se opportunamente corroborata,
dovrebbe assumere il ruolo di una "legge naturale", una "fondamentale scoperta circa le
limitazioni del potere matematizzante dell'Homo sapiens" (Post 1936, pag. 105).
La Tesi du Church può essere utilizzata come "scorciatoia" nelle dimostrazioni: per
dimostrare che esiste una MT che svolge un certo compito, si fa vedere che c'è un
algortimo intuitivo per quel compito e poi, appellandosi appunto alla Tesi di Church, si
conclude che esiste una MT equivalente. Vedremo alcuni esempi del genere nella terza
parte di questa dispensa. Ovviamente, ai fini di una dimostrazione rigorosa e completa
ciò non è sufficiente, ed è necessario dimostrare in maniera diretta l'esistenza della MT
desiderata.
2.5. La macchina di Turing universale e il calcolatore di von Neumann
L'interesse delle MT per la teoria delle macchine calcolatrici e per l'informatica
risiede innanzi tutto nel fatto che le MT sono un modello del calcolo algoritmico, di un
tipo di calcolo quindi che è, in linea di principio, automatizzabile, eseguibile cioè da un
dispositivo meccanico. Ogni MT è il modello astratto di un calcolatore - astratto in
quanto prescinde da alcuni vincoli di limitatezza cui i calcolatori reali devono
sottostare; ad esempio, la memoria di una MT (vale a dire il suo nastro) è
potenzialmente estendibile all'infinito (anche se, in ogni fase del calcolo, una MT può
sempre utilizzarne solo una porzione finita), mentre un calcolatore reale ha sempre
limiti ben definiti di memoria.
Vi sono altre ragioni che giustificano l'analogia tra MT e moderni calcolatori
digitali. Sino ad ora abbiamo considerato MT che sono in grado di effettuare un solo
tipo di calcolo, sono cioè dotate di un insieme di quintuple che consente loro di
calcolare una singola funzione (ad esempio la somma, o il prodotto). Esiste tuttavia la
possibilità di definire una MT, detta Macchina di Turing Universale (d'ora in poi
MTU), che è in grado di simulare il comportamento di ogni altra MT. Ciò è reso
possibile dal fatto che le quintuple di ogni MT possono essere rappresentate in maniera
tale da poter essere scritte sul nastro di una MT. Abbiamo accennato al fatto (par. 4) che
Algoritmi, Macchine, Grammatiche
41
i numeri naturali possono essere utilizzati per codificare informazioni di tipo discreto di
diverso genere. In particolare, è possibile sviluppare un metodo per codificare mediante
numeri naturali la tavola di una qualsiasi MT. In questo modo, il codice di una MT può
essere scritto sul nastro e dato in input a un'altra MT. Inoltre, tale codifica può essere
definita in maniera tale che, dato un codice, si possa ottenere la tavola corrispondente e
viceversa mediante un procedimento algoritmico (una codifica che goda di questa
proprietà è detta una codifica effettiva).
Si può dimostrare che esiste un MT (la MTU appunto) che, preso in input un
opportuno codice effettivo delle quintuple di un'altra macchina, ne simula il
comportamento. In altre parole, la MTU è una macchina il cui input è composto da due
elementi (si veda la parte superiore di fig. 2.6): 1. la codifica della tavola di una MT
(chiamiamola M), 2. un input per M (chiamiamolo I). Per ogni M e per ogni I, la MTU
"decodifica" le quintuple di M, e le applica ad I, ottenendo lo stesso output che M
avrebbe ottenuto a partire da I (come schematizzato nella parte inferiore di fig. 2.6).
Inizio del calcolo:
Codifica della
macchina M
Input I per la
macchina M
∆
q1
Fine del calcolo:
Output della macchinaM
per l'input I
∆
q0
Fig. 2.6
Poiché la MTU è in grado di simulare il comportamento di qualsiasi MT, allora
essa, in virtù della Tesi di Church, è in grado di calcolare qualsiasi funzione che sia
calcolabile mediante un algoritmo. Ciò che caratterizza la MTU rispetto alle MT usuali
è costituito dal fatto di essere una macchina calcolatrice programmabile. Mentre infatti
le normali macchine di Turing eseguono un solo programma, che è "incorporato" nella
tavola delle loro quintuple, la MTU assume in input il programma che deve eseguire
(cioè, la codifica delle quintuple della MT che deve simulare), e le quintuple che
compongono la sua tavola hanno esclusivamente la funzione di consentirle di
interpretare e di eseguire il programma ricevuto in input.
Un'altra caratteristica fondamentale della MTU è dato dal tipo di trattamento
riservato ai programmi. La MTU tratta i programmi (cioè la codifica delle quintuple
della MT da simulare) e i dati (l'input della MT da simulare) in maniera sostanzialmente
analoga: essi vengono memorizzati sullo stesso supporto (il nastro), rappresentati
utilizzando lo stesso alfabeto di simboli ed elaborati in modo simile. Queste
caratteristiche sono condivise dagli attuali calcolatori, che presentano la struttura nota
come architettura di von Neumann (dal nome dello scienziato di origine ungherese John
von Neumann che la ideò). La struttura di un calcolatore di von Neumann è raffigurata,
Algoritmi, Macchine, Grammatiche
42
molto schematicamente, nella fig. 2.7. Un dispositivo di input e un dispositivo di output
permettono di accedere dall'esterno alla memoria del calcolatore, consentendo,
rispettivamente, di inserirvi e di estrarne dei dati. Le informazioni contenute in memoria
vengono elaborate da una singola unità di calcolo (detta CPU - Central Processing
Unit), che opera sequenzialmente su di essi. La caratteristica più importante della
macchina di von Neumann è costituita dal fatto che sia dati che programmi vengono
trattati in modo sostanzialmente omogeneo, ed immagazzinati nella stessa unità di
memoria. Così, quando un programma deve essere eseguito, l'unità di calcolo lo
reperisce in memoria, e lo applica quindi ai dati, anch'essi conservati in memoria.
Questo consente una grande flessibilità al sistema. Ad esempio, poiché dati e
programmi sono oggetti di natura omogenea, è possibile costruire programmi che
prendano in input altri programmi e li elaborino, e che producano programmi in output.
Queste possibilità sono ampiamente sfruttate negli attuali calcolatori digitali, e da esse
deriva gran parte della loro potenza e della loro facilità d'uso (ad esempio, un
compilatore o un sistema operativo sono essenzialmente programmi che operano su altri
programmi). In questo senso limitato, un calcolatore di von Neumann costituisce una
realizzazione concreta della MTU (e la memoria dati/programmi può essere considerata
l'equivalente del nastro della MTU). Anche la potenza computazionale è la stessa, nel
senso che, se lo si suppone dotato di una memoria e di tempi di calcolo virtualmente
illimitati, un calcolatore di von Neumann è in grado di calcolare tutte le funzioni
computabili secondo la Tesi di Church (per questo si dice che una macchina di von
Neumann è un calcolatore universale). La MTU costituisce quindi un modello astratto
degli attuali calcolatori digitali (elaborato prima della loro realizzazione fisica).
CPU
unità
di input
memoria
dati/programmi
unità
di output
Fig. 2.7
Si noti che anche i vari linguaggi di programmazione sviluppati in informatica
consentono di definire tutte e sole le funzioni ricorsive generali (purché, ovviamente, si
supponga che tali linguaggi "girino" su calcolatori ideali con memoria e tempi di
calcolo illimitati). Questo vale sia per i linguaggi di programmazione di alto livello
(come PASCAL, FORTRAN, BASIC, VISUAL BASIC, C, C++, JAVA, LISP,
PROLOG, eccetera), sia per i vari tipi di codice assembler. In questo senso, tali
linguaggi possono essere considerati analoghi ai vari formalismi citati al punto (b) del
par. 4.
2.6. Il problema della fermata
La tesi di Church ha molte importanti conseguenze dal punto di vista teorico. Dalla
sua validità consegue l'esistenza di problemi che non sono risolvibili mediante un
algoritmo (come si ricorderà, stabilire se tutti i problemi matematici possono essere in
linea di principio risolti con un algoritmo era stata una delle motivazioni principali per
lo studio rigoroso del concetto di algoritmo). In particolare, si può dimostrare che non è
Algoritmi, Macchine, Grammatiche
43
effettivamente decidibile il problema della fermata (halting problem) per le MT, cioè il
problema di stabilire se, per ogni MT M e per ogni input I, M con input I termina il suo
calcolo o meno. Va precisato innanzi tutto che il fatto che la tavola di una MT
comprenda almeno una configurazione finale è una condizione necessaria ma non
sufficiente perché la macchina termini il calcolo. Si consideri ad esempio la MT
seguente:
q1 s0 s0 D q1
q1 |
s0 C q2
La coppia (q2, s0) costituisce una configurazione finale; se tuttavia questa
macchina viene attivata col nastro completamente vuoto, il suo calcolo andrà avanti
all'infinito.
L'indecidibilità del problema della fermata comporta che non esista alcun
algoritmo che, data una generica MT (o il suo codice secondo una opportuna codifica
effettiva) e dato un generico input per essa, consenta di stabilire se il calcolo di quella
macchina con quell'input termina o meno. Diamo qui di seguito una breve traccia
intuitiva di come, partendo dalla tesi di Church, si possa giungere a questo risultato
ragionando per assurdo. Data una generica macchina M, sia CM il suo codice in base a
una codifica effettiva (scritta nell'alfabeto Σ ≡ {|}). Supponiamo, per assurdo, che il
problema della fermata per le MT sia decidibile. Per la tesi di Church, questo comporta
che deve esistere una certa macchina di Turing H si comporti nella maniera seguente.
Per ogni macchina di Turing M e per ogni input I di M,
H con input C M e I
dà come output 1 se il calcolo di M per l'input I termina
dà come output 0 se il calcolo di M per l' input I non termina.
Qualora esistesse la macchina H, allora sarebbe banale costruire un'altra macchina
H' che si comporti come segue:
H ' con input C
M
dà come output 1 se il calcolo di M per l'input C termina
M
dà come output 0 se il calcolo di M per l' input C
M
non termina.
H' infatti calcola una funzione che è un "caso particolare" della funzione calcolata
da H (in quanto CM è un valore particolare di I). Se tuttavia esistesse H', allora si
potrebbe a sua volta costruire una macchina Z così definita:
Z con input C
M
genera un calcolo che non termina se H ' con input C dà come output 1
M
(cioè, se il calcolo di M per l'input CM termina)
dà come output 0 se H ' con input C dà come output 0
M
(cioè, se il calcolo di M per l'input C non termina).
M
Per ottenere Z a partire da H' sarebbe sufficiente aggiungere alla tavola di H'
alcune quintuple che facciano in modo che, se l'output di H' è 1, allora abbia origine un
Algoritmi, Macchine, Grammatiche
44
calcolo che non termina (ad esempio, la testina potrebbe iniziare a spostarsi a destra sul
nastro qualunque sia il simbolo osservato).
Ora, si immagini di dare in input a Z il suo stesso codice CZ. E' facile constatare
che, in base alla definizione di Z, Z con input CZ darebbe origine a un calcolo che
termina se e soltanto se il calcolo di Z per l'input CZ non termina, il che è palesemente
assurdo. Ne consegue quindi che una macchina che si comporti come H non può
esistere, e che quindi, se è vera la tesi di Church, non può esistere un algoritmo che
decida il problema della fermata6.
Da questo risultato consegue che esistono problemi i quali, neppure in linea di
principio, possono essere risolti da un calcolatore. Ad esempio, non può esistere alcun
programma che sia grado di stabilire in generale se un programma qualsiasi con un
certo input terminerà il suo calcolo o meno. E' importante ricordare che l'indecidibilità
del problema della fermata è strettamente collegata ai risultati di limitazione della
logica matematica, in primo luogo i teoremi di Gödel.
2.7. Le macchine di Turing e la mente: il test di Turing
Abbiamo accennato alla tendenza ad interpretare la Tesi di Church come un'ipotesi
empirica sulle capacità computazionali degli esseri umani. Su questa linea procedono
alcuni sviluppi successivi del pensiero dello stesso Turing. Nel suo saggio "Macchine
calcolatrici ed intelligenza" (Turing 1950) assistiamo ad una sorta di "radicalizzazione"
di questo modo di intendere la Tesi di Church. Facendo riferimento a calcolatori reali,
che tuttavia vengono caratterizzati in maniera analoga a macchine di Turing, Turing si
dichiara fiducioso che macchine di questo tipo possano giungere a simulare, nel volgere
di pochi decenni, non soltanto il "comportamento computazionale" di un essere umano,
ma anche qualsiasi altra attività cognitiva umana. Turing propone di riformulare la
domanda "possono pensare le macchine?" nei termini del cosiddetto gioco
dell'imitazione. Il gioco viene giocato da tre "attori": a) un essere umano, b) una
macchina calcolatrice e c) un altro essere umano, l'interrogante. L'interrogante non può
vedere a) e b), non sa chi dei due sia l'essere umano, e può comunicare con loro solo in
maniera indiretta (ad esempio, attraverso un terminale video e una tastiera).
L'interrogante deve sottoporre ad a) e a b) delle domande, in maniera tale da scoprire,
nel più breve tempo possibile, quale dei due sia l'uomo e quale la macchina. a) si
comporterà in modo da agevolare c), mentre b) dovrà rispondere in modo da ingannare
c) il più a lungo possibile. Invece di chiedersi se le macchine possono pensare, dice
Turing, è più corretto chiedersi se una macchina possa battere un uomo nel gioco
dell'imitazione, o, comunque, quanto a lungo possa resistergli. Questo "esperimento
mentale" viene oggi abitualmente indicato col nome di Test di Turing.
Turing era decisamente troppo ottimista circa le possibili prestazioni delle macchine
calcolatrici: "Credo che entro circa 50 anni sarà possibile programmare calcolatori ...
per far giocare loro il gioco dell'imitazione così bene che un esaminatore medio non
avrà più del 70 per cento di probabilità di compiere l'identificazione esatta dopo cinque
6
Nel paragrafo 2.4 abbiamo detto che si può usare la Tesi di Church come "scorciatoia" per dimostrare
teoremi che possono essere dimostrati anche in maniera diretta. Si noti che questo non è un caso del
genere: la dimostrazione dell'indecidibilità del problema della fermata dipende in maniera essenziale
dall'accettazione della Tesi di Church. In particolare, senza fare appello alla Tesi di Church, si può
dimostrare che nessuna MT può decidere il problema della fermata (per ottenere una dimostrazione
rigorosa di questo fatto basta far vedere nei dettagli come, supposto per assurdo che esista la macchina H,
si possano costruire le macchine H' e Z). Da questo risultato, facendo appello alla Tesi di Church, si può
concludere che il problema della fermata non può essere deciso da nessun algoritmo.
Algoritmi, Macchine, Grammatiche
45
minuti di interrogazione. Credo che la domanda iniziale, 'possono pensare le
macchine?', sia troppo priva di senso per meritare una discussione. Ciò nonostante
credo che alla fine del secolo l'uso delle parole e l'opinione corrente si saranno talmente
mutate che chiunque potrà parlare di macchine pensanti senza aspettarsi di essere
contraddetto". Ci troviamo qui di fronte ad una sorta di versione "estremista", o
"radicale", della Tesi di Church, che, grosso modo, potrebbe essere formulata come
segue: ogni attività cognitiva è T-computabile (il che non vuol dire, ovviamente, che la
nostra mente funziona come una macchina di Turing, ma che ogni attività mentale è
simulabile da un dispositivo che abbia la stessa potenza computazionale delle macchine
di Turing). Seppure modificata e raffinata rispetto alla formulazione di Turing, una
assunzione di questo genere è a fondamento di numerose teorie e ricerche svolte
nell'ambito di quel settore di ricerca che va sotto il nome di scienze cognitive. Si tratta
di un ambito di ricerca interdisciplinare che ha per oggetto lo studio della mente, e che
raccoglie i contributi di diverse discipline quali la psicologia cognitiva, la linguistica, la
filosofia, l'informatica e le neuroscienze. Ciò che accomuna le ricerche svolte nelle
scienze cognitive è appunto l'ipotesi che gli strumenti di tipo computazionale possano
essere in qualche misura adeguati come modelli per lo studio delle facoltà mentali. In
particolare, tra le scienze cognitive, l'intelligenza artificiale è quel settore
dell'informatica che si prefigge di elaborare programmi di calcolatore che simulino
specifiche attività cognitive umane, sia allo scopo di meglio comprendere queste ultime,
sia allo scopo di costruire manufatti tecnologicamente rilevanti.
2.8. Oltre von Neumann: reti neurali e calcolo parallelo
Nel corso della storia dell'informatica, sono stati proposti vari modelli alternativi al
calcolatore di von Neumann. Infatti, benché siano estremamente versatili, alle macchine
con architettura di von Neumann sono stati imputati dei limiti dal punto di vista
informatico. In particolare, è stata criticata la netta separazione tra immagazzinamento
ed elaborazione dei dati che questo tipo di architettura comporta. In un calcolatore di
von Neumann memoria e unità centrale di calcolo (CPU) sono due componenti
rigidamente distinte. L'unità di calcolo attinge di volta in volta ai dati contenuti nella
memoria, ma quest'ultima rimane sostanzialmente passiva durante la maggior parte
della durata del calcolo. Si tratta del cosiddetto problema del "collo di bottiglia"
dell'architettura di von Neumann: le informazioni vengono elaborate solo quando
vengono richiamate dalla CPU del sistema. Ciò comporta problemi di efficienza nello
sfruttamento delle risorse computazionali.
Queste limitazioni hanno un corrispettivo anche dal punto di vista dello studio
computazionale della mente. Una distinzione netta tra memorizzazione delle
informazioni e loro elaborazione è difficilmente giustificabile sulla base delle
conoscenze disponibili sul sistema nervoso. Nel cervello non esiste alcun dispositivo
centralizzato per il controllo dell'elaborazione. Le operazioni computazionali nel
sistema nervoso sembrano demandate ad un meccanismo di controllo altamente
distribuito. Inoltre, non esiste una separazione netta tra dispositivi per la
memorizzazione e per l'elaborazione delle informazioni. Ciò pone problemi ai modelli
computazionali dell'intelligenza artificiale e delle scienze cognitive tradizionali, relativi
alla mancanza di plausibilità dal punto di vista anatomico e neurofisiologico del
paradigma computazionale di Turing e di von Neumann. Il punto centrale è che il
cervello è un dispositivo di calcolo altamente parallelo. Il numero dei neuroni è di un
ordine stimabile tra 1010 e 1011, e ciascuno di essi si comporta come una singola unità
Algoritmi, Macchine, Grammatiche
46
di calcolo, che lavora contemporaneamente a tutte le altre. I neuroni sono altamente
interconnessi: ogni neurone ha moltissime sinapsi in entrata e in uscita, mediante le
quali scambia i propri input e i propri output con gli altri neuroni. Ogni neurone esegue
operazioni relativamente semplici. La complessità dei meccanismi cognitivi viene
determinata dall'interazione di un grande numero di neuroni.
Su considerazioni di questo genere si è basato lo sviluppo delle cosiddette reti
neurali, una classe di dispositivi di calcolo in parte motivati dall'intento di superare i
limiti del modello di von Neumann. Si tratta di sistemi distribuiti ad alto parallelismo,
ispirati, in senso lato, alle proprietà del sistema nervoso. Una rete neurale è costituita da
un insieme di unità (che sono il corrispettivo dei neuroni), collegate tra loro da
connessioni, che costituiscono l'analogo delle sinapsi. Ogni unità ha un certo numero di
connessioni in ingresso e/o un certo numero di connessioni in uscita. Ciascuna unità
costituisce un semplice processore, un singolo dispositivo di calcolo che, ad ogni fase
del calcolo, riceve i propri input attraverso le connessioni in ingresso, li elabora, e invia
l'output alle altre unità connesse per mezzo delle sue connessioni in uscita. In una rete
tutte le unità operano in parallelo, e non esiste alcun processo di ordine "più alto",
nessuna CPU che ne coordini l'attività. Il calcolo che ciascuna unità esegue è di norma
molto semplice; la potenza computazionale del sistema deriva dal grande numero delle
unità e delle connessioni. Nell'ambito delle scienze cognitive, sulle reti neurali si basano
le teorie e i modelli di tipo connessionista. Il connessionismo è una tendenza nello
studio computazionale della mente che ha avuto un grande sviluppo nel corso degli
ultimi quindici anni, e che si è in parte contrapposta agli approcci dell'intelligenza
artificiale e delle scienze cognitive tradizionali. Rispetto a queste ultime, il
connessionismo è caratterizzato appunto da una maggiore attenzione per i rapporti tra
attività cognitive e struttura del sistema nervoso.
E' opportuno notare tuttavia che questi sviluppi non hanno comportato un
superamento dei risultati della teoria della computabilità effettiva, o una qualche forma
di "falsificazione" della tesi di Church. Di fatto, tutti i modelli computazionali basati
sulle reti neurali che siano stati effettivamente realizzati sono risultati riconducibili
entro i limiti della ricorsività generale, nel senso che le funzioni computate da tali
modelli risultano essere funzioni ricorsive generali.
Più in generale, benché le MT e i calcolatori con architettura di von Neumann siano
dispositivi di calcolo di tipo strettamente sequenziale, è possibile estendere la validità
della tesi di Church anche a calcoli di tipo parallelo. Rilevanti in questa direzione sono
state ad esempio le ricerche di Robin Gandy (1980), che è partito dalla constatazione
che il concetto di MT corrisponde ad una nozione di calcolo troppo specifica e
particolare perché le possa essere ricondotto ogni tipo di dispositivo di calcolo
concepibile. Ad esempio, nelle MT si assume che il calcolo proceda secondo una
sequenza di passi elementari, elaborando un solo simbolo alla volta, mentre un
calcolatore artificiale può procedere in parallelo, elaborando contemporaneamente un
numero arbitrario di simboli. Per superare tali limitazioni, Gandy ha formulato,
utilizzando strumenti di tipo insiemistico, una caratterizzazione estremamente generale
del concetto di macchina calcolatrice, in cui le MT rientrano come caso particolare. Egli
ha dimostrato quindi che ogni funzione calcolabile da tali macchine è ricorsiva
generale, a patto che vengano rispettate alcune condizioni molto generali di finitezza
(determinismo, possibilità di descrivere il calcolo in termini discreti, e così via).
Va ricordato tuttavia che, dal punto di vista applicativo, l’architettura di von
Neumann (o sue varianti che non ne differiscono in maniera sostanziale) resta il
modello di calcolatore di gran lunga più diffuso. Attualmente calcolatori con
Algoritmi, Macchine, Grammatiche
47
architettura non di von Neumann vengono progettati e utilizzati per tipi di applicazioni
specifiche.
Algoritmi, Macchine, Grammatiche
48
3. Computabilità, automi e grammatiche formali
3.1 Premessa
Una delle caratteristiche più peculiari dei linguaggi umani è il loro carattere
generativo: dati un insieme finito di espressioni di partenza (le parole o i morfemi di
una lingua) e le capacità cognitive dei parlanti di generare e comprendere enunciati
(capacità che a loro volta sono presumibilmente finite) il linguaggio consente di
produrre e comprendere un numero praticamente illimitato di enunciati.
Si consideri ad esempio l'enunciato seguente:
(*) Le cinquantasei lepri che ieri aggredirono il vescovo di Salerno sono fuggite
in autobus verso Savona
È molto plausibile che una frase come (*) non sia mai stata formulata da nessuno
prima d’ora. Tuttavia, nonostante la sua bizzarria, non abbiamo alcuna difficoltà nel
riconoscerla come una frase del tutto corretta della lingua italiana, a differenza ad
esempio di:
(**) Uno lepre cinquantasei ieri aggredendo Salerno verso fuggire autobus
autobus
che, sebbene composta interamente da parole italiane, non è una frase dell’italiano.
Il carattere generativo del linguaggio riguarda sia il piano sintattico, sia quello
semantico. Sul piano sintattico noi riconosciamo (*) come una frase corretta
dell’italiano anche se la incontriamo per la prima volta. Sul piano semantico, se
conosciamo il significato delle parole che vi compaiono, siamo in grado di comprendere
il significato di (*), per quanto stravagante e inatteso esso possa apparirci. In questa
sede ci occuperemo esclusivamente degli aspetti sintattici7.
Argomento di questo capitolo è quel settore di ricerca in cui si sviluppano modelli
formali in grado di rendere conto del carattere generativo dei sistemi linguistici, siano
essi lingue naturali o linguaggi artificiali. La teoria delle grammatiche formali studia le
proprietà di sistemi che consentono di caratterizzare linguaggi eventualmente infiniti
utilizzando strumenti finiti. Si tratta di un settore di ricerca nato nell’ambito della logica
matematica e della teoria della computabilità, che ha avuto importanti sviluppi in
linguistica ad opera soprattutto di Noam Chomsky e ha trovato in seguito applicazione
anche in informatica8.
3.2 Che cos’è una grammatica formale?
Una grammatica formale consente di specificare (con mezzi finiti) quali sono le
espressioni sintatticamente corrette (ossia grammaticali) di un linguaggio.
7
Gli aspetti semantici del carattere generativo dei linguaggi vengono indagati soprattutto nell’ambito
della semantica formale. Utilizzando strumenti di tipo logico e insiemistico, la semantica formale studia
come il significato di espressioni sintatticamente complesse di un linguaggio dipenda dal significato delle
espressioni che le compongono. Queste tecniche vengono applicate sia alle lingue naturali, sia a linguaggi
artificiali come i linguaggi di programmazione. Sulla semantica formale delle lingue naturali si vedano ad
esempio Casalegno (1997) e Chierchia e McConnell-Ginet (1990).
8
De Palma (1974) è una raccolta di articoli classici che ripercorrono la nascita di questo settore di
ricerca.
Algoritmi, Macchine, Grammatiche
49
Dato un alfabeto A, ossia un insieme finito di simboli, una stringa (o parola) su A
è una n-pla ordinata di elementi A.
Se l’alfabeto è Al = {a, b, c, d, e, +, −, *, /, (, )}, un esempio di stringa su Al è la 7pla seguente:
(a, d, +, (, (, a, *)
Nello scrivere le stringhe ometteremo le virgole e le parentesi all’inizio e alla fine,
per cui scriveremo la stringa precedente più semplicemente come segue:
a d + ( (a *
Altre stringhe su Al sono le seguenti:
(i)
(ii)
(iii)
(iv)
(v)
(vi)
a+b
(((+ − (*))))
a b c ( d ))/
a * (c − e)
(e + c)/(a + (a − b))
+ab
Alcune di esse, cioè la (i), la (iv) e la (v), sono espressioni algebriche “corrette”,
mentre le altre sono sequenze arbitrarie di simboli. Al può quindi essere usato per
scrivere espressioni algebriche ben formate, le quali costituiscono un sottoinsieme
proprio dell’insieme di tutte le stringhe su Al.
In generale, chiamiamo linguaggio L con alfabeto A un sottoinsieme di tutte le
stringhe su A e stringhe grammaticali di L le stringhe che appartengono a L.
Potremmo ad esempio definire un linguaggio con alfabeto Al che includa come
grammaticali espressioni algebriche come (i), (iv) e (v) ed escluda stringhe “casuali”
come (ii), (iii) e (vi).
Una grammatica per un linguaggio L è uno strumento per specificare in modo
rigoroso quali sono le stringhe grammaticali di L rispetto a tutte le stringhe che si
possono scrivere con lo stesso alfabeto.
Vediamo un esempio. Consideriamo il seguente alfabeto:
S = {il, un, topo, gatto, rincorre, mangia, dorme, canta}
Vogliamo individuare una grammatica che consenta di caratterizzare quelle
stringhe sull’alfabeto S che corrispondono a frasi sintatticamente corrette dell’italiano
(come ad esempio il topo canta oppure un gatto rincorre il topo), distinguendole dalle
stringhe che non corrispondono a espressioni sintatticamente corrette dell’italiano
(come ad esempio un un topo canta gatto) o non costituiscono frasi complete (ad
esempio il gatto).
Una grammatica del genere può essere formulata come segue:
(1)
(2)
(3)
(4)
(5)
(6)
E → SN SV
SN → Articolo Nome
SV → VerbTrans SN
SV → VerbIntr
VerbTrans → rincorre
VerbTrans → mangia
Algoritmi, Macchine, Grammatiche
50
(7)
(8)
(9)
(10)
(11)
(12)
VerbIntr → dorme
VerbIntr → canta
Articolo → un
Articolo → il
Nome → topo
Nome → gatto
Le espressioni del tipo e1 → e2 vengono chiamate produzioni, o regole di
produzione, o regole di riscrittura. Una grammatica comprende sempre un insieme
finito di produzioni. Nelle espressioni e1 ed e2 di ogni regola di produzione compaiono
simboli dell’alfabeto del linguaggio, assieme ad altri simboli che non ne fanno parte.
Questi ultimi vengono detti simboli non terminali della grammatica. La grammatica
del nostro esempio (che d’ora in avanti chiameremo G) utilizza i simboli non terminali
“E”, “S”, “N”, “Articolo”, “Nome”, “VerbTrans” e “VerbIntr”. Seguiremo la
convenzione di scrivere i simboli non terminali in tondo e con iniziale maiuscola, per
distinguerli dai simboli dell’alfabeto che sono scritti in corsivo con iniziale minuscola
(d’ora in poi chiameremo i simboli dell’alfabeto anche simboli terminali della
grammatica). I simboli non terminali possono essere interpretati come nomi metateorici
di categorie sintattiche. Ad esempio, in G “E” sta per “enunciato”, “SN” per “sintagma
nominale”, “SV” per “sintagma verbale”; il significato intuitivo degli altri simboli non
terminali di G è ovvio. Data una regola di produzione e1 → e2, in e1 deve sempre
comparire almeno un simbolo non terminale.
Data una grammatica per un certo linguaggio, vediamo come si ottengono le
stringhe che fanno parte del linguaggio. Si parte da uno specifico simbolo non terminale
detto assioma (o simbolo iniziale) della grammatica. Ogni grammatica deve avere uno
ed un solo assioma. In G l’assioma è il simbolo E. Una regola e1 → e2 indica che ogni
volta che si trova l’espressione e1 la si può riscrivere (cioè sostituire) con l’espressione
e2. A partire dall’assioma si procede applicando le regole, in modo da riscrivere parti
dell’espressione via via ottenuta, fino a ottenere una stringa composta esclusivamente di
simboli terminali. Si dice allora che la grammatica consente di derivare tale stringa, e
che la stringa fa parte del linguaggio generato dalla grammatica. Il linguaggio generato
da una grammatica comprende tutte e sole le stringhe che la grammatica consente di
derivare. Ad esempio G consente di derivare le stringhe un gatto canta e il gatto
rincorre un topo, le quali quindi fanno parte del linguaggio generato da G, mentre non
consente di derivare la stringa un gatto canta il topo, che quindi non fa parte del
linguaggio generato da G.
Verifichiamo che la grammatica G consente di derivare il gatto rincorre un topo.
Si parte dall’assioma:
E
In base alla regola (1) (che è l’unica che in questo momento è possibile applicare)
possiamo riscrivere “E” sostituendolo con “SN SV”:
SN SV
In base alla regola (2) “SN” si può riscrivere come “Articolo Nome”, per cui
otteniamo:
Articolo Nome SV
Algoritmi, Macchine, Grammatiche
51
In base alla regola (10) “Articolo” si può riscrivere con il simbolo terminale “il”,
per cui otteniamo:
il Nome SV
E così via. Riportiamo tutti i passaggi che consentono di ottenere il gatto rincorre
un topo a partire dall’assioma E.
1) E
2) SN SV
3) Articolo Nome SV
4) il Nome SV
5) il gatto SV
6) il gatto Verb Trans SN
7) il gatto rincorre SN
8) il gatto rincorre Articolo Nome
9) il gatto rincorre un Nome
10) il gatto rincorre un topo
assioma
da 1) per mezzo della regola (1)
da 2) per mezzo della regola (2)
da 3) per mezzo della regola (10)
da 4) per mezzo della regola (12)
da 5) per mezzo della regola (3)
da 6) per mezzo della regola (5)
da 7) per mezzo della regola (2)
da 8) per mezzo della regola (9)
da 9) per mezzo della regola (11)
I passi 1)-10) costituiscono una derivazione in G della stringa il gatto rincorre un
topo. Il processo di derivazione non è deterministico: ad ogni passo si può scegliere tra
le varie produzioni disponibili quale applicare.
Per formulare le grammatiche in maniera più sintetica si adotta di solito la
seguente convenzione. Quando si hanno più regole del tipo A → B1, A → B2, …,
A → Bn (cioè regole nelle quali la parte a sinistra della freccia è la stessa), si scrivono
sulla stessa riga nella maniera seguente: A → B1 | … | Bn. Adottando questa
convenzione, le regole di G si possono formulare più concisamente:
E → SN SV
SN → Articolo Nome
SV → VerbTrans SN | VerbIntr
VerbTrans → rincorre | mangia
VerbIntr → dorme | canta
Articolo → il | un
Nome → topo | gatto
È facile constatare che la grammatica G consente di generare solo un insieme
finito di stringhe. Ossia, il linguaggio generato da G è un linguaggio finito. Non è
difficile estenderla in maniera da generare un linguaggio infinito. Passiamo ad esempio
dall’alfabeto S all’alfabeto S’ = S ∪ {e, oppure, ma}, aggiungiamo il simbolo non
terminale “Connettivo” e le produzioni seguenti:
E → E Connettivo E
Connettivo → e | oppure | ma
Questa nuova grammatica G’ consente di generare tutte le (infinite) stringhe del
tipo: un gatto dorme ma il topo canta, il gatto rincorre il topo e il topo dorme, un gatto
dorme e un topo canta oppure un gatto mangia il topo, e così via.
Algoritmi, Macchine, Grammatiche
52
Un altro modo per estendere G in modo da generare un linguaggio infinito è il
seguente. Passiamo dall’alfabeto S all’alfabeto S” = S ∪ {che} e sostituiamo la seconda
produzione con:
SN → Articolo Nome | SN che SV
Con questa nuova grammatica, che chiameremo G”, diventa possibile generare
tutte le (infinite) stringhe del tipo: un gatto che dorme rincorre il topo, un gatto
rincorre il topo che dorme, il topo che rincorre il gatto canta, il topo che rincorre il
gatto che dorme canta, e così via.
La possibilità di generare infinite stringhe dipende dalla presenza di produzioni in
cui il simbolo non terminale a sinistra della freccia compare anche nell’espressione che
si trova sulla destra (in G’ si tratta del simbolo “E” nella prima delle nuove produzioni;
in G” è il simbolo “SN”).
In generale, lo stesso linguaggio può essere generato da grammatiche diverse.
Consideriamo ad esempio due grammatiche G1 e G2, che utilizzano entrambe l’alfabeto
{a, b, c} e impiegano i simboli non terminali S, A, B e C (dei quali S è l’assioma). Le
produzioni di G1 e di G2 sono rispettivamente:
S → S1 C
S1 → A B
A → aA | a
B → bB | b
C → cC | c
S → A S1
S1 → B C
A → aA | a
B → bB | b
C → cC | c
Le due grammatiche sono diverse (in particolare, sono diverse le prime due
produzioni), ma il linguaggio generato è lo stesso: coincide con l’insieme di tutte e sole
le stringhe in cui m occorrenze di a sono seguite da n occorrenze di b, seguite a loro
volta da k occorrenze di c (con m, n, k ≥ 1). Si possono cioè derivare tutte le stringhe
del tipo aaabbbbbbcc, abbbbbcc, aabcccc, abcc.
Si dicono equivalenti due grammatiche che generano lo stesso linguaggio.
3.3 La gerarchia di Chomsky
Alla fine degli anni ’50 il linguista Noam Chomsky (1959) propose una
classificazione delle grammatiche formali che oggi va sotto il nome di gerarchia di
Chomsky. Egli individuò quattro classi di grammatiche, ciascuna delle quali include la
successiva.
La classe più generale della gerarchia di Chomsky è costituita dalle grammatiche
di tipo 0, nelle quali non si impone alcun vincolo sulla forma delle produzioni: può
essere usata qualunque regola e1 → e2.
La classe successiva è costituita dalle grammatiche di tipo 1, dette anche
grammatiche non decrescenti. In una grammatica non decrescente non deve esserci
alcuna produzione e1 → e2 in cui e2 sia più corta di e1 (la lunghezza di e1 e di e2 è data
dal numero di simboli terminali e/o non terminali che le compongono). Ad esempio, una
produzione come la seguente può far parte di una grammatica di tipo 0, ma non di una
grammatica di tipo 1:
Algoritmi, Macchine, Grammatiche
53
Sa→b
in quanto l’espressione a sinistra della freccia è formata da due simboli mentre quella a
destra è formata da un solo simbolo.
Da ciò il nome di questa classe: applicando una produzione a un’espressione
quest’ultima non può mai decrescere, cioè non può mai accadere che, dopo aver
applicato una regola, si ottenga un’espressione più corta di quella da cui si era partiti.
Si può facilmente constare che le grammatiche G, G’ e G” del paragrafo
precedente sono grammatiche non decrescenti9. Un altro esempio di grammatica non
decrescente è la grammatica GnD, che ha come alfabeto l’insieme {a, b, c}, come
simboli non terminali S e B, dei quali S è l’assioma, e le cui produzioni sono:
S → aSBc | abc
cB → Bc
bB → bb
Il linguaggio generato da questa grammatica comprende tutte e sole le stringhe del
tipo abc, aabbcc, aaabbbccc,…, in cui n occorrenze di a sono seguite da n occorrenze
di b, seguite a loro volta da n occorrenze di c (con n ≥ 1).
Si può dimostrare che, per ogni grammatica non decrescente, esiste una
grammatica ad essa equivalente in cui tutte le produzioni hanno la forma:
αAβ→αγβ
dove A è un simbolo non terminale e α, β e γ sono stringhe di simboli terminali e/o non
terminali. Le stringhe α e β (ma non la stringa γ) possono avere lunghezza nulla10. In
base a una produzione di questo tipo, se il simbolo non terminale A è collocato tra le
stringhe α e β, allora A può essere sostituito dalla stringa γ. Ossia, in questo genere di
produzioni la possibilità di riscrivere un simbolo non terminale dipende dal contesto in
cui il simbolo compare. Per questa ragione le grammatiche di tipo 1 vengono dette
anche grammatiche dipendenti dal contesto (o grammatiche context sensitive).
Le grammatiche di tipo 2 sono dette anche grammatiche libere dal contesto
(context free). Tutte le produzioni di una grammatica libera dal contesto hanno la forma:
A→γ
dove A è un simbolo non terminale e γ è una stringa di simboli terminali e/o non
terminali. Questo schema corrisponde al caso particolare dello schema α A β → α γ β
in cui α e β hanno entrambe lunghezza nulla. In questo caso il simbolo non terminale A
può essere sostituito con γ a prescindere dal contesto in cui A compare. Da qui il nome
di grammatiche libere dal contesto.
Un esempio di grammatica di tipo 2 è la grammatica LC, che ha come alfabeto
l’insieme {a, b}, come unico simbolo non terminale l’assioma S, e le cui produzioni
sono:
Si noti che una grammatica che includa tra le sue regole Aa → b | C | dD non è una grammatica di tipo 1
(cioè non decrescente). Infatti questa scrittura equivale alle tre produzioni seguenti : Aa → b, Aa → C,
Aa → dD, le prime due delle quali violano il vincolo imposto sulle produzioni di questa classe di
grammatiche.
10
Se γ avesse lunghezza nulla la grammatica non sarebbe non decrescente.
9
Algoritmi, Macchine, Grammatiche
54
S → aSb
S → ab
Il linguaggio generato da LC comprende tutte e sole le stringhe del tipo ab, aabb,
aaabbb, …, in cui n occorrenze di a sono seguite da n occorrenze di b (con n ≥ 1).
Anche le grammatiche G, G’ e G” del paragrafo precedente sono libere dal
contesto.
L’ultima classe di grammatiche della gerarchia di Chomsky è la classe della
grammatiche di tipo 3, o grammatiche lineari. Una grammatica si dice lineare destra
(o regolare) se tutte le produzioni hanno la forma:
A→tB
oppure:
A→t
dove A e B sono simboli non terminali e t è un simbolo terminale. Una grammatica si
dice lineare sinistra se tutte le produzioni hanno la forma
A→Bt
oppure:
A→t
dove A e B sono simboli non terminali e t è un simbolo terminale. Si può dimostrare
che, per ogni grammatica lineare destra, ne esiste una lineare sinistra ad essa
equivalente, e viceversa.
Un esempio di grammatica lineare (destra) è la grammatica LD che ha come
alfabeto l’insieme {a, b}, come simboli non terminali S e B, dei quali S è l’assioma, e le
cui produzioni sono:
S → aS
S→B
B → bB
B→b
Il linguaggio generato da LD comprende tutte e sole le stringhe del tipo ab, abb,
aab, aabb, abbb, aaab, aabbb, …, in cui n occorrenze di a sono seguite da m
occorrenze di b (con m, n ≥ 1).
Si può constatare agevolmente dalle definizioni che tutte le grammatiche di tipo 3
sono anche grammatiche di tipo 2, ma non viceversa (esistono cioè grammatiche di tipo
2 che non sono grammatiche di tipo 3), che tutte le grammatiche di tipo 2 sono anche
grammatiche di tipo 1, ma non viceversa, e così via. Perciò, se indichiamo con Gi
l’insieme delle grammatiche di tipo i (con i che va da 0 a 3), valgono le seguenti
relazioni di inclusione stretta:
G3 ⊂ G2 ⊂ G1 ⊂ G0
Diremo che un linguaggio è di tipo i (con i che va da 0 a 3) quando esiste una
grammatica di tipo i che lo genera. Si può dimostrare che tra gli insiemi dei linguaggi
Algoritmi, Macchine, Grammatiche
55
valgono relazioni di inclusione analoghe a quelle tra le grammatiche: se Li è l’insieme
dei linguaggi di tipo i (con i che va da 0 a 3), valgono le seguenti relazioni di inclusione
stretta:
L3 ⊂ L2 ⊂ L1 ⊂ L0
Pertanto, ci sono linguaggi generati da una grammatica di tipo Gi che non possono
essere generati da alcuna grammatica di tipo Gi+1. Ad esempio, si può dimostrare che il
linguaggio generato dalla grammatica dipendente dal contesto GnD che abbiamo
descritto sopra non può essere generato da alcuna grammatica libera dal contesto, e che
il linguaggio generato dalla grammatica libera dal contesto LC non può essere generato
da alcuna grammatica lineare.
I linguaggi finiti sono un sottoinsieme proprio dei linguaggi di tipo 3: ogni
linguaggio finito può essere generato da una grammatica lineare. Ad esempio, abbiamo
visto che la grammatica G del paragrafo precedente è una grammatica libera dal
contesto (quindi di tipo 2), ed è facile constatare che non è una grammatica lineare.
Poiché, come abbiamo osservato, il linguaggio che essa genera è finito, esiste tuttavia
una grammatica lineare ad essa equivalente11.
3.4
Alberi di derivazione e grammatiche ambigue
In una grammatica di tipo 2 la derivazione di una stringa può anche essere
rappresentata graficamente per mezzo di un albero, detto albero sintattico, o albero di
derivazione (sugli alberi e la relativa terminologia si veda la finestra “Alberi”).
Vediamo ad esempio l’albero di derivazione nella grammatica G della stringa il gatto
rincorre un topo (fig. 3-1).
E
(1)
SV (3)
SN (2)
SN (2)
Articolo (10)
Il
Nome (12) VerbTrans (5) Articolo (9)
gatto
rincorre
un
Nome(11)
topo
Figura 3-1
In un albero di derivazione le foglie sono simboli terminali e i nodi che non sono
foglie sono simboli non terminali. In particolare, la radice deve essere l’assioma della
grammatica. Leggendo le foglie da sinistra verso destra si ottiene la stringa derivata. Se
11
Si veda a questo proposito l’esercizio 3.11.
Algoritmi, Macchine, Grammatiche
56
un nodo S ha come successori i nodi S1, …., Sn, allora nella grammatica vi è una
produzione S → S1 …. Sn che è stata impiegata per riscrivere il simbolo S.
Nella figura 3-1, a fianco a ogni nodo abbiamo riportato il numero della
produzione corrispondente.
Alberi
In matematica è detto albero un insieme ordinato in modo tale che i suoi
elementi, detti nodi, si possono disporre come nella fig. 3-2.
• (1)
• (2)
• (4)
• (3)
• (5) • (6) • (7) • (8)
• (9)
• (10)
Figura 3-2
• (11)
Il nodo (1) più in alto, da cui parte lo sviluppo dell’albero, è detto radice. Ogni
albero ha una e una sola radice. I nodi (2) e (3) sono i successori della radice; (4), (5)
e (6) sono i successori di (2), e così via. I nodi che non hanno successori vengono
detti foglie. Nell’albero della fig. 3-2 le foglie sono (4), (5), (9), (7), (10) e (11).
Vi sono grammatiche che assegnano a una stessa stringa strutture sintattiche
diverse: la stessa stringa può essere generata in modi diversi, attraverso derivazioni che
non differiscono soltanto per l’ordine con cui sono state applicate le regole. Le
grammatiche con questa caratteristica vengono dette ambigue. Nel caso di
grammatiche libere dal contesto, una grammatica ambigua associa a certe stringhe
alberi di derivazione differenti.
Un esempio di grammatica ambigua è la grammatica G’ del paragrafo 2. Si
consideri la stringa:
(***) un gatto corre e un topo canta oppure un gatto canta
Essa può essere analizzata nei termini dei due alberi della fig. 3-3 (questi due
alberi non corrispondono a un’analisi sintattica completa: per non complicare troppo le
figure, le tre stringhe un gatto corre, un topo canta e un gatto canta non sono state
analizzate).
Algoritmi, Macchine, Grammatiche
57
(a)
E
E
E
Connettivo
E
un gatto corre
e
Connettivo
un topo canta oppure
(b)
E
un gatto canta
E
E
Connettivo
E
E
Connettivo
un gatto corre
e
E
un topo canta
oppure un gatto canta
Figura 3-3
Gli alberi (a) e (b) corrispondono a derivazioni diverse in G’. In (a) il connettivo e
collega la stringa un gatto corre alla stringa un topo canta oppure un gatto canta; in (b)
il connettivo oppure collega la stringa un gatto corre e un topo canta alla stringa un
gatto canta.
Se interpretiamo le stringhe generate da G’ come enunciati, e i connettivi come
connettivi proposizionali cui è associata l’usuale interpretazione verofunzionale, allora
la differenza di struttura sintattica tra (a) e (b) ha anche una controparte semantica: nel
caso in cui un gatto corre sia falso e un gatto canta sia vero, se (***) viene analizzata
come in (a) risulta falsa, se invece viene analizzato come in (b) risulta vera.
Nel caso di sistemi artificiali come i linguaggi di programmazione l’ambiguità è
un fenomeno da evitare ad ogni costo, per cui le grammatiche sviluppate per tali sistemi
devono essere definite in maniera tale da non essere ambigue. Nel caso delle lingue
naturali invece l’ambiguità è un dato di fatto di cui si deve inevitabilmente tenere conto
(ad esempio la stringa (***) corrisponde a un enunciato perfettamente grammaticale ma
ambiguo dell’italiano). Nel caso di costrutti linguistici ambigui le grammatiche formali
sviluppate per lo studio delle lingue naturali devono essere in grado di generare
strutture sintattiche diverse che rendano conto delle diverse analisi possibili.
3.5
Grammatiche e macchine
Alle diverse classi della gerarchia di Chomsky sono associate varie proprietà
computazionali delle grammatiche e delle classi di linguaggi corrispondenti.
Distinguiamo tra due tipi di problemi rilevanti rispetto alle proprietà delle
grammatiche. Generare un linguaggio consiste nel produrre una dopo l’altra mediante
un algoritmo (o una macchina) tutte e sole le stringhe che costituiscono il linguaggio.
Riconoscere (o accettare) un linguaggio è invece un problema di decisione: presa in
Algoritmi, Macchine, Grammatiche
58
input una stringa di simboli dell’alfabeto, si tratta di produrre una risposta positiva se la
stringa appartiene al linguaggio, e una risposta negativa in caso contrario.
Computazionalmente riconoscere un linguaggio è più difficile che generarlo:
come vedremo, in generale il riconoscimento di un linguaggio richiede strumenti più
potenti della sua generazione.
Insiemi ricorsivamente numerabili e insiemi ricorsivi
Intuitivamente, diciamo che un insieme è ricorsivamente enumerabile se e solo
se esiste una MT che ne genera uno dopo l'altro tutti gli elementi; diciamo che un
insieme è ricorsivo se esiste una MT che, preso in in put (la codifica di) un oggetto, è
in grado di decidere se esso appartiene all'insieme o meno.
Si può dimostrare che tutti gli insiemi ricorsivi sono anche ricorsivamente
enumerabili, ma che, in generale, non vale il viceversa (esistono cioè in siemi
ricorsivamente enumerabili che non sono ricorsivi).
Da queste definizioni, e dalle definizioni di generazione e di accettazione di un
linguaggio date nel testo, segue che un linguaggio è generato da una MT se e solo se
costituisce un insieme ricorsivamente enumerabile; e che un linguaggio è accettato da
una MT se e solo se costituisce un insieme ricorsivo.
Si può dimostrare che, data qualunque grammatica a struttura di frase (cioè,
qualunque grammatica di tipo 0), il linguaggio da essa generato costituisce un insieme
ricorsivamente enumerabile, ossia può essere generato da una MT (vedi la finestra
Insiemi ricorsivamente numerabili e insiemi ricorsivi )12.
Diamo la traccia di una dimostrazione di questo fatto che fa appello alla Tesi di
Church (dello stesso risultato si può dare una dimostrazione rigorosa diretta, che non
faccia appello alla Tesi di Church).
Data una qualsiasi grammatica a struttura di frase, si può definire come segue un
algoritmo intuitivo che generi il linguaggio corrispondente, ossia produca una dopo
l’altra tutte le stringhe del linguaggio generato:
•
•
•
si parte dall’assioma;
si selezionano tutte le produzioni che possono essere applicate all’assioma, e si
applicano una dopo l’altra ottenendo un insieme (finito) E1 di espressioni;
per ciascun elemento di E1 si selezionano tutte le produzioni che possono essergli
applicate; poi si applicano una dopo l’altra agli elementi di E1 le produzioni
corrispondenti ottenendo un nuovo insieme E2 (sempre finito) di espressioni,
e così via.
Man mano che nel corso del processo si ottiene una stringa di soli simboli
terminali la si produce in output: tale stringa fa parte del linguaggio generato dalla
grammatica. Se una stringa appartiene al linguaggio generato, questo procedimento
prima o poi consente produrla in output.
12
Se definiamo un linguaggio estensionalmente, come un insieme qualunque di stringhe su un alfabeto,
allora non tutti i linguaggi possono essere generati da una grammatica. Tutti i possibili insiemi di stringhe
su un dato alfabeto costituiscono un insieme più che numerabile. Esiste dunque un insieme più che
numerabile di linguaggi. L’insieme di tutte le grammatiche, invece, è un insieme numerabile. Di
conseguenza esistono infiniti linguaggi che non sono generati da alcuna grammatica.
Algoritmi, Macchine, Grammatiche
59
In base alla Tesi di Church, tutto ciò che può essere fatto da un algortimo intuitivo
può essere fatto anche da una MT, per cui ogni linguaggio generato da una grammatica
di tipo 0 può essere generato anche da una MT, e quindi, in base alla definizione,
costituisce un inseme ricorsivamente enumerabile.
Analogamente, si può dimostrare che, data una grammatica di tipo 1 (non
decrescente), il linguaggio da essa generato costituisce un insieme ricorsivo, ossia può
essere accettato da una MT (vedi ancora la finestra Insiemi ricorsivamente numerabili e
insiemi ricorsivi ).
Anche qui, diamo la traccia di una dimostrazione di questo fatto che fa appello
alla Tesi di Church (tenendo conto che lo stesso risultato si può dimostrare in maniera
diretta, senza fare appello alla Tesi di Church).
Data una grammatica di tipo 1, si verifica facilmente che esiste un algoritmo
intuitivo per decidere se una stringa appartiene o meno al linguaggio generato dalla
grammatica. Presa in input una stringa s, sia n la sua lunghezza. Si applica un
procedimento simile a quello dell’algoritmo precedente con la differenza che, ogni volta
che si ottiene un’espressione la cui lunghezza è maggiore di n, la si scarta e non la si
prende più in considerazione. Infatti, per come sono definite le grammatiche di tipo 1,
l’applicazione delle regole non può mai ridurre la lunghezza delle espressioni, per cui
possiamo essere certi che s non potrà essere generata a partire da espressioni più lunghe
di n. In questo modo, se s è una stringa del linguaggio, come nel caso precedente verrà
generata in un numero finito di passi. Se s invece non fa parte del linguaggio ce ne
renderemo conto perché, a un certo punto, non verranno più prodotte stringhe nuove
poiché tutte le espressioni ottenute superano la lunghezza n. Il procedimento potrà così
terminare producendo una risposta negativa.
In virtù della Tesi di Church, se, per ogni linguaggio di tipo 1, esiste un algoritmo
intuitivo che lo accetta, allora esiste anche da una MT che lo accetta. Quindi, possiamo
concludere che ogni linguaggio di tipo 1 può essere riconosciuto da una MT, e quindi
cosituisce un insieme ricorsivo. Si può dimostrare che questo risultato non può essere
esteso alle grammatiche di tipo 0; esistono cioè linguaggi di tipo 0 che non sono
ricorsivi.
Si può dimostrare che, per riconoscere (o accettare) i linguaggi di tipo 2, sono
sufficienti dispositivi di calcolo meno potenti delle MT, detti automi a pila, i quali,
invece del nastro delle MT, impiegano un supporto di memoria meno flessibile, detto
appunto pila (in inglese stack). A differenza del nastro delle MT, lungo il quale la
testina può scorrere liberamente in entrambe le direzioni, una pila si comporta come una
catasta di libri appoggiati su un piano: si può accedere soltanto all’elemento in cima alla
pila, eventualmente rimuoverlo oppure aggiungerne uno nuovo sopra gli altri (si veda la
finestra “Automi a pila”).
Per riconoscere i linguaggi generati dalle grammatiche di tipo 3 sono sufficienti
dispositivi ancora meno potenti, detti automi a stati finiti. Sostanzialmente, un automa
a stati finiti è un dispositivo di calcolo che ha solo un numero finito di stati di memoria
interni (analoghi agli stati di una MT), ma non ha alcun supporto di memoria esterno
come il nastro delle MT o la pila degli automi a pila (si veda la finestra “Automi a stati
finiti”).
Algoritmi, Macchine, Grammatiche
60
Possiamo ricapitolare quanto detto in questo paragrafo per mezzo di una tabella:
Tipi di linguaggi
Accettati da un automa a stati finiti
Accettati da un automa a pila
Accettati da una MT
(tutti i linguaggi di tipo 1 sono ricorsivi)
Generati da una MT
(i linguaggi di tipo 0 sono ricorsivamente
enumerabili ma, in generale, non ricorsivi)
Tipo 3 (Lineari)
Tipo 2 (Liberi dal contesto)
Tipo 1 (Dipendenti dal contesto)
Tipo 0
Automi a stati finiti
Un automa a stati finiti (o automa finito, in simboli AF) è una macchina
calcolatrice astratta con associato un insieme finito Q = {q0,…, qn} di stati interni. In
ciascuna fase del calcolo un automa finito si trova in uno e uno solo degli stati di Q.
L’automa riceve via via dall’esterno una successione di dati in input. La transizione
allo stato successivo viene determinata esclusivamente sulla base dello stato corrente
e dall’input ricevuto. In un certo senso, gli automi a stati finiti sono delle MT “senza
nastro”: gli stati interni sono analoghi a quelli delle MT, ma, mentre per le MT il
nastro costituisce una memoria aggiuntiva potenzialmente illimitata sulla quale
leggere, scrivere e spostarsi a piacimento, un automa a stati finiti può tenere traccia
delle operazioni eseguite solo attraverso i cambiamenti di stato. Pertanto gli automi a
stati finiti risultano molto meno potenti delle MT.
Qui prenderemo in considerazione automi a stati finiti che riconoscono, o
accettano, le stringhe di un linguaggio. Si tratta di automi che prendono in input uno
dopo l’altro (da sinistra verso destra) i simboli che compongono una stringa. Si dice
che un automa AFL riconosce, o accetta, un linguaggio L se e solo se, per ogni stringa
S, AFL è in grado di decidere se S appartiene a L o meno.
Tra gli stati q0,…, qn di un automa si deve specificare uno stato iniziale e un
insieme non vuoto di stati finali (che può comprendere eventualmente anche lo stato
iniziale). Come stato iniziale utilizzeremo q0. All’inizio del calcolo un automa si trova
sempre nello stato q0; a questo punto legge in input il primo simbolo della stringa S e
il calcolo comincia. Ogni automa è attrezzato con un “programma”, un insieme di
istruzioni che, sulla base dello stato corrente e del simbolo letto, specifica lo stato
successivo. Una volta avvenuto il cambio di stato, viene preso in input il simbolo
successivo della stringa, e così via. Le istruzioni di un automa finito hanno la forma:
qi
sj
qk
dove qi è lo stato corrente, sj è il simbolo preso in input e qk è lo stato successivo: se
lo stato è qi e il simbolo in input è sj, allora l’automa assume lo stato qk (e passa a
leggere in input il simbolo successivo).
Quando un automa AFL si ferma, se la stringa S degli input è stata esaminata
interamente, e se AFL si trova in uno stato finale, allora S fa parte del linguaggio L
riconosciuto (o accettato) da AFL; altrimenti (se cioè ci sono simboli di S che non
Algoritmi, Macchine, Grammatiche
61
sono ancora stati presi in input oppure se AFL non si trova in uno stato finale), S non
fa parte di L.
Vediamo ad esempio un automa finito AFLD che accetta il linguaggio generato
dalla grammatica LD del par. 3.3. LD genera tutte e sole le stringhe in cui n
occorrenze di a sono seguite da m occorrenze di b (con m, n ≥ 1). AFLD ha tre stati, q0,
q1 e q2, dei quali q2 è l’unico stato finale. Le istruzioni di AFLD sono le seguenti:
1)
2)
3)
4)
q0
q1
q1
q2
a
a
b
b
q1
q1
q2
q2
Supponiamo di voler stabilire se la stringa S = aabbb fa parte del linguaggio
accettato da AFLD. All’inizio del calcolo AFLD si trova nello stato iniziale q0, e prende
in input il primo simbolo della stringa:
a a b b b
↑
q0
In base all’istruzione 1), AFLD passa nello stato q1, e va a leggere il secondo
simbolo di S:
a a b b b
↑
q1
Ora, per l’istruzione 2), AFLD resta in q1, e va a leggere il terzo simbolo:
a a b b b
↑
q1
Ora si applica l’istruzione 3), per cui AFLD passa in q2, poi va a leggere il quarto
simbolo:
a a b b b
↑
q2
A questo punto si applica l’istruzione 4): AFLD resta in q2, e va a leggere il
quinto simbolo. Dopo di che viene applicata ancora una volta l’istruzione 4): lo stato
resta q2, ma, non essendoci più simboli da leggere, l’automa si ferma e il calcolo
termina. Poiché q2 è uno stato finale e la stringa S è stata esaminata interamente, S fa
parte del linguaggio accettato da AFLD.
Algoritmi, Macchine, Grammatiche
62
È facile constatare che stringhe come abba, aaaa e bbbaa non vengono
accettate da AFLD: nel primo caso, quando AFLD si ferma ha raggiunto uno stato
finale, ma la stringa non è stata esaminata interamente; nel secondo caso la stringa
viene esaminata interamente, ma senza che venga raggiunto uno stato finale; nel terzo
caso AFLD si ferma senza aver raggiunto uno stato finale e senza avere esaminato
l’intera stringa.
Un automa a stati finiti può essere rappresentato per mezzo di un diagramma a
stati nella maniera seguente. Gli stati non terminali vengono rappresentati con cerchi
semplici, gli stati terminali con cerchi doppi. Ogni istruzione qi sj qk viene
rappresentata con un arco orientato (ossia una freccia) contrassegnato con il simbolo
sj che va dal nodo qi al nodo qk, come in fig. 3-4.
qi
sj
qk
Figura 3-4
In questa modo l’automa AFLD può essere rappresentato come in fig. 3-5.
a
q0
a
q1
b
b
q2
Figura 3-5
Si verifica facilmente che una stringa s1,…, sk è accettata da un automa finito se
e solo se nel corrispondente diagramma a stati esiste un percorso che parte dallo stato
iniziale e termina in uno finale, in cui gli archi attraversati sono contrassegnati,
nell’ordine, con i simboli s1,…, sk.
Automi a pila
Gli automi a pila, in simboli AP (in inglese pushdown automata) possono
essere visti come automi a stati finiti dotati di una memoria ausiliaria sulla quale
durante il calcolo possono scrivere, leggere o cancellare simboli. Tale memoria è
organizzata come una pila: i simboli sono memorizzati uno sopra l’altro, e in ogni
momento si può leggere o rimuovere solo il simbolo collocato in cima alla pila,
oppure si possono aggiungere sopra gli altri nuovi simboli. Per accedere ai simboli
sottostanti si devono prima rimuovere i simboli collocati sopra. La presenza della pila
rende questi automi più potenti degli automi a stati finiti (per cui possono accettare
linguaggi che gli automi a stati finiti non accettano). Essi però sono meno potenti
delle MT in quanto, rispetto al nastro di queste ultime, la pila è comunque meno
flessibile.
Algoritmi, Macchine, Grammatiche
63
Come gli automi a stati finiti, ogni automa a pila ha un insieme finito Q di stati
interni che deve comprendere uno stato iniziale e almeno uno stato finale. Per
accettare una stringa anche gli automi a pila ne leggono i simboli scorrendola da
sinistra verso destra.
I simboli che si possono memorizzare nella pila appartengono a un alfabeto
diverso rispetto ai simboli dell’input. Indicheremo con AlP = {A1,…, Am} l’insieme
(finito) di simboli che un automa può scrivere nella propria pila.
Nelle istruzioni di un automa a pila la transizione allo stato interno successivo
può dipendere anche dal simbolo che si trova in cima alla pila. Inoltre un’istruzione
può causare anche la cancellazione del simbolo in cima alla pila o la sua sostituzione
con altri simboli. In generale, le istruzioni hanno la forma seguente:
(qi , sj, Ak) → (ql , Ah … At)
dove qi e ql sono stati interni, sj è un simbolo dell’input e Ak, Ah … At sono simboli di
AlP. Il significato di tale istruzione è il seguente: se lo stato interno è qi, il simbolo
preso in input è sj e il simbolo in cima alla pila è Ak, passa nello stato ql, cancella Ak e
aggiungi in cima alla pila, nell’ordine, Ah … At. Poi passa a leggere in input il simbolo
successivo.
È possibile avere istruzioni del tipo:
(qi , sj, Ak) → (ql , –)
e
(qi , sj, –) → (ql , Ah … At)
Nel primo caso, se lo stato è qi, il simbolo in input è sj e il simbolo in cima alla
pila è Ak, si passa nello stato ql e si cancella Ak dalla pila (senza aggiungere nulla).
Nel secondo caso, se lo stato è qi e il simbolo in input è sj, allora, a prescindere da
quale sia il simbolo in cima alla pila, si passa nello stato ql e si aggiungono alla pila
Ah … At.
All’inizio del calcolo la pila deve essere vuota. Una stringa fa parte del
linguaggio accettato da un automa a pila AP se e solo se, quando AP si ferma, sono
vere contemporaneamente queste tre condizioni: a) tutta la stringa di input è stata
letta; b) l’automa è in uno stato finale; c) la pila è vuota.
Vediamo un automa APLC che accetta il linguaggio LC del paragrafo 3, formato
da tutte le stringhe con n occorrenze di a seguite da n occorrenze di b (con n ≥ 1).
APLC ha due stati interni q0 e q1, di cui q1 è l’unico stato finale. A è l’unico simbolo
che APLC può scrivere nella sua pila. Le istruzioni di APLC sono le seguenti:
1) (q0, a, –) → (q0, A)
2) (q0, b, A) → (q1, –)
3) (q1, b, A) → (q1, –)
La fig. 3-6 mostra il calcolo svolto da APLC a partire dalla stringa di input aabb.
Algoritmi, Macchine, Grammatiche
64
a a b b
↑
q0
pila
a a b b
↑
q0 A
a a b b
↑
q0 A
A
aa b b
↑
q1 A
a a b b
↑
q1
Figura 3-6
Per ciascuna fase del calcolo a fianco dello stato interno abbiamo rappresentato
la situazione della pila. All’inizio APLC prende in input il primo simbolo a, lo stato è
q0 e la pila è vuota. Viene applicata l’istruzione 1): lo stato rimane q0 e viene aggiunto
A in cima alla pila. Dopo di che APLC prende in input il secondo simbolo a e viene
applicata di nuovo l’istruzione 1). Poi APLC legge il terzo simbolo, che è b e va perciò
applicata l’istruzione 2): si ha un cambiamento di stato e viene cancellato il simbolo
in cima alla pila. Al passo successivo si applica l’istruzione 3), che mantiene l’automa
nello stato q1 e comporta la cancellazione dell’ultimo simbolo dalla pila. A questo
punto l’automa si ferma: la pila è vuota, q1 è uno stato finale e tutti i simboli della
stringa sono stati esaminati. Pertanto la stringa aabb fa parte del linguaggio accettato
da APLC.
L’esempio chiarisce come funziona APLC: l’automa usa la pila per contare le
occorrenze di a nella prima parte della stringa. Quando incontra la prima b, ALC passa
dallo stato q0 allo stato q1, e comincia a togliere dalla pila una A per ogni b che trova.
Pertanto, quando APLC si ferma, affinché tutta la stringa sia stata letta e la pila sia
vuota, il numero delle a deve essere lo stesso del numero delle b.
L’uso della pila è essenziale per accettare il linguaggio LC, che infatti non può
essere accettato da alcun automa finito.
3.6 Conclusioni
La teoria delle grammatiche formali e la proposta, dovuta essenzialmente a Noam
Chomsky, di impiegarle per descrivere la struttura sintattica delle lingue naturali è alla
base della linguistica teorica contemporanea. La proposta chomskyana di utilizzare le
grammatiche formali per spiegare psicologicamente la capacità degli esseri umani di
produrre e comprendere il linguaggio è stata determinante anche per lo sviluppo delle
scienze cognitive e della concezione dei processi mentali come processi
computazionali.
Le grammatiche formali hanno inoltre una notevole rilevanza applicativa in
informatica, in quanto vengono utilizzate per descrivere la sintassi dei linguaggi di
programmazione. Il processo di traduzione automatica dei programmi da un linguaggio
di programmazione di alto livello al codice assembler e al linguaggio macchina
presuppone una fase di analisi sintattica automatica (parsing) resa possibile dal fatto
che la sintassi dei linguaggi di alto livello è espressa mediante una grammatica formale.
Affinché tale analisi possa stabilire se un programma è sintatticamente corretto o meno,
i linguaggi di programmazione devono essere decidibili (non possono cioè essere
linguaggi di tipo 0). Di solito i linguaggi di programmazione sono di tipo 1, ossia
linguaggi sensibili al contesto. Tuttavia si preferisce caratterizzarne la sintassi per
mezzo di grammatiche libere dal contesto (di tipo 2) cui vengono affiancate delle
Algoritmi, Macchine, Grammatiche
65
restrizioni di tipo contestuale13. Questo consente di rendere più efficiente il processo di
parsing.
Nell’ambito della linguistica, stabilire a quale classe appartengano le lingue
naturali è una questione empirica ben più complessa, e a tutt’oggi ancora aperta. Vi
sono tuttavia buone ragioni per ritenere che, in generale, le lingue naturali non siano
libere dal contesto, cioè che non sia sufficiente una grammatica di tipo 2 per generarle,
e richiedano quindi una grammatica di tipo 1, sensibile al contesto14.
13
La cosiddetta forma BNF (Bacchus Normal Form, o Bacchus-Naur Form) che viene usata
abitualmente per esprimere la sintassi dei linguaggi di programmazione altro non è che una notazione
equivalente per le grammatiche libere dal contesto.
14
Su grammatiche formali e linguistica si vedano i capp. 16-22 di Partee et al. [1990] e in particolare, per
questo tipo di problemi, i paragrafi 17.3.2 e 18.6.
Algoritmi, Macchine, Grammatiche
66
ESERCIZI RELATIVI ALLA TERZA PARTE
Esercizio 3.1. Verificare che le seguenti stringhe appartengono al linguaggio generato
da G: a) un gatto dorme; b) un topo mangia il gatto.
Esercizio 3.2. Come formulata nel paragrafo 2, la grammatica G non genera frasi quali
un topo mangia o il gatto rincorre, dove un verbo transitivo non è seguito da un
complemento oggetto. Modificare G in modo che generi anche questo tipo di frasi.
Esercizio 3.3. Verificare che le seguenti stringhe appartengono al linguaggio generato
da G’: a) il topo canta ma il gatto dorme; b) un gatto rincorre il topo e il topo dorme; c)
un gatto rincorre un topo oppure il topo mangia il gatto; d) un topo dorme ma il gatto
canta e il topo mangia il gatto.
Esercizio 3.4. Verificare che le seguenti stringhe appartengono al linguaggio generato
da G”: a) il topo che mangia il gatto rincorre un topo; b) un topo rincorre il gatto che
canta; c) il gatto che rincorre il topo mangia un topo che canta; d) un gatto mangia il
topo che rincorre il gatto che dorme.
Esercizio 3.5. Sviluppare gli alberi sintattici che corrispondono alle derivazioni degli
esercizi 3.1 e 3.3-4.
Esercizio 3.6. Completare gli alberi sintattici corrispondenti alle due possibili analisi
nella grammatica G’ della stringa un gatto corre e un topo canta oppure un gatto canta.
Esercizio 3.7. Sviluppare due derivazioni in G’ che corrispondano alle due possibili
analisi sintattiche della stringa un gatto corre e un topo canta oppure un gatto canta.
Esercizio 3.8. Determinare una grammatica che generi tutte e sole le espressioni
algebriche ben formate che si possono scrivere con il linguaggio Al.
Esercizio 3.9. Individuare una derivazione della stringa aaaabbbbcccc nella
grammatica GnD.
Esercizio 3.10. Determinare le produzioni di una grammatica lineare sinistra LS che
generi lo stesso linguaggio di LD.
Esercizio 3.11. Determinare le produzioni di una grammatica lineare destra che generi
lo stesso linguaggio di G.
Esercizio 3.12. Individuare una derivazione della stringa un topo rincorre il gatto nella
grammatica dell’esercizio precedente.
Esercizio 3.13. Sia AF un automa finito con Q = {q0, q1} e q1 come unico stato finale,
caratterizzato dalle istruzioni:
q0
q0
a
b
Algoritmi, Macchine, Grammatiche
q0
q1
q1
q1
b
a
q0
q1
67
Rappresentare AF mediante un diagramma a stati e stabilire qual è il linguaggio
accettato da AF.
Esercizio 3.14. Specificare gli stati e scrivere le istruzioni di un automa a stati finiti
AFG che accetti il linguaggio generato dalla grammatica G del par. 3.2. Rappresentare
AFG mediante un diagramma a stati.
Esercizio 3.15. Specificare gli stati, scrivere le istruzioni e disegnare il diagramma a
stati di un AF che accetti tutte le stringhe sul linguaggio
{a, b} con esattamente due occorrenze di a.
Esercizio 3.16. Specificare gli stati, scrivere le istruzioni e disegnare il diagramma a
stati di un AF che accetti tutte le stringhe sul linguaggio
{a, b} con almeno due occorrenze di a.
Esercizio 3.17. Specificare gli stati, scrivere le istruzioni e disegnare il diagramma a
stati di un AF che accetti tutte le stringhe sul linguaggio
{a, b} in cui compare almeno una sequenza di tre a consecutive.
Esercizio 3.18. Verificare che le stringhe 1) bbaa, 2) aaabb, 3) aabbb e 4) aabba non
fanno parte del linguaggio accettato dall’automa a pila AFLC.
Esercizio 3.19. Stabilire qual è il linguaggio accettato da un AP con due stati interni q0
e q1, di cui q1 è l’unico stato finale, caratterizzato dalle seguenti istruzioni:
(q0, a, –) → (q0, A A)
(q0, b, A) → (q1, –)
(q1, b, A) → (q1, –)
Esercizio 3.20. Determinare gli stati e le istruzioni di un automa a pila che accetti il
linguaggio formato da tutte le stringhe in cui n occorrenze di a sono seguite da m
occorrenze di b, le quali sono seguite a loro volta da altre n occorrenza di a (con m, n ≥
1). Ad esempio devono appartenere al linguaggio accettato le stringhe aabbbbaa,
aaaabbaaaa, eccetera.
Esercizio 3.21. Stabilire qual è il linguaggio accettato da un AP con quattro stati interni
q0, q1, q2 e q3, di cui q3 è l’unico stato finale, caratterizzato dalle seguenti istruzioni:
(q0, a, –)
(q0, b, A)
(q1, b, A)
(q1, a, –)
(q2, a, –)
(q2, b, A)
(q3, b, A)
Algoritmi, Macchine, Grammatiche
→
→
→
→
→
→
→
(q0, A)
(q1, –)
(q1, –)
(q2, A)
(q2, A)
(q3, –)
(q3, –)
68
Algoritmi, Macchine, Grammatiche
69
SOLUZIONI DEGLI ESERCIZI RELATIVI
ALLA TERZA PARTE
Esercizio 3.1. Riportiamo a titolo di esempio una derivazione di a) in G:
1) E
2) SN SV
3) Articolo Nome SV
4) un Nome SV
5) un gatto SV
6) un gatto VerbIntr
7) un gatto dorme
Esercizio 3.2. Sostituire la regola SV → VerbTrans SN | VerbIntr di G con la regola SV
→ VerbTrans SN | VerbTrans |VerbIntr.
Esercizio 3.3. Riportiamo a titolo di esempio una derivazione di a) in G’:
1) E
2) E Connettivo E
3) SN SV Connettivo E
4) Articolo Nome SV Connettivo E
5) il Nome SV Connettivo E
6) il topo SV Connettivo E
7) il topo VerbIntr Connettivo E
8) il topo canta Connettivo E
9) il topo canta ma E
10) il topo canta ma SN SV
11) il topo canta ma Articolo Nome SV
12) il topo canta ma il Nome SV
13) il topo canta ma il gatto SV
14) il topo canta ma il gatto VerbIntr
15) il topo canta ma il gatto dorme
Esercizio 3.4. Riportiamo a titolo di esempio una derivazione di a) in G”:
1) E
2) SN SV
3) SN che SV SV
4) Articolo Nome che SV SV
5) il Nome che SV SV
6) il topo che SV SV
7) il topo che VerbTrans SN SV
8) il topo che mangia SN SV
9) il topo che mangia Articolo Nome SV
10) il topo che mangia il Nome SV
11) il topo che mangia il gatto SV
12) il topo che mangia il gatto VerbTrans SN
13) il topo che mangia il gatto rincorre SN
14) il topo che mangia il gatto rincorre Articolo
Nome
15) il topo che mangia il gatto rincorre un
Nome
16) il topo che mangia il gatto rincorre un topo
Esercizio 3.5. Riportiamo a titolo di esempio l’albero che corrisponde alla derivazione
della stringa a) dell’esercizio 3.4:
Algoritmi, Macchine, Grammatiche
70
E
SN
Il
SN
SV
SN
Articolo
SV
topo
VerbTrans
SN
Nome
che
VerbTrans
Articolo
mangia
il
Articolo
Nome
gatto
rincorre
un
Nome
topo
Esercizio 3.7. (Traccia). In entrambi i casi la derivazione inizia con questi passi: 1) E;
2) E Connettivo E. Dopo di che, nel caso dell’analisi corrispondente all’albero (a) di fig.
9-3 si applica la produzione E → SN SV all’occorrenza di sinistra di E in 2), e la
produzione E → E Connettivo E all’occorrenza di destra; nel caso dell’albero (b) si fa il
viceversa.
Esercizio 3.8.
S → Exp Op Exp
Exp → (Exp Op Exp) | Lettera
Op → + | − | * | /
Lettera → a | b | c | d | e
Esercizio 3.9.
1) S
2) aSBc
3) aaSBcBc
4) aaaSBcBcBc
5) aaaabcBcBcBc
6) aaaabBccBcBc
7) aaaabBcBccBc
8) aaaabBBcccBc
9) aaaabBBccBcc
10) aaaabBBcBccc
11) aaaabBBBcccc
12) aaaabbBBcccc
13) aaaabbbBcccc
14) aaaabbbbcccc
Esercizio 3.10.
S → Sb | A
A → Aa | a
Esercizio 3.11.
S → il S1 | un S1
S1 → topo S2 | gatto S2
S2 → dorme | canta | rincorre S3 | mangia S3
S3 → il S4 | un S4
S4 → topo | gatto
Esercizio 3.12.
Algoritmi, Macchine, Grammatiche
71
1) S
2) un S1
3) un topo S2
4) un topo rincorre S3
5) un topo rincorre il S4
6) un topo rincorre il gatto
Esercizio 3.13. L’automa AF accetta le stringhe sull’alfabeto {a, b} con un numero dispari di
occorrenze di b. Ciò si constata facilmente se si esamina il diagramma a stati di AF:
a
q0
a
b
q1
b
Esercizio 3.14. Il diagramma a stati di un automa AFG è il seguente:
q3
canta
dorme
il
q0
rincorre
cane
q1
un
q2
gatto
il
q4
mangia
cane
q5
un
q6
gatto
Esercizio 3.15. Il diagramma a stati di un AF con le caratteristiche richieste dall’esercizio è il
seguente:
b
q0
b
a
q1
b
a
q2
Esercizio 3.16. Il diagramma a stati di un AF con le caratteristiche richieste dall’esercizio è il
seguente:
b
q0
b
a
q1
b
a
q2
a
Algoritmi, Macchine, Grammatiche
72
Esercizio 3.17. Il diagramma a stati di un AF con le caratteristiche richieste dall’esercizio è il
seguente:
b
a
q0
b
b
b
q1
a
q2
a
q3
a
Esercizio 3.18. 1) L’automa si ferma subito perché non c’è nessuna istruzione che preveda il caso
che lo stato sia q0 e il simbolo in input b; 2) quando l’automa si ferma la pila non è vuota; 3)
l’automa si blocca sull’ultima b perché non ci sono più A nella pila e non può essere applicata la
terza istruzione; 4) l’automa si blocca sull’ultima a perché non c’è nessuna istruzione che preveda
che lo stato sia q1 e il simbolo in input a.
Esercizio 3.19. L’automa accetta il linguaggio di tutte le stringhe formate da n occorrenze di a
seguite da m occorrenze di b, dove m = 2n e n ≥ 1. Ad esempio appartengono al linguaggio
accettato le stringhe aabbbb, aaabbbbbb, eccetera.
Esercizio 3.20. Un possibile automa per il compito richiesto ha tre stati interni q0, q1 e q2, di cui q2
è l’unico stato finale, ed è caratterizzato dalle seguenti istruzioni:
(q0, a, –)
(q0, b, A)
(q1, b, A)
(q1, a, A)
(q2, a, A)
→
→
→
→
→
(q0, A)
(q1, A)
(q1, A)
(q2, –)
(q2, –)
Esercizio 3.21. L’automa accetta il linguaggio di tutte le stringhe formate nell’ordine da n
occorrenze di a, n occorrenze di b, m occorrenza di a e m occorrenza di b (con m, n ≥ 1). Ad
esempio appartengono al linguaggio accettato le stringhe aabbab, aabbaaabbb, eccetera.
Algoritmi, Macchine, Grammatiche
73
Ulteriori letture
Per chi volesse affrontare gli aspetti tecnici della teoria della computabilità, alcune trattazioni
approfondite sono (Kleene 1952 [parte III], Hermes 1961; Rogers 1967; Minsky 1967; Lewis e
Papadimitriou 1981; Davis e Weyuker 1983; Odifreddi 1989). (Frixione e Palladino 2004) è
un’introduzione alla teoria che non presuppone conoscenze logico-matematiche. (Davis 1965) è una
raccolta di articoli storici sulla teoria della computabilità. Una raccolta di articoli classici di logica e
filosofia della matematica, con molti punti di contatto con i temi qui trattati è (van Heijenhoort
1967). Sui problemi di filosofia della matematica che hanno portato alla formulazione della teoria
della computabilità si veda (Borga e Palladino 1997). (Turing 1994) è una raccolta di scritti di Alan
Turing. Una biografia di Turing è stata scritta da Andrew Hodges (1983). L'articolo dove viene
proposto il test di Turing è compreso in (Somenzi e Cordeschi 1994). Partee et al. (1990) è
un'introduzione agli strumenti matematici per la linguistica, che include un'ampia trattazione delle
grammatiche formali. Infine, un libro che tratta in modo affascinante e molto particolare i temi della
computabilità e dei teoremi di limitazione della logica in relazione alla teoria della mente è
(Hofstadter 1979).
Bibliografia
Borga, M. e Palladino, D. (1997). Oltre il mito della crisi. Fondamenti e filosofia della matematica
nel ventesimo secolo. La Scuola, Brescia.
Casalegno, P. (1997). Filosofia del linguaggio. Un’introduzione, La Nuova Italia Scientifica (poi
Carocci), Roma.
Chierchia, G. e McConnell-Ginet, S. (1990). Meaning and Grammar: An Introduction to Semantics,
MIT Press, Cambridge, Mass.; tr. it.: Significato e grammatica: semantica del linguaggio
naturale, F. Muzzio, Padova, 1993.
Chomsky, N. (1959). On certain formal properties of grammars, Information and Control 2, pp.
137-167; tr. it. in De Palma (1974).
Church, A. (1936). An unsolvable problem of elementary number theory. American Journal of
Mathematics, 58:345-363.
Curry, H.B. (1929). An analysis of logical substitution. American Journal of Mathematics, 51:
363-384.
Curry, H.B. (1930). Grundlagen der Kombinatorischen Logik. American Journal of Mathematics,
52:509-536, 789-834.
Curry, H.B. (1932). Some additions to the theory of combinators. American Journal of
Mathematics, 54:551-558.
Davis, M. (a cura di) (1965). The Undecidable. Raven Press, Hewlett, New york.
Davis, M. e Weyuker, E. (1983). Computability, Complexity, and Languages: Fundamentals of
Theoretical Computer Science. Academic Press, New York.
De Palma, A. (a cura di) (1974). Linguaggio e sistemi formali, Einaudi, Torino.
Frixione, M. e Palladino, D. (2004). Funzioni, macchine, algoritmi. Introduzione alla teoria della
computabilità. Carocci, Roma.
Gandy, R. (1980). Church's thesis and principles for mechanisms. In The Kleene Symposium,
123-148, North Holland, Amsterdam.
Algoritmi, Macchine, Grammatiche
74
van Heijenhoort, J. (a cura di) (1967). From Frege to Gödel. Harvard University Press, Harvard.
Hermes, H. (1961). Aufzählbarkeit, Entscheidbarkeit, Berechenbarkeit.
Springer, Berlin,
Heidelberg. Tr. it. Numerabilità, decidibilità, computabilità, Boringhieri, Torino, 1973.
Hodges, A. (1983). Alan Turing: The Enigma. New York, Simon and Shuster. Tr. it. Storia di un
enigma. Vita di Alan Turing (1912-1954). Bollati, Torino, 1991.
Hofstadter, D. (1979). Gödel, Escher, Bach: An Eternal Golden Braid. Basic Books. Tr. it. Gödel,
Escher, Bach: un'eterna ghirlanda brillante, Adelphi, Milano, 1984.
Kleene, S.C. (1952). Introduction to metamathematics. North Holland, Amsterdam.
Lewis, H.R. e Papadimitriou, C.H. (1981). Elements of the Theory of Computation. Prentice-Hall,
Englewood Cliffs, NJ.
Markov, A.A. (1951). Theory of algorithms. American Mathematical Society Translations, seconda
serie, 15(1960):1-14 (trad. ingl. dell'originale russo).
Markov, A.A. (1954). Theory of algorithms. National Science Foundation and Israel Program for
Scientific Translation (1961) (trad. ingl. dell'originale russo).
Minsky, M. (1967). Computation. Finite and Infinite Machines. Prentice-Hall, Englewood Cliffs.
Odifreddi, P. (1989). Classical Recursion Theory : The Theory of Functions and Sets of Natural
Numbers. Elsevier, Amsterdam.
Partee, B.H., Meulen, A. e Wall, R. (1990). Mathematical Methods in Linguistics, Kluwer,
Dordrecht.
Post, E. (1936). Finite combinatory processes - formulation 1. Journal of Symbolic Logic,
1:103-105.
Post, E. (1943). Formal reductions of the general combinatorial decision problem. American
Journal of Mathematics, 65:197-215.
Post, E. (1946). A variant of a recursively unsolvable problem. Bullettin of the American
Mathematical Society, 52:284-316.
Rogers H. (1967). Theory of Recursive Functions and Effective Computability. McGraw-Hill, New
York. Tr. it. Teoria delle funzioni ricorsive e della computabilità effettiva, Tecniche Nuove,
Milano, 1992.
Schönfinkel, M. (1924). Über die Bausteine der mathematischen Logik. Mathematische Annalen,
92:305-316.
Somenzi, V. e Cordeschi, R. (a cura di) (1994). La filosofia degli automi. Origini dell'intelligenza
artificiale. Bollati, Torino.
Turing, A. (1936-7). On computable number, with an application to the Entscheidungsproblem.
Proceedings of the London Mathematical Society, serie 2, 42:230-365; A correction, ibid.,
43:544-546 (ristampato in Davis 1965).
Turing, A. (1937). Computability and λ-definability. Journal of Symbolic Logic, 2:153-163.
Turing, A. (1950). Computing machinery and intelligence. Mind, 59:433-460.
Turing, A. (1994). Intelligenza meccanica. A cura di G. Lolli, Bollati, Torino.
Algoritmi, Macchine, Grammatiche
75