(Algoritmi, dati e programmi. Scopo della lezione è l’introduzione ai concetti di: problema, dato, algoritmo, linguaggio di programmazione, programma, diagramma di flusso. Cerchiamo prima di tutto di collocare l’argomento nel mondo dell’informatica. Da una parte, l’informatica si occupa di informazione, argomento nel quale rientra la rappresentazione dell’informazione; possiamo considerare questo ramo come il più teorico dell’informatica. Dall’altra parte, dal punto di vista più pratico, ci sono i mezzi utilizzati dall’informatica: i mezzi fisici (quindi i calcolatori ed in particolare i circuiti che compongono i calcolatori – già introdotti nella lezione sull’HW) ed il software. Di seguito saranno approfonditi gli aspetti di sviluppo del software. Definiamo gli elementi principali di un problema di elaborazione dell’informazione; vi saranno: - dei dati di partenza; un risultato cercato. Una soluzione al problema è una procedura che elabora i dati di partenza in modo da ottenere il risultato cercato. Un banale esempio di problema di elaborazione dell’informazione è il problema di determinare la somma di due addendi a e b. I dati di partenza sono i dati a e b, il risultato cercato è il numero c = a + b. Una possibile soluzione al problema è descritta dalla procedura per la somma di due numeri (scrivere i numeri incolonnati correttamente, sommare le unità con riporto, sommare le decine… ). Quando si affronta un problema dobbiamo distinguere tra: - la conoscenza di come si risolve un problema (che implica l’analisi del problema, l’identificazione di una soluzione e la descrizione della soluzione stessa); l’effettiva capacità di risolvere un problema (cioè l’interpretazione della soluzione e la sua attuazione). Torniamo al problema della somma per esemplificare i diversi passi. Problema di elaborazione dell’informazione: “Quanto fa 21+32?” Conoscenza di come si risolve il problema: Analisi: è un problema di somma tra due numeri interi; Identificazione di una soluzione: esiste una procedura per la somma tra due numeri interi; Descrizione della soluzione: i due numeri interi vanno incolonnati, si sommano le unità, il riporto viene sommato alla somma delle decine, il riporto viene sommato alla somma delle centinaia, ... Effettiva capacità di risolvere il problema: Intepretazione della soluzione: un utente italiano legge la descrizione del problema e comprende le operazioni da eseguire; Attuazione della soluzione: 21 + 32 = ----53 Dunque, la conoscenza di come si risolve un problema permette di sviluppare un programma. Ad esempio, possono essere sono necessarie competenze in ambito matematico per capire come si risolve un’equazione differenziale. Il matematico può quindi fornire una procedura per la risoluzione di un’equazione differenziale, quindi fornisce una rappresentazione della soluzione al problema (ad esempio specificando una serie di istruzioni programma). Il programma viene poi interpretato da un utente (es. esecutore umano, calcolatore) che eseguirà le istruzioni specificate. Le competenze necessarie per i diversi passi sono dunque diverse: - - servono competenze in ambito matematico per trovare la soluzione del problema; servono competenze necessarie alla scrittura di un programma per specificare la soluzione in un linguaggio comprensibile (capacità di descrivere una serie di operazioni utilizzando un opportuno linguaggio); servono le competenze duali (capacità di comprendere una serie di operazioni in un opportuno linguaggio) per interpretare la soluzione, e… … Servono infine i mezzi per l’attuazione della soluzione. La conoscenza di come si risolve un problema è ciò che ci permette di sviluppare un programma. La scoperta / invenzione di un algoritmo per la risoluzione di un problema richiede uno sforzo creativo. Al contrario, la risoluzione di un problema richiede solo la comprensione di uno specifico linguaggio e l’attuazione di una serie di operazioni: è dunque un’operazione puramente meccanica. In particolare, non è necessaria la comprensione del problema per la sua risoluzione! La macchina (o comunque l’esecutore) può calcolare la soluzione al problema senza avere la più pallida idea del problema in sé. Per riassumere i diversi ambiti in cui ci si muove: il problema viene definito ed analizzato nell’ambito scientifico proprio del problema. Una volta individuato l’algoritmo adatto al problema, interviene il programmatore che crea un’opportuna codifica dell’algoritmo. Infine, il calcolatore interpreta il codice scritto dal programmatore ed elabora dai dati per arrivare alla soluzione del problema. Le competenze per l’analisi del problema e l’identificazione della soluzione provengono dunque da diversi ambiti disciplinari e non ha senso analizzarle in questa sede. Il calcolatore è lo strumento che fornisce l’effettiva capacità di risoluzione del problema. Quello che ci interessa è il ponte tra questi due mondi, ovvero la descrizione della soluzione , cioè la formalizzazione dell’algoritmo che risolve il problema in un opportuno linguaggio di programmazione. Per la descrizione della soluzione si potrebbe utilizzare il linguaggio naturale (italiano, russo, giapponese, …). Il problema è che il linguaggio naturale è spesso ambiguo. Ad esempio, consideriamo una ricetta (abbiamo già accennato al fatto che una ricetta è in effetti un algoritmo); se nella ricetta compare la frase “aggiungere un pizzico di sale”… Quanto sale dobbiamo aggiungere? E’ meglio dunque ricorrere, per la descrizione della soluzione, ad un linguaggio formale, il quale è privo di ambiguità. Sono esempi di linguaggio formale il formalismo matematico, la rappresentazione di un algoritmo in pseudo-codice, i diagrammi di flusso, i linguaggi di programmazione, il codice macchina, … E’ importante notare che la descrizione della soluzione può essere fatta a diversi livelli di dettaglio. Ad esempio, nella ricetta per preparare le lasagne, potremmo trovare una sequenza di 4 istruzioni: 1) preparare il ragù, 2) preparare la besciamelle, 3) bollire le lasagne, 4) mettere a strati nella teglia e cuocere in forno. L’istruzione 2) potrebbe però essere di difficile interpretazione per molti esecutori (come si prepara la besciamelle?). Dunque potremmo trovare delle istruzioni a livello di dettaglio minore all’interno della ricetta, ad esempio, 2.1) setacciare 50g di farina e stemperarla con 10cc di latte, 2.2) …, 2.3) aggiungere 30cc di latte e 50g di burro a pezzetti, … Queste istruzioni saranno di “alto livello” (vicino al modo di comprendere di chi ha scritto le istruzioni) o di “basso livello” (vicino al modo di comprendere dell’esecutore). Per analogia avremo anche a che fare con linguaggi di alto livello (quelli in cui la sintassi è simile al modo di esprimersi naturalmente – la singola istruzione di un linguaggio ad alto livello è significativa per l’utente umano, ma richiede un grande numero di operazioni elementari per la macchina che non è dunque in grado di interpretare l’istruzione in modo diretto) e di basso livello (quelli in cui la sintassi descrive in maniera dettagliata le operazioni fin nei più piccoli dettagli, es. svuotamento di un registro nella CPU – difficilmente comprensibile per un operatore umano, esprime operazioni elementari per la macchina). Dopo questa breve introduzione, andiamo dunque ad elaborare il processo con il quale un problema viene affrontato e risolto utilizzando un “approccio informatico”. Prima di tutto è necessario dare una formalizzazione del concetto di algoritmo, dal momento che l’algoritmo costituisce il metodo attraverso il quale il problema viene risolto (sempre che il problema sia risolvibile mediante un algoritmo!). La definizione di algoritmo deve essere dato, come in ogni disciplina scientifica, in modo rigoroso (non è sufficiente la definizione del vocabolario!). Diamo dunque tale definizione. Definizione di algoritmo: • • • • • • un insieme ordinato di passi eseguibili e non ambigui che determinano un procedimento atto a risolvere un problema o una classe di problemi utilizzando dati iniziali e ottenendo dei risultati in un tempo finito. Esempi di algoritmo sono i procedimenti per le 4 operazioni che ci sono stati insegnati alle elementari, oppure la ricetta per fare le lasagne. E’ evidente che, a seconda dell’esecutore (bambino delle elementari, cuoco) il linguaggio utilizzato è differente; in particolare, il linguaggio utilizzato deve essere tale da essere compreso dall’esecutore! L’algoritmo per eseguire un origami viene espresso non in linguaggio naturale, bensì attraverso una serie di disegni che possano essere compresi dall’esecutore. E’ importante notare che un algoritmo esiste indipendentemente dal linguaggio in cui è espresso. L’algoritmo è un concetto astratto, mentre la sua rappresentazione in un determinato linguaggio è concreta! L’algoritmo è dunque indipendente dal linguaggio in cui è espresso. In particolare, nella definizione che abbiamo dato di algoritmo, non vi è alcun accenno al linguaggio di programmazione! Il linguaggio è necessario per rappresentare l’algoritmo, in particolare per darne una rappresentazione che sia comprensibile da parte dell’esecutore. Ogni linguaggio è costituito da: - un vocabolario (l’insieme dei simboli utilizzati dal linguaggio); una sintassi (un insieme di regole che specificano come comporre i vocaboli per ottenere costrutti ben formati); una semantica (associa un significato ad ogni costrutto linguistico sintatticamente corretto). Una volta che un algoritmo è stato specificato utilizzando un opportuno linguaggio, questo può essere “passato” ad un esecutore. Un esecutore è un soggetto in grado di eseguire le operazioni specificate in un algoritmo. Quindi, in maniera duale rispetto al linguaggio, anche un esecutore è caratterizzato da un linguaggio, dall’insieme di azioni che è in grado di compiere, e dall’insieme delle regole che permettono di associare un’azione da compiere ad ogni costrutto semanticamente corretto del linguaggio. A seconda dell’insieme delle azioni che un esecutore può eseguire, esecutori diversi possono utilizzare algoritmi diversi per risolvere lo stesso problema! Ad esempio, il calcolatore non utilizza l’algoritmo per la divisione che utilizziamo noi! Oppure, per spostarsi avanti di un metro, noi mettiamo in movimento le gambe, mentre un robot metterebbe in rotazione le ruote… Anche CPU diverse possono utilizzare operazioni elementari diverse per eseguire semplici istruzioni! Concentriamoci allora sulla scrittura interpretazione di un programma. Parliamo cioè di linguaggi di programmazione. Introduciamo prima di tutto il concetto di programma. I programmi sono sequenze finite di istruzioni, ognuna scritta in un fissato linguaggio (di programmazione). I programmi eseguibili da un computer devono essere scritti usando un linguaggio che il computer è in grado di “comprendere”. Un algoritmo può essere quindi specificato sottoforma di programma eseguibile da un calcolatore. Att.ne però! Programma e algoritmo sono concetti diversi! Un algoritmo è un concetto astratto. Un programma può essere la rappresentazione di un algoritmo in un determinato linguaggio. D’altra parte, la sequenza di istruzioni all’interno del programma potrebbe anche non terminare mai… In tal caso il programma non sarebbe la rappresentazione di un algoritmo, che, per definizione, termina in un tempo finito!!! Come abbiamo già introdotto in precedenza, affinché l’esecutore possa eseguire un programma è necessario che questo sia specificato in un linguaggio comprensibile all’esecutore stesso. Nel caso in cui l’esecutore sia un calcolatore, si parlerà di linguaggio di programmazione. Ogni linguaggio di programmazione sarà dunque dotato di un vocabolario, un insieme di regole sintattiche, una semantica. Rispetto ad un linguaggio naturale umano, un linguaggio di programmazione ha una sintassi molto semplificata. Esiste una disciplina, nota come teoria dei linguaggi formali, la quale si occupa tra l’altro di classificare i linguaggi in base alla loro “semplicità” (dunque è in grado di quantificare in modo opportuno la complessità di un linguaggio). Distinguiamo tra linguaggi di basso livello e linguaggi di alto livello. I linguaggi di basso livello hanno una sintassi che permette di codificare istruzioni che corrispondono ad azioni molto elementari da parte del calcolatore. Ad esempio, in un linguaggio di basso livello ci aspettiamo che siano codificate istruzioni del tipo “riempi un registro della CPU con X”, “preleva dalla memoria il contenuto che si trova in Y”, … Dal momento che le istruzioni codificate sono di tipo elementare, è richiesto uno sforzo di codifica notevole da parte del programmatore (Es. per fare una somma, sarebbe necessaria una sequenza di istruzioni del tipo: “preleva dalla memoria il contenuto della cella A e mettilo nel registro AA della CPU”, “preleva dalla memoria il contenuto della cella B e mettilo nel registro BB della CPU”, “Somma i registri AA e BB e metti il risultato nel registro CC della CPU”, “Copia in memoria il registro CC della CPU, in posizione C”). Le istruzioni sono immediatamente comprensibili da parte dell’elaboratore, ma per codificare quelle che un utente umano ritiene delle operazioni semplici, è necessario un numero molto alto di istruzioni. Al contrario, i linguaggi di alto livello permettono di programmare usando un insieme di istruzioni più articolato. Ad esempio, per sommare due numeri è possibile utilizzare l’istruzione “c = a + b”, che associa alla cella di memoria c il contenuto di a + il contenuto di b. Mentre nel linguaggio a basso livello erano necessarie 4 istruzioni, nel linguaggio ad alto livello ne basta una! Dunque, lo sforzo di programmazione da parte del programmatore è inferiore. Le istruzioni sono più vicine a quelle che un utente umano ritiene operazioni “elementari”. Per contro, ogni istruzione elementare di alto livello deve essere scissa in una sequenza di istruzioni elementari di basso livello per essere compresa dal calcolatore. Il processore è in grado di riconoscere (e quindi di eseguire) solo programmi scritti in un proprio linguaggio di basso livello (linguaggio macchina). Ogni modello di processore (es: Intel, Pentium, Motorola, PowerPC) ha un proprio linguaggio macchina diverso da quello degli altri processori. Dunque un programma scritto in linguaggio macchina per un processore Intel probabilmente non funzionerà per un processore Motorola! Ad esempio, supponiamo che un processore A sia dotato di N registri, mentre il processore B ne abbia M>N. Ogni istruzione che utilizza un registro da N+1 a M non ha senso per il processore A! Inoltre i due processori possono utilizzare linguaggi macchina con insiemi di istruzioni o sintassi diverse… Dal momento che ogni processore è in grado di intendere solo il proprio linguaggio macchina, quando si programma utilizzando un linguaggio di alto livello è necessario tradurre il codice in linguaggio macchina per fare eseguire il codice al processore. Esistono dei programmi, chiamati interpreti e compilatori, che si occupano di tradurre il linguaggio ad alto livello nel linguaggio macchina comprensibile per il processore. Un compilatore traduce per intero un programma che è stato scritto in linguaggio ad alto livello. Una volta effettuata la traduzione il linguaggio macchina, il programma può essere copiato in memoria ed eseguito. E’ questo il modo di programmare “tradizionale”, ad esempio quello utilizzato quando si programma utilizzando C o C++. L’interprete, invece, traduce ed esegue immediatamente ogni singola istruzione di alto livello che viene digitata dall’utente. Questo modo di lavorare è usato ad esempio da Matlab (utilissimo per fare ricerca). Programma (istruzione) in linguaggio macchina Intel Programma (istruzione) in linguaggio ad alto livello Interprete o compilatore Processore Intel Programma (istruzione) in linguaggio macchina Athlon Processore Athlon Programma (istruzione) in linguaggio macchina Motorola Processore Motorola Analizziamo allora le caratteristiche dei programmi a basso livello rispetto ai programmi ad alto livello. Per quanto riguarda la velocità di esecuzione, i linguaggi di basso livello sono più vantaggiosi rispetto a quelli di alto livello (il programmatore può scegliere come fare lavorare la CPU, dunque può ottimizzarne al meglio il comportamento – al contrario, utilizzando un linguaggio di alto livello, il comportamento reale della CPU viene definito dalla traduzione che il compilatore / interprete effettua, che può non essere ottimale in termini di efficienza). Per quanto riguarda la portabilità, cioè la possibilità di eseguire il programma su macchine diverse, i linguaggi di alto livello offrono sicuri vantaggi rispetto ai programmi di basso livello. Infatti, basta compilare un programma di alto livello per lo specifico processore per poter utilizzare lo stesso codice su macchine diverse. Al contrario, come abbiamo già detto, un programma scritto in linguaggio macchina per un processore Athlon non è detto che funzioni su un processore Motorola! Giocano in questo caso un ruolo fondamentale i compilatori, che devono essere in grado di generare correttamente il codice di basso livello per diversi processori, a partire dallo stesso codice di alto livello. Infine, per quanto riguarda la complessità, abbiamo già accennato al fatto che per un utente umano è più facile scrivere (e capire) codice ad alto livello che a basso livello, in quanto le istruzioni di base utilizzate sono più vicine al modo di ragionare umano (al contrario, le istruzioni a basso livello sono comprensibili dal calcolatore). Dunque la complessità è maggiore per il codice a basso livello. Ciò può essere intuito anche pensando al fatto che, per eseguire la stessa operazione, è necessario specificare un numero di righe di codice molto più alto quando si utilizza un linguaggio di basso livello che uno di alto livello. Torniamo allora indietro nel nostro schema generale di elaborazione dell’informazione, ed occupiamoci ora di come si arriva alla scrittura di un programma. Per arrivare a scrivere un programma, è conveniente muoversi seguendo i passi già individuati: 1) analisi del problema (richiede competenze specifiche al tipo di problema) 2) individuazione dell’algoritmo di risoluzione a sua prima scrittura in un “linguaggio” adatto all’uomo ma non troppo lontano dai linguaggi di programmazione 3) scrittura del programma nel linguaggio di programmazione scelto. In realtà questo modo di procedere può essere espanso come segue: – – – – – analisi del problema e specificazione dei dati in ingresso e in uscita; identificazione e formalizzazione di una soluzione, definizione dell’algoritmo risolutivo; programmazione in un linguaggio di programmazione “ad alto livello”; traduzione in linguaggio macchina; verifica (testing). Il modo di procedere individuato, che si sposta dallo sviluppo di una soluzione di massima alla soluzione dei sottoproblemi, è noto come approccio top-down alla progettazione. Con tale approccio, dunque, si parte da una descrizione ad alto livello della soluzione, in cui si individuano sotto-problemi; si definiscono quindi le soluzioni dei sotto-problemi in termini di operazioni più elementari… e così via, fino ad esprimere tutto in termini di problemi elementari. Per contro, esiste anche una modalità di programmazione bottom-up, nella quale vengono affrontati e risolti mediante piccoli programmi prima i sottoproblemi – le soluzioni parziali dei sottoproblemi vengono poi assemblate in maniera opportuna per arrivare alla soluzione del problema generale. Un esempio di questo modo di procedere è lo sviluppo di librerie, cioè insiemi di funzioni elementari che vengono utilizzate da altri utenti per la risoluzione di problemi generali. Quale che sia il paradigma di programmazione scelto (meglio usare top-down perché più intuitivo), nello sviluppo di un programma vi sono due aspetti da gestire: 1) Gestione dei dati (dati in ingresso, risultati parziali delle operazioni, risultati) – Dove e come memorizzare i dati? 2) Successione di operazioni da compiere. Per affrontare questi due problemi, introdurremo la nozione di contenitore di dati come astrazione dalla nozione di zona della memoria utilizzata da un computer per i dati. Introdurremo quindi i principali tipi di istruzioni. Descriveremo quindi gli algoritmi mediante diagrammi di flusso, strumento per descrivere una successione di operazioni adatto all’uomo e orientato alla traduzione in un linguaggio di programmazione Descriviamo dunque i passi da seguire nella costruzione di un algoritmo, intesa come passo preliminare alla costruzione di un corrispondente programma. Tale descrizione è adatta a quella che è nota come programmazione in piccolo, cioè la costruzione di programmi trattabili da un singolo programmatore. Per contro, la programmazione in grande richiede processi di sviluppo ingegnerizzati, che non tratteremo. Abbiamo detto che un problema è caratterizzato da dei dati in ingresso e da un risultato. Per ottenere il risultato, avremo inoltre bisogno di svolgere dei calcoli e di mettere da parte dei risultati parziali. Per fare ciò, ci viene incontro la nozione di contenitore dei dati, cioè un’astrazione della nozione di area di memoria contenente dei dati. Tale contenitore di dati è detta variabile di un programma. In un programma avremmo tante variabili quanti sono i dati che ci interessa memorizzare! Ogni contenitore di dati (variabile) ha un tipo; a seconda del tipo di variabile, avremo un insieme di elementi rappresentabili ed un insieme di operazioni possibili su di essa. Ad esempio, vi saranno variabili di tipo intero (int); ciascuna di queste variabili occuperà 4 byte in memoria (32 bit) e permetterà di rappresentare un intervallo di numeri interi. Tra le operazioni possibili vi saranno +, -, *, /, >, <, ==, avendo presente che l’insieme dei numeri rappresentabili è finito e tutti i numeri sono interi (Æ possibile overflow, in accuratezza nella divisione, …). Un altro tipo possibile è il tipo BYTE, che permette di rappresentare 256 numeri interi. Un ultimo tipo presentato qui è il tipo boolean, per la rappresentazioni di variabili booleane (cioè variabili che possono essere VERE o FALSE,). In questo caso, per esempio, l’operazione di divisione non è definita! Per la variabili possiamo utilizzare la seguente rappresentazione grafica: Nome del contenitore: pippo tipo : intero pippo: intero 54 Contenuto = dato (appartenete al tipo di dati associato al nome, infatti 54 è un numero intero, e su di esso sono ammesse le usuali operazioni aritmetiche) I linguaggi di programmazione ad alto livello (ma anche Excel e Access) prevedono la tipizzazione dei dati. Vi è poi da fare la distinzione tra dati di tipo semplice (Es. una variabile di tipo intero, reale, testo o logico) ed i dati di tipo strutturato. I dati di tipo strutturato sono dati che contengono più di un valore, ad esempio matrici o vettori (es. [1 2 3 4 5]). Un altro dato di tipo strutturato sono i record: strutture non uniformi, cioè contenenti più valori non necessariamente dello stesso tipo. Esempi: Variabile semplice di tipo intero: a, contiene a = [1]; Variabile strutturata di tipo vettore di reali: v, contiene v = [0.1 0.5 0.666] Variabile strutturata di tipo “studente” (un particolare record definito dall’utente), un record: s, s contiene: s.nome = ‘Pippo’ s.cognome = ‘ Franco’ s.matricola = 234543 s.mediaesami = 18.1 In quest’ultimo esempio la variabile strutturata s (un record) è di tipo “studente”, un tipo composto da due variabili di tipo testo, una variabile di tipo intero, una variabile di tipo reale. Passiamo allora a vedere quali sono le istruzioni fondamentali per lavorare sui dati. Vi sono tre tipi fondamentali di istruzioni: 1) Ingresso / uscita; 2) Aritmetico / logiche; 3) Di controllo. 1) Ingresso / uscita Le istruzioni di ingresso / uscita permettono di acquisire dati e di presentare risultati. Ad esempio: read a - acquisisci un dato da tastiera e mettilo nel contenitore ‘a’ print 'La media dei valori dati in ingresso è ', media - stampa il contenuto di ‘media’ preceduto da un commento *** 2) Aritmetico / logiche Per quanto riguarda le istruzioni aritmetico / logiche, abbiamo: Le istruzioni di assegnamento, che modificano lo stato di memoria, cioè i valori dei contenitori dati (detti variabili). Sono della forma: CONTENITORE = ESPRESSIONE [(leggi: metti ESPRESSIONE in CONTENITORE)] Es.: a = b + 3 ESPRESSIONE può essere: una costante, una variabile, un’espressione vera e propria, una funzione CONTENITORE è il nome di una variabile l’esecutore valuta l’ESPRESSIONE e mette il valore così calcolato in CONTENITORE, sostituendone il valore precedente. Le espressioni aritmetiche esprimono calcoli numerici (somma, sottrazione, prodotto divisione, elevamento a potenza, radice, logaritmo, esponenziale, ecc.) Es.: b**2 – 4 * a * c Le espressioni sui caratteri modificano parole e testi: Es. concatenazione, moto&sega (risultato: motosega) Le espressioni logiche (o booleane) esprimono calcoli logici e possono quindi assumere solo i valori vero o falso. Operatori logici relazionali: confronto fra due valori, ad esempio • x<y • (x + 5) = y • ecc. Operatori logici AND, OR, NOT, ecc. per comporre, ad es.: • (x < y) AND (y < z) Un valore booleano (vero, falso) è rappresentabile con un bit. Convenzionalmente si assegna 1 a vero, 0 a falso. *** 3) Di controllo Le espressioni logiche assumono un particolare importante quando abbinate alle istruzioni di controllo, in quanto permettono di prendere delle decsioni. Infatti, le istruzioni di controllo permettono di modificare il flusso di esecuzione delle istruzioni all’interno di un programma, altrimenti puramente sequenziale. Es. Selezione if, case, ... Iterazione while, repeat for Si basano sull’uso di espressioni booleane Es. se x > 0, calcola la radice quadrata di x, altrimenti calcola la radice cubica di x. *** Una comoda rappresentazione di un programma, utile anche per la sua traduzione in un linguaggio di programmazione, è data dalla rappresentazione mediante diagrammi di flusso. Ogni diagramma di flusso contiene dei blocchi di elaborazione; ogni blocco di elaborazione contiene delle sequenze di azioni (dunque troveremo all’ingresso di un blocco di elaborazione delle istruzioni aritmetico / logiche o delle operazioni di input/output). metti x+y in y metti x-1 in x Vi sono poi blocchi decisionali, che contengono un’espressione booleana e dirigono il flusso del programma diversamente a seconda del risultato dell’operazione booleana. falso vero x = 0 Un diagramma di flusso si ottiene connettendo la frecce uscenti dai blocchi di elaborazione e decisionali. Le tre principali strutture sono: sequenza Æ una sequenza di operazioni che vengono eseguite una dopo l’altra; selezione Æ un’espressione booleana che devia il flusso del programma; iterazione Æ si ottiene componendo una sequenza con una selezione (non necessariamente in quest’ordine) permette di iterare una sequenza di istruzioni fino al verificarsi di una condizione. Raggruppando insieme dei blocchi otteniamo quella che è nota come decomposizione modulare: ovvero, è possibile raggruppare una serie di blocchi che eseguono una serie di operazioni complesse e raggrupparli in un unico blocco che permetta di ottenere una rappresentazione più semplice. Proviamo allora a tradurre uno schema a blocchi in codice. Inizio Function RadiceQuadrata; Acquisisci a SI’ Real a,b; // Si alloca spazio in memoria per a, b Repeat a<0? Display(‘Dammi a’); read a; NO Until (a>=0) b = sqrt (a) b=sqrt(a); Scrivi: “la radice di a è <b>” Display(‘La radice di a è: <b>’) end Fine Laboratorio di Informatica AA 2006/2007 75 La traduzione di uno schema a blocchi (o da un’altra rappresentazione di alto livello) in un linguaggio di programmazione è l’attività di programmazione. Esistono diversi paradigmi di programmazione. Approfondiremo la programmazione dal punto di vista del paradigma procedurale, basato su moduli (funzioni) che permettono di elaborare dei dati fornendo dei risultati; le funzioni possono essere incapsulate l’una dentro l’altra. Un diverso paradigma procedurale (ma ve ne sono anche altri!) è quello ad oggetti, nel quale il programmatore crea una serie di oggetti, ciascuno dotato di uno stato e in grado di svolgere alcune funzioni caratteristiche… Questo paradigma di programmazione è più vicino come interpretazione alla vita di tutti i giorni, ad esempio pensiamo all’implementazione di un oggetto “panettiere”, questo oggetto potrà avere più stati (es. ‘dietro al bancone’, ‘al forno’, …) e potrà svolgere diverse funzioni (es. ‘cuocere il pane’, ‘emettere uno scontrino’, …). L’interazione con altri oggetti (es. oggetto consumatore) fa poi avanzare il programma… Approfondiamo dunque un paio di aspetti della programmazione procedurale. Il programmatore crea diversi moduli (funzioni). Ogni funzione può avere una serie di input (variabili di ingresso – i dati da elaborare) e di output (variabili di uscita – i risultati). Ad esempio, se pensiamo ad una funzione che esegue la divisione intera, potremo avere: [Quoziente, Resto] = DivisioneIntera (Dividendo, Divisore), dove (Dividendo, Divisore) sono gli input, [Quoziente, Resto] sono gli output, DivisioneIntera è il nome della funzione. Andiamo allora a vedere come una possibile implementazione della funzione DivisioneIntera, utilizzando la sintassi di Matlab: [Quoziente, Resto] = function DivisioneIntera (Dividendo, Divisore); Quoziente = div (Dividendo, Divisore); Resto = mod (Dividendo, Divisore); return; La prima riga contiene la dichiarazione della funzione. Nella dichiarazione viene utilizzata la parola chiave function, che specifica che si sta effettuando una dichiarazione di funzione. La seconda e terza riga effettuano l’elaborazione dei dati. Notiamo che queste righe contengono delle chiamate ad altre funzioni! Dunque: - per dichiarare una funzione, la sintassi è data da: o [Output1, Output2, … OutputN] = function Funzione (Input1, Input2, … input) E’ necessario dichiarare una funzione per poterla utilizzare! - per utilizzare una funzione, la sintassi è data da: o [Output1, Output2, … OutputN] = Funzione (Input1, Input2, … input) Siamo dunque di fronte ad un esempio di incapsulamento delle funzioni! La funzione DivisioneIntera effettua delle chiamate alle funzioni div e mod! E’ comodo creare una librearia di funzioni, perché ciascuna funzione può essere utilizzata da altre funzioni quando necessario (programmazione modulare – ogni funzione è un modulo da riutilizzare). Infine, la quarta e ultima riga termina l’esecuzione della funzione mediante la parole chiave return (in Matlab, questa istruzione è in realtà opzionale – se non viene utilizzata, la funzione termina automaticamente dal momento che non trova più codice da eseguire!).