Programmazione e algoritmi 1. Algoritmi e programmi Il numero apparentemente illimitato di compiti che un elaboratore elettronico è in grado di svolgere dipende da un unica capacità: quella di eseguire un programma, che consiste in una codifica opportuna di un algoritmo. Algoritmi e programmi sono dunque alla base di ogni attività che un computer è in grado di svolgere ed il loro studio è propedeutico per ogni altro ambito dell informatica. 1.1 Il concetto di algoritmo Quando abbiamo imparato ad eseguire operazioni in colonna, a calcolare perimetro ed area di certe figure geometriche, ma anche più semplicemente a trascrivere un testo, colorare un disegno stando nei contorni, oppure cercare un nome o un vocabolo in un elenco ordinato alfabeticamente, cioè in generale a svolgere qualunque compito in modo prefissato applicando un metodo, abbiamo appreso un algoritmo. Più precisamente un algoritmo è un metodo di calcolo per risolvere un problema computazionale. Quest ultimo descrive cosa sia un istanza del problema, ossia ciò che è lecito attendersi in ingresso, ed una condizione di uscita, ossia un criterio per riconoscere quando una risposta, o uscita, sia corretta. Più astrattamente un problema computazionale può essere identificato con una relazione ingresso/uscita. Un semplice esempio è la ricerca del massimo (condizione d uscita) in una collezione finita non vuota di valori numerici positivi n, K 1 (istanza). È chiaro che occorre confrontare tra loro tutti i valori in ingresso, ma senza un metodo rischiamo di ripetere gli stessi confronti o peggio di trascurarne qualcuno. Una soluzione del problema consiste nel percorrere la sequenza dei valori dal primo all ultimo ricordando ogni volta il massimo dei valori ispezionati, e confrontandolo con il valore stiamo ispezionando. Questo stesso metodo può essere descritto come una sequenza di istruzioni numerate da eseguirsi in progressione dalla numero 1 in poi, salvo diversa indicazione contenuta nelle istruzioni, fermandosi quando non vi siano ulteriori istruzioni da eseguire: 1. leggi in ingresso i valori n, K 1,nk 2. siano i il numero 2 e max il valore di n1 3. se i > k vai all istruzione 6 4. se max < ni allora ridefinisci max con il valore di n i 5. incrementa i di 1 e vai all istruzione 3 6. comunica max come valore in uscita Le relazione ingresso/uscita di questo algoritmo è univoca: non ci sono due risposte corrette per lo stesso ingresso. Ciò non vale in generale, come si vede se ridefiniamo il problema chiedendo di conoscere anziché il massimo tra n, K 1, un indice del massimo, che possiamo ottenere semplicemente modificando l istruzione numero 4 come segue: 4. se n max < n i allora ridefinisci max con il valore di i
Non avendo infatti richiesto che i valori in ingresso siano tra loro distinti, vi possono essere diverse risposte corrette che l algoritmo potrebbe fornire. Ma un algoritmo è per sua natura deterministico, il che vuol dire che se ripetuto sullo stesso ingresso, non può che fornire la medesima uscita. Per come è definito l algoritmo sopra riportato sceglierà sempre il minimo tra gli indici del valore massimo. 1.2 La programmazione. Per comunicare e studiare gli algoritmi ci occorre un insieme di convenzioni per descriverli, ovvero un linguaggio. L uso del linguaggio naturale espone infatti a fraintendimenti ed ambiguità inammissibili nel nostro contesto. Questo passaggio è indispensabile anche dal punto di vista pratico. Un calcolatore è in grado di interpretare ed eseguire solo istruzioni estremamente elementari (come: riconoscere se un bit è 0 o 1; copiare un byte, ovvero 8 bit consecutivi, da una parte all altra, ecc.) che costituiscono il codice macchina. Non è umanamente possibile produrre direttamente programmi di questo tipo salvo il caso di algoritmi molto semplici che operino su dati elementari. Si ricorre allora ad opportuni linguaggi artificiali, detti linguaggi di programmazione (come il BASIC, il PASCAL, il C o Java), i cui programmi sono più simili ad un testo matematico che ad una sequenza di istruzioni in codice macchina, ma sufficientemente precisi da essere interpretabili da un computer, vuoi perché automaticamente traducibili in codice macchina mediante un compilatore, vuoi perché riconoscibili, un istruzione dopo l altra, da un programma interprete. Per sfuggire alla babele dei linguaggi esistenti, il cui numero è in continuo aumento, e per ridurre il peso dei dettagli necessari nelle realizzazioni concrete, useremo una sorta di linguaggio ideale, molto semplificato e tuttavia sufficiente per esprimere le idee fondamentali della programmazione. Volendo introdurre la sintassi (la grammatica cui attenersi per formare le espressioni) e la semantica (il significato di ciascuna espressione) di questo linguaggio, riprendiamo in considerazione l algoritmo per il calcolo del massimo in una sequenza finita di valori. I suoi ingredienti essenziali sono: le variabili, come i e max, ma anche quelle che rappresentano i valori in ingresso n, K 1. Si parla di variabili perché il valore associato a queste espressioni può cambiare ad ogni esecuzione (come nel caso di n, K 1 ) ovvero nel corso della singola esecuzione (come avviene per i e max). Le variabili possono occorrere in espressioni più complicate, il cui significato, posto che le variabili in esse contenute sia un valore, è ancora un valore. L operazione più importante che si compie con le variabili è l assegnazione di un valore, ciò che accade quando diciamo: sia il valore v dell espressione e il nuovo valore della variabile x ; ciò per cui potremmo scrivere x e Vi sono poi istruzioni la cui esecuzione è condizionata all esito di un controllo: se allora, e che potremmo trovare in una forma più elaborata, per cui si assume che il controllo abbia due soli possibili esiti, si oppure no, e cioè: se allora altrimenti. Si parla in questo caso di selezione a due vie tra diverse istruzioni. Infine vi sono istruzioni che riguardano il flusso, ossia la successione delle istruzioni, dette istruzioni di salto: vai all istruzione. Queste ultime interferiscono con l ordine in cui le istruzioni sono elencate, dando luogo a strutture elaborate che i programmatori hanno cercato di visualizzare attraverso rappresentazioni grafiche dette diagrammi di flusso. Quello che segue è il diagramma di flusso del programma per il calcolo del massimo tra n, K 1 :
inizio leggi n 1,, n k i 2, max n 1 i > k no max < n i no i i + 1 si si comunica max max n i fine Nel diagramma i punti di inizio e fine sono rappresentati con ovali ed hanno rispettivamente un punto di uscita ed uno di ingresso; le istruzioni di assegnazione sono rappresentate con rettangoli, con un ingresso ed un uscita; le istruzioni di controllo sono rappresentate da rombi con un ingresso e due uscite, a seconda dell esito del test. Le frecce indicano una sorta di percorso, che rappresenta il flusso dell esecuzione, il quale inizia e termina in un ovale. Questa rappresentazione, facilmente traducibile in un linguaggio come il BASIC, ha il vantaggio di visualizzare la sequenza delle istruzioni e le reciproche dipendenze, ma induce facilmente ad una programmazione caotica, con salti in punti arbitrari, il cui effetto è l oscuramento del significato del programma nel suo insieme. La programmazione strutturata si basa sull idea di restringere i diagrammi a quelli che si possono ottenere per composizione di parti rispondenti ad uno dei tre schemi fondamentali, quali la sequenza, la selezione e l iterazione. La sequenza consiste nella semplice concatenazione lineare di azioni: La selezione è composta da un test e da una azione condizionata, oppure da due azioni in alternativa:
Infine l iterazione è costruita con un test di controllo ed un azione (il corpo) che viene ripetutamente eseguita fintanto che il test non divenga falso. (Sia la selezione che l iterazione hanno varianti comunque riconducibili a quelle sopra presentate). Questi schemi si possono comporre nel senso che le azioni rappresentate da rettangoli possono essere a loro volta istanze di uno qualsiasi dei tre schemi. Essi rappresentano cioè dei blocchi. In corrispondenza con gli schemi di sequenza, selezione ed iterazione, possiamo introdurre costrutti linguistici che consentano di rappresentare un diagramma con un testo (il codice sorgente del programma relativo): per la sequenza si usa la semplice scrittura in colonna, con la stessa tabulazione per indicare il blocco di appartenenza (i linguaggi che non sono sensibili all a-capo ed ai tabulatori usano ; ed opportuni marcatori di inizio e fine blocco); per la selezione si usa if then if then oppure else Infine per l iterazione scriveremo: while do Per porre in chiaro l uso di questi costrutti riproduciamo qui di seguito la pseudo-codifica dell algoritmo per il calcolo del massimo visto sopra (immaginando delle istruzioni per la lettura dell ingresso e per la scrittura dell uscita):
read n,,n 1 K k i 2 max n 1 while not i > k do if max < n i then max n i i i + 1 write max Come accade ogni volta che ci imponiamo delle limitazioni, è opportuno chiedersi se queste non comportino delle perdite, in questo caso nella capacità di esprimere algoritmi che potremmo invece rappresentare con diagrammi non strutturati: la risposta è nel teorema di Böhm-Jacopini, il quale assicura che tutto ciò che possiamo rappresentare con diagrammi non strutturati è esprimibile (sebbene in modo diverso) con diagrammi strutturati. Se combiniamo questo risultato con quello secondo cui i diagrammi di flusso sono un formalismo Turing completo, ossia capace di rappresentare in linea di principio qualunque algoritmo, ne concludiamo che in un linguaggio di programmazione l uso di istruzioni di salto come il goto del PASCAL o del C non solo non è auspicabile se si vuole mantenere la perspicuità logica del codice, ma non è neppure necessario.