Questa serie di post accompagnerò il corso che sto seguendo online e gratuitamente presso il MIT di Boston. Trovate tutte le informazioni a questo indirizzo, non lasciatevelo scappare.

Introduzione al corso

È un corso di informatica di base, un approccio gentile alla programmazione. Verrà utilizzato il linguaggio Python (non storcete il naso!).

Il Professor Guttag fa subito notare che non sarà un corso “facile”, inizia con un approccio gentile ma diventa subito più impegnativo.

È opportuno anche considerare che questo non è un corso sull’abilità di ricordare le cose, ma sul saper risolvere i problemi e riuscire a far fare al computer quello che si desidera. No, per certe cose è ancora necessaria una fidanzata, ma la tecnologia avanza 🙂

I due tipi di conoscenza

Ci sono due tipi di conoscenza: dichiarativa e imperativa.

Dichiarativa

La conoscenza dichiarativa si basa sui fatti:

“una buona assicurazione migliora la qualità delle cure mediche e fa risparmiare” – almeno in USA 🙂

“\(y\) è la radice quadrata di \(x\) se e solo se \(y \ast y = x\)“

Questa impostazione si presta molto bene alla tecnica “prova e controlla”, prendo un dato talvolta anche a caso e controllo se corrisponde alla dichiarazione.

Imperativa

La conoscenza imperativa invece fornisce le istruzioni per risolvere il problema. Un po’ come una ricetta di cucina. Ad esempio, per trovare la radice quadrata si può usare il metodo di Erone di Alessandria:

  1. Prova con un numero \(g\)
  2. Se \(g \ast g\) è abbastanza vicino a \(x\), allora \(g\) è una buona approssimazione della radice quadrata di \(x\)
  3. Altrimenti, crea una nuova approssimazione facendo una media tra il vecchio valore di \(g\) e \(\frac{x}{g}\). Precisamente: \(g_{nuovo} = \frac{(g_{vecchio} + \frac{x}{g_{vecchio}})}{2}\)

Proviamo a seguire queste istruzioni per trovare \(\sqrt{25}\), dunque \(x=25\):

  1. Iniziamo con \(g =1\)
  2. \(g \ast g\) è abbastanza vicino a \(x\), allora \(g\)? Dunque \(1 \ast 1 = 1\) non è abbastanza vicino a \(25\) allora \(1\) non è una buona approssimazione della radice quadrata di \(25\)
  3. Perciò crea una nuova approssimazione con la formula \(g_{nuovo} = \frac{(g_{vecchio} + \frac{x}{g_{vecchio}})}{2}\), dunque \(g_{nuovo} = \frac{(1 + \frac{25}{1})}{2} =\color{blue}{13}\)

Proseguiamo con un’altra interazione

  1. Iniziamo con \(g =\color{blue}{13}\)
  2. \(g \ast g\) è abbastanza vicino a \(x\), allora \(g\)? Dunque \(13 \ast 13 = 169\) non è abbastanza vicino a \(25\) allora \(13\) non è una buona approssimazione della radice quadrata di \(25\)
  3. Perciò crea una nuova approssimazione con la formula \(g_{nuovo} = \frac{(g_{vecchio} + \frac{x}{g_{vecchio}})}{2}\), dunque \(g_{nuovo} = \frac{(13 + \frac{25}{13})}{2} =\color{pink}{7,4615}\)

Proseguiamo con un’altra interazione

  1. Iniziamo con \(g =\color{pink}{7,4615}\)
  2. \(g \ast g\) è abbastanza vicino a \(x\), allora \(g\)? Dunque \(7,4615 \ast 7,4615= 55,6740\) non è abbastanza vicino a \(25\) allora \(7,4615\) non è una buona approssimazione della radice quadrata di \(25\)
  3. Perciò crea una nuova approssimazione con la formula \(g_{nuovo} = \frac{(g_{vecchio} + \frac{x}{g_{vecchio}})}{2}\), dunque \(g_{nuovo} = \frac{(7,4615 + \frac{25}{7,4615})}{2} =\color{orange}{5,4060}\)

Proseguiamo con un’altra interazione

  1. Iniziamo con \(g =\color{orange}{5,4060}\)
  2. \(g \ast g\) è abbastanza vicino a \(x\), allora \(g\)? Dunque \(5,4060\ast 5,4060= 29,2248\) non è abbastanza vicino a \(25\) allora \(5,4060\) non è una buona approssimazione della radice quadrata di \(25\)
  3. Perciò crea una nuova approssimazione con la formula \(g_{nuovo} = \frac{(g_{vecchio} + \frac{x}{g_{vecchio}})}{2}\), dunque \(g_{nuovo} = \frac{(5,4060+ \frac{25}{5,4060})}{2} =\color{ForestGreen}{5,0152}\)

Proseguiamo con un’altra interazione

  1. Iniziamo con \(g =\color{ForestGreen}{5,0152}\)
  2. \(g \ast g\) è abbastanza vicino a \(x\), allora \(g\)? Dunque \(5,0152\ast 5,0152= 25,15\) è abbastanza vicino a \(25\) allora \(5,0152\) è una buona approssimazione della radice quadrata di \(25\)
  3. Questa volta non serve eseguire il terzo passaggio.

L’algoritmo

Possiamo dunque sostenere che l’algoritmo (il set di istruzioni di calcolo) converge. In sostanza abbiamo utilizzato tre elementi: le istruzioni e il controllo del flusso, il tutto corredato da una non meno importante condizione di fine.

L’alba dei calcolatori

È bene ricordare i primi calcolatori funzionavano con istruzioni fisse, non si poteva cioè cambiare l’algoritmo ma solo inserire i dati e attendere la computazione.

Questo è facilmente riscontrabile anche nelle calcolatrici più economiche e basilari. Posso inserire i dati, ma non cambiare il modo in cui vengono svolte le operazioni.

Da notare che anche il computer costruito da Alan Turing per decifrare la famosa macchina Enigma seguiva questa impostazione. Infatti fu costruito appositamente per quello scopo e non avrebbe potuto fare cose come ricordare un promemoria.

Se non l’avete fatto, è una buona idea guardare il film The imitation game, magari non accurato ma ben realizzato.

Quello che fece davvero la differenza, fu l’invenzione dei computer programmabili. Il programma e i dati convivevano nella memoria rendendo la macchina incredibilmente flessibile. Quindi si verificò l’ipotesi in cui un programma “fa” un altro programma. Questo perché se il codice viene conservato come dei dati, allora posso fare un programma che scrive del codice. Intrigante, anche se meno fantascientifico di Skynet alla fine!

Spuntò dunque il programma noto come Interprete con il compito di “spiegare” al calcolatore il codice scritto da noi. Più precisamente un interprete è un software che è in grado di eseguire altri programmi a partire dal codice sorgente.

E sempre Turing fece capire che ogni possibile operazione può essere scomposta in 6 operazioni di base (primitive) permettendo dunque di far svolgere al computer qualunque compito spiegato in questi 6 elementi:

  1. Muovi il puntatore a destra
  2. Muovi il puntatore a sinistra
  3. Stampa un simbolo nella posizione
  4. Leggi un simbolo dalla posizione
  5. Cancella il contenuto della posizione
  6. Non fare più nulla, fermati

Un video renderà il concetto molto meglio digeribile:

Il linguaggio di programmazione

Finalmente comincia dunque ad essere più chiaro cosa effettivamente è un linguaggio di programmazione, quello in cui noi scriviamo il nostro codice sorgente. Diremo che il linguaggio di programmazione è un linguaggio formale che specifica un insieme di istruzioni.

Sono proprio gli elementi nominati prima (istruzioni, controllo del flusso, compilazione) a differenziare per lo più i linguaggi di programmazione. Nello specifico noteremo delle differenze in:

  • Sintassi: la sequenza dei caratteri e dei simboli necessari a costituire una stringa regolare
  • Semantica Statica: la validità del significato di queste stringhe correttamente composte
  • Semantica: il significato stesso

Quando scriviamo delle istruzioni per il computer ricordiamo una cosa: lui farà solo e sempre quello che gli abbiamo chiesto! Ciò significa soprattutto che se il programma che abbiamo scritto non funziona, è ovviamente colpa nostra. 🙁

Quindi se scrivo \(x = 3+4\), questa è una stringa sintatticamente regolare. Se però scrivo \(x = 3 4\) lasciando uno spazio tra le cifre, abbiamo un errore. Quindi controllando la sintassi, verifichiamo solo se la forma dell’espressione è corretta.

Ora vediamo la stringa \(x = 3 /div “abc”\) e ci rendiamo conto che sintatticamente è corretta, ma priva di alcun significato. Che senso ha dividere un numero per un insieme di lettere? Manca di Semantica Statica.

Pensa a scrivere “io essere bello”. La sintassi è corretta: soggetto+verbo+complemento, ma non ha significato.

Questi aspetti (sintassi e semantica statica) sono facilmente controllabili dal computer che ci restituisce subito un errore, spesso abbastanza chiaro da permetterci una veloce riparazione.

La vera difficoltà si ha quando un programma è sintatticamente corretto e non ha errori di semantica statica, ma fa una cosa diversa da quello che ci aspettiamo (errore semantico puro). Qui il calcolatore non si accorge dell’errore perché tutto è formalmente corretto e fornisce la risposta giusta ad una domanda sbagliata. Sono le situazioni che fanno perdere più tempo, ma anche quelle dove alla fine si impara di più dai propri errori grazie al processo di debuging.

Le situazioni a cui può portare un errore semantico puro

Abbiamo visto come un programma che non ha errori sintattici e di semantica statica viene eseguito dal calcolatore, ma può portare a situazioni critiche:

  • crash: a chi non è mai capitato?
  • ciclo infinito: continua a girare a vuoto impegnando la macchina.
  • risposta sbagliata: il programma arriva fino alla fine ma ci da il risultato sbagliato. A volte è difficile da individuare questa situazione mentre i casi precedenti casi sono molto più “visibili”.

Perché nel corso verrà utilizzato Python

  • È facile da imparare
  • È molto usato nelle scienze
  • È molto più facile il debugging rispetto ad altri linguaggi.

Interprete e compilatore

I linguaggi di programmazione possono seguire due vie per arrivare all’esecuzione: quella corta o quella lunga 🙂

Nella via corta, cioè quella dove è previsto solo l’interprete, il codice sorgente viene controllato (sintassi e semantica statica) e tradotto per il calcolatore in linguaggio di basso livello. Se qui si verifica un errore, viene subito individuato nel codice sorgente e segnalato con una apprezzabile precisione.

Nella via lunga, dopo i soliti controllini, interviene un compilatore che trasforma il nostro codice in un “object code”, qualcosa a metà tra noi e la macchina. Poi arriva l’interprete che fa la traduzione finale, ma se ci sono problemi punterà il dito contro pezzi di object code, rendendoci la vita difficile.

E allora perché diavolo compiliamo? Beh i programmi compilati sono solitamente più efficienti proprio per via di questa traduzione intermedia. Python si presta ad essere compilato, ma non è necessario.

Il professor Guttag

Vi lascio un video simpatico del Prof: