Chiunque inizi a cimentarsi con le lezioni di programmazione (che sia in C, in C++, in Java o in qualsivoglia altro linguaggio) si trova fin da subito a dover far i conti con un nuovo modo di pensare: il pensiero algoritmico, noto anche come pensiero computazionale, ("Computational Thinking" in inglese).
Lezioni di programmazione online
Sia chiaro: dico "nuovo" non perché sia nuovo per davvero. Il pensiero algoritmico è infatti parte integrante del modo in cui pensa un essere umano quando si approccia alla risoluzione di un problema. E allora perché "nuovo"? Seguitemi e capirete.
Quando siamo chiamati a risolvere un problema il primo istinto è quello di tuffarsi in pista, iniziando a guardarci intorno prestando attenzione ai vari elementi del problema in esame alla ricerca di "appigli" vari, in altre parole fatti o informazioni utili a sbloccare nella nostra mente tutte quelle intuizioni, associazioni, deduzioni che, alla fine, ci spianano la strada verso la soluzione. È un po' quello che accade quando cerchiamo di risolvere un problema di geometria, dove generalmente ci viene fornita una figura e alcune misure relative a delle parti che la compongono; oltre a, chiaramente, una domanda sulla figura stessa, come ad esempio: "Quanto misura il lato ai vertici B e C del trapezio dato?" oppure "Quanto misura l'area del rettangolo aventi vertici A, B, C' e D' facente parte del trapezio riportato in figura?".
Questo approccio coinvolge però in realtà due tipi di pensiero, che in questa sede chiameremo pensiero esecutivo (o estemporaneo) e pensiero proiettivo. Qual è la differenza?
Diciamo che pensiamo esecutivamente ogni volta che ci accorgiamo che un certo passo di risoluzione può essere eseguito subito, senza attendere oltre. Ad esempio, se ci viene chiesto di ordinare numericamente in ordine crescente tre carte prese a caso da un mazzo di carte francesi, ci renderemo conto quasi subito se ci sono carte da scambiare e se ci sono carte da lasciare dove sono. Ad esempio, nella figura seguente ci accorgiamo immediatamente che occorre scambiare il 9 e il J, e che il K va invece lasciato lì dov'è.
In una situazione del genere, l'istinto sarebbe dunque quello di scambiare di posto il J e il 9, ottenendo così un insieme di tre carte ordinate in modo crescente (9, J e K). Osserviamo adesso la seguente situazione.
In questo scenario conosciamo solo una delle tre carte, il K. Questa situazione ci dà subito una informazione certa e chiara: il K non si sposterà. Questo è vero perché, se le due carte ignote fossero inferiori a K, nessuna delle due andrebbe ad occupare il posto di K; mentre, se una o entrambe fossero un K, non ci sarebbe motivo di modificare la posizione del K a destra (attenzione: per semplicità, stiamo volutamente ignorando la precedenza dei semi, in quanto non sostanziale per questa discussione).
In definitiva, quindi, sappiamo che il K mostrato in figura non si sposterà qualunque cosa capiti, poiché non esiste alcuna carta maggiore di K. Pertanto, deduciamo che non c'è alcuna ragione per cui K debba spostarsi; e così decidiamo di lasciarla lì dov'è. Questa decisione estemporanea viene fuori da quello che abbiamo chiamato pensiero esecutivo. Ma come ci comportiamo con le altre due carte? Intuitivamente, verrebbe da dire che non possiamo risolvere il problema fin quando non sapremo di quali carte si tratta. Questo però è vero solo in parte.
È vero se intendiamo risolvere il problema solo attraverso l'approccio estemporaneo visto poc'anzi (osservo --> ragiono --> svolgo). È falso se contempliamo la possibilità di applicare un approccio di tipo proiettivo.
Un approccio proiettivo non intende produrre come soluzione una sequenza di azioni svolte sul momento, come invece suggerisce di fare l'approccio estemporaneo, ma piuttosto una sequenza di azioni da svolgere in base a diversi scenari possibili. Una soluzione per il problema delle carte elaborata seguendo un approccio proiettivo si presenterà quindi come una sfilza di "SE ... ALLORA ...", ciascuno avente il compito di imporre una propria regola operativa in base al verificarsi di una determinata condizione.
Potremmo quindi muoverci nel seguente modo. Anzitutto, poiché avremo bisogno di riferirci ad entrambe le carte in modo non ambiguo, introdurremo dei nomi per tali carte, banalmente "A" per la prima a sinistra e "B" per quella al centro.
Ora, è chiaro che al posto di A e B potrebbe esserci qualunque cosa. Questo potrebbe portarci a concludere che è necessario prevedere tutti i casi possibili e dunque definire un'azione per ciascun caso. Ad esempio, potremmo pensare di produrre le seguenti regole:
Poi però dovremmo proseguire in questo modo:
Si riprenderà quindi definendo tutte le regole per le coppie di tipo <3, b>, ovvero:
e via così per tutte le coppie possibili <a, b>.
Domanda: è davvero necessario ragionare in questo modo? Non c'è un modo più "asciutto" ma comunque in grado di contemplare tutti i casi possibili in un colpo solo o comunque in una manciata di istruzioni? La risposta è sì. A ben vedere, infatti, riflettendoci un po' su, si capisce subito che lo scambio debba avvenire solo quando la carta A risulti maggiore della carta B. Grazie a questa osservazione possiamo quindi formulare le seguente semplice regola:
SE A è più grande di B ALLORA scambia di posto A e B.
Detto, fatto. Con un approccio proiettivo abbiamo trovato la soluzione a un problema apparentemente irrisolvibile. In realtà, la nostra soluzione non va a risolvere un problema specifico, come quello dello scenario J, 9, K visto prima, ma un'intera classe di problemi dello stesso tipo, nella fattispecie il problema (computazionale) di dover ordinare due carte in base al loro valore numerico. L'applicazione o meno della regola dipenderà quindi dalla specifica istanza del problema computazionale che si presenterà, dando così luogo a uno scambio o meno in base alla specifica situazione.
I due tipi di pensiero appena discussi (estemporaneo e proiettivo) si rincorrono continuamente nella nostra esperienza quotidiana in un continuo alternarsi di decisioni attuate istantaneamente e, viceversa, di decisioni pianificate. Questa mescolanza ci fa apparire il nostro pensiero come un'unica cosa, un unico modo di approcciare alla risoluzione di un problema. In realtà, come abbiamo visto, si tratta di due approcci distinti che semplicemente si alternano nella nostra mente in base alla necessità e alla complessità del problema da risolvere. Ecco perché il pensiero proiettivo, che chiameremo anche pensiero computazionale, non è qualcosa di veramente nuovo, inteso come "estraneo alla nostra esperienza". Al contrario, esso scatta in modo naturale in ognuno di noi, agendo nascostamente in congiunzione con il pensiero esecutivo, confondendosi con esso e lasciando quindi, in un certo senso, perdere le proprie tracce nei meandri dei nostri ragionamenti. L'obiettivo, per un aspirante programmatore, è imparare a distinguere e riconoscere le due forme di pensiero e ad applicarle a suo piacimento, ovverosia in funzione delle sue necessità. In particolare:
Una soluzione computazionale è sempre qualcosa espressa nella forma SE ... ALLORA ...? No, non sempre. Si prenda a titolo di esempio il problema dello scambio.
Si hanno un bicchiere A e un bicchiere C, ognuno contenente una bibita differente, e si desidera scambiarne il contenuto. L'esperienza quotidiana ci suggerisce che è possibile usare un terzo bicchiere di appoggio al fine di versarvi temporaneamente uno dei due contenuti. L'idea è cioè quella di liberare uno dei due bicchieri per consentire il travaso del contenuto dell'altro. Se conveniamo di chiamare V il bicchiere vuoto, la sequenza computazionale finale sarà dunque:
Se però la si guarda bene, questa sequenza corrisponde esattamente alle azioni che avremo svolto una volta effettuato lo scambio. Sembrerebbe quindi che, in questo caso, non ci sia una distinzione netta tra una soluzione computazionale ed una esecutiva, cosa che prima eravamo in grado di fare grazie alla presenza delle regole SE ... ALLORA ....
In realtà però non è così. Una differenza è ancora riscontrabile e consiste nel livello di generalità che emerge dalla sequenza. Riflettendoci bene, infatti, è certamente vero che le azioni svolte al termine dello scambio saranno le tre sopra descritte; ma è altrettanto vero che, al momento dell'esecuzione, i bicchieri A e C hanno un contenuto ben definito, cioè acqua e coca cola. L'esecuzione delle tre azioni sopra descritte darà quindi luogo ad una sequenza che è più appropriato descrivere nel modo seguente:
Come vedete, a sinistra delle frecce sono rimasti i nomi dei contenitori; ma a destra delle frecce sono comparsi i "valori" associati ai contenitori in un preciso momento della computazione in via di svolgimento.
La differenza sta dunque solo ed unicamente nella presenza o meno di generalità? Nì. Immaginiamo di avere un bicchiere vuoto e di voler versare al suo interno dell'acqua. L'azione da svolgere è in tal caso la seguente:
V <-- acqua
Cos'è questa? È un'azione computazionale o esecutiva? La risposta in questo caso è: dipende. In sintesi:
La differenza dunque sta tutta nella presenza o meno di progettualità. Quindi:
In definitiva, un algoritmo si presenta come una sequenza di istruzioni, ovvero di azioni ancora da svolgere (e che non è detto verranno svolte tutte quante). Una computazione è invece una sequenza di azioni già svolte, come risultato dell'esecuzione di un algoritmo.
Nel prossimo articolo vedremo alcuni esempi dove metteremo a confronto algoritmi e rispettive sequenze di computazione.