Sicuramente abbiamo tutti letto un'infinità di articoli che ci dicono che programmare è anzitutto risolvere problemi, ma non solo. Quella del problem solving sta, sempre più, diventando una disciplina di prima classe con un nome e un cognome e tutto uno studio delle strategie di ciò che si fa quando si analizza e si risolve un problema.
E dunque ti poniamo la seguente domanda:
Che cos'è, esattamente, il problem solving?
La definizione più banale che possiamo dare è una mera traduzione dall'inglese. Si tratta dell'attività di risolvere un problema. Ma qui c'è un problema!
Esatto. Siffatta, la nostra definizione è tautologica, ma possiamo scomporla in due componenti: l'atto del risolvere e l'oggetto di questo atto, cioè il problema.
Ergo, dobbiamo andare più a fondo e chiederci:
Che cos'è un problema?
Lo psicologo e pittore Gaetano Kanizsa definì un problema come "una qualsiasi cosa che sorga quando un essere vivente cerca di ottenere qualcosa, ma non può farlo attraverso un'attività istintiva o un comportamento appreso". In altre parole, un problema è un ostacolo non banale tra noi e un nostro obiettivo.
Da un punto di vista logico e matematico, dobbiamo fornire una definizione un po' più rigorosa: per quel che ci compete, adotteremo la seguente definizione:
un problema è l'insieme delle seguenti componenti:
-
Un'esigenza: può essere un'informazione che abbiamo bisogno di ottenere oppure un'azione che abbiamo bisogno di eseguire.
-
Dei dati espliciti: sono informazioni di partenza che rappresentano la situazione iniziale, in cui il problema si presenta.
-
Dei dati impliciti: sono tutte le informazioni contestuali che vengono dalla nostra conoscenza e possono andare accumulandosi mentre tentiamo di risolvere il problema; sono dati impliciti anche quelli che emergono dalla combinazione logica dei dati espliciti.
-
(facoltativo)Una soluzione: in realtà dovrei dire "una conclusione". Una conclusione valida potrebbe essere che il problema non ha soluzioni oppure che una soluzione univoca non è determinabile.
-
(facoltativo) Una ragione: la ragione non fa esattamente parte del problema, ma di fatto ne costituisce la radice. Questo è importante perché l'eliminazione della radice di un problema può equivalere alla sua risoluzione. A tal proposito, teniamo a mente locuzioni come "risolvere il problema alla radice" e "il problema non si pone".
Che significa risolvere
Risolvere viene dal latino solvo che significa sciogliere, slegare. In italiano usiamo, metaforicamente, espressioni come" sciogliere un nodo" oppure "districare una matassa", per indicare la risoluzione di problemi più o meno complessi.
Siamo pronti per chiederci in che senso un problema viene risolto. Si suol dire che ogni problema contiene la sua soluzione; a nostro avviso la realtà dei fatti è ben diversa. È, tuttavia, vero, metaforicamente parlando, che nella quasi totalità dei casi la soluzione del problema deriva (direttamente o indirettamente) dai dati di partenza. È anche vero che "la semplicità è l'ultimo livello della complicazione".
Spieghiamolo meglio: molto spesso risolvere un problema significa aprire una grande quantità di parentesi e porsi moltissime domande. Generalmente, quando le informazioni o competenze che abbiamo acquisito sono sufficienti, tutte le parentesi si chiudono e tutte le informazioni raccolte si sintetizzano in una soluzione relativamente semplice.
I problemi in matematica, geometria e fisica
Nell'esperienza comune di ognuno di noi, queste sono le tre principali materie in cui è presente una nozione sufficiente formalizzata di problema; inoltre, nel corso della nostra carriera scolastica, chi più chi meno, tutti siamo chiamati a risolvere una notevole quantità di problemi di matematica, geometria o fisica.
Generalmente negli esercizi scolastici sono presenti tutte le caratteristiche che abbiamo usato prima per definire un problema. L'esigenza è ciò che l'esercizio ci richiede, i dati espliciti ci vengono forniti dal cosiddetto testo del problema. I dati impliciti vengono (o dovrebbero venire) dal nostro studio dell'argomento trattato.
La soluzione è ciò che troveremo collegando logicamente i dati espliciti e i dati impliciti, la ragione (non condivisa da molti studenti) è che facendo molti esercizi acquisiremo padronanza dell'argomento.
I problemi e le verità
In teoria, il problem solving ha un rapporto strettissimo con il concetto logico di verità, cioè di affermazione veritiera. In logica, un'affermazione che può essere veritiera oppure no è definita un predicato. L'idea che c'è dietro letteralmente a tutta la matematica è che, partendo da taluni assiomi (affermazioni indimostrabili date per vere), è possibile derivarne una serie di verità necessarie, cioè affermazioni che sono vere per forza, se si assume che gli assiomi sono veri. Ricordiamo tutti il sillogismo aristotelico, cioè quel processo deduttivo per cui a partire da due premesse veritiere A e B se ne trae una terza C, anch'essa veritiera.
Capire il flusso della verità, cioè il processo mediante il quale la veridicità di un'affermazione si propaga combinandola con altre informazioni è, a nostro avviso, l'aspetto più importante del problem solving di tipo logico/analitico.
Possiamo propagare la verità in due direzioni: dal particolare al generale o dal generale al particolare.
Dal particolare al generale: il metodo induttivo
Questo è sicuramente l'approccio più empirico e forse anche pragmatico, stiamo utilizzando un approccio induttivo ogni volta che andiamo per tentativi, lasciando che l'emisfero schematico del nostro cervello estragga informazioni strutturali e generali.
Il grande limite del metodo induttivo è che, a meno di non verificare tutti i casi particolari (che potrebbero essere infiniti), non possiamo essere certi che la conclusione generale sia esatta. Nel metodo scientifico, si dice che in questi casi la legge generale è vera fino a prova contraria, o che è falsificabile. Un caso interessante è quello del principio di induzione che si usa in matematica, in cui si dimostra che un'affermazione vale per ogni numero e per il suo successivo: in questo caso vengono verificati tutti gli infiniti casi particolari, e quindi la conclusione generale è vera.
Dal generale al particolare: il metodo deduttivo
Anche se finora abbiamo menzionato la matematica parlando di induzione, bisogna dire che la matematica è praticamente tutta basata sulla deduzione. Il principio deduttivo è l'esatto opposto di un ragionamento di tipo empirico e, infatti, alla radice di ogni teorema matematico, per quanto complesso, ci saranno sempre degli assiomi, cioè delle affermazioni di carattere generico, assolutamente indimostrabili (per quanto ragionevolmente credibili).
Torniamo al problem solving: il caso più lampante di uso della deduzione nel problem solving è l'applicazione di modelli. Prendere un'equazione o un algoritmo, e infilarci dentro i nostri dati iniziali per ottenere una soluzione, corrisponde all'applicazione di un sistema deduttivo.
I problemi e l'informatica
Nel nostro lavoro risolviamo continuamente problemi, di natura logica e non. Sicuramente, avere nella propria cassetta degli attrezzi un pensiero analitico molto ben sviluppato avvantaggia il bravo programmatore informatico, ma torniamo alla definizione più grezza e lassa di problema, inteso come un ostacolo tra un contesto iniziale e la soddisfazione di un'esigenza. Nel caso della programmazione informatica, il mezzo con il quale si soddisfano le esigenze è un certo tipo di sistema di regole.
La prima cosa da chiarire è che un programma è un'entità che di per sé risolve un'intera classe di problemi. A nostra volta, noi programmatori abbiamo il problema di definire tale classe e individuare le regole che normano il rapporto tra i dati e le soluzioni dei problemi che ne fanno parte.
L'uso del termine classe qui non ha nulla a che vedere con la programmazione a oggetti, anche se immagino che nella programmazione a oggetti l'uso del termine classe faccia riferimento appunto ad una classe di entità che una data classe è in grado di rappresentare. L'omologia tra le classi di problemi reali e le classi dei linguaggi a oggetti è proprio uno dei punti di forza dei design pattern basati su OOP.
Bene, tutto chiaro: ma, concretamente, come facciamo?
Quello che segue è un elenco euristico e personale, non è esaustivo e non sappiamo se abbia riscontri ufficiali nella letteratura accademica, ma è senz'altro un elenco utile di approcci alla risoluzione che abbiamo riscontrato nell'esperienza e che abbiamo adottato e visto adottare un gran numero di volte, in modo consapevole o meno.
Approccio brutale
L'approccio brutale ricorda un po' il metodo induttivo e, nonostante il nome, ti assicuriamo che è una cosa seria. L'approccio brutale è quello utilizzato dagli hacker per trovare falle o password. Questo approccio prescrive, semplicemente, di adottare un qualunque criterio (anche uno aleatorio) atto a fare un numero potenzialmente infinito di tentativi, fino a che viene individuata una soluzione. Un programmatore usa l'approccio brutale quando scrive un test e, poi, inizia a fare tentativi in attesa che il test passi. Il criterio con cui vengono generati i tentativi può essere un criterio creativo o intuitivo che ci porta poi all'approccio intuitivo.
Approccio intuitivo
L'intuito è una piccola magia del cervello umano; si tratta di un approccio brutale integrato nella nostra mente. Il nostro cervello simula in ogni momento varianti della realtà e noi possiamo avvalercene per avere delle cosiddette idee. Niente di più, niente di meno.
Approccio creativo
L'approccio creativo è come quello intuitivo ma fa uso di alcuni elementi aggiuntivi, come il pensiero laterale, cioè l'analisi di un problema attraverso informazioni non direttamente connesse al problema così come viene posto. Generalmente, l'approccio creativo punta ad esplorare in massima parte i dati impliciti ma anche ad applicare modelli eterogenei a quello suggerito dall'intuito.
Parlando dell'applicazione di modelli…
Approccio schematico/strutturale
L'approccio schematico è un po' l'opposto dell'approccio brutale e, in effetti, ricorda molto il metodo deduttivo. Per fare un parallelismo tra approccio schematico/brutale e metodo deduttivo/induttivo, abbiamo usato un approccio schematico: abbiamo applicato un modello astratto, quello della mappa (cioè un'associazione biunivoca tra due insiemi) alle due coppie di metodi matematici e di approcci analitici. A nostro modo di vedere, l'approccio schematico è il più potente che un programmatore ha a disposizione. A pensarci bene, tutti i design pattern e i framework, sono applicazioni di modelli. Comprendere un vasto numero di modelli (pattern) e diventare bravi a scovare relazioni di applicabilità tra i modelli e i problemi specifici è una competenza che un bravo programmatore deve sviluppare.
Approccio euristico
L'approccio euristico è l'anello di congiunzione tra quello brutale e quello schematico, in altre parole è ciò che sta in mezzo tra i tentativi e la creazione di modelli. A tutti gli effetti, senza l'aspetto euristico, l'approccio brutale sarebbe fine a sé stesso e, dunque, non pienamente induttivo poiché il metodo induttivo impone la ricerca di una legge generale.
In generale, si parla di euristiche ogni volta che si riportano delle regole di massima, non strettamente vere o dimostrabili, ma confermate dall'esperienza in un vasto numero di casistiche. L'approccio euristico è quello che guida un'attività di refactoring fatta come si deve: i modelli emergono man mano che il codice cresce e si struttura. A quel punto, tali modelli vanno riconosciuti ed implementati, riorganizzando il codice. Per non essere eccessivamente rigorosi, facciamo rientrare nell'approccio euristico non solo la nascita di nuovi modelli, ma anche l'emersione di un criterio di applicabilità di un pattern già noto ad una data casistica.
Approccio partitivo
Questo approccio prevede la ripartizione di un problema in componenti più semplici, un po' come l'approccio brutale, si può applicare sia alla programmazione, sia nella programmazione.
Vale a dire:
-
da un lato, esiste una vastità di algoritmi basati sul concetto di scomporre un problema in sottoproblemi più semplici dello stesso tipo, arrivare ad una soglia di semplicità desiderabile e poi ricombinare le soluzioni dei sottoproblemi in una soluzione definitiva al problema iniziale;
-
dall'altro, la vastissima maggioranza di tutti i modelli citati all'approccio schematico è votata a scomporre la risoluzione di un problema in una serie di sottoproblemi gestiti da diverse componenti;
-
anche nella progettazione, è buona norma seguire regole di tipo partitivo: se un problema nella sua interezza ha una complessità che richiede livelli di concentrazione insostenibili, la cosa più intelligente da fare è suddividerlo.
Intorno al problema
Le soluzioni
Qualunque sia l'approccio che più si confà alla nostra forma mentis, prima o poi ci ritroveremo con una, nessuna o centomila possibili soluzioni. A questo punto, se non abbiamo ancora le idee chiare su come procedere, dobbiamo, necessariamente, fare un ulteriore passo di lato; entriamo in quello che potremmo definire meta-problem solving. Proviamo a configurare una prospettiva in cui l'esistenza del nostro problema è essa stessa un problema. Possiamo fare a meno di questo problema? Qual è il contesto in cui questo problema nasce? Possiamo chiedere aiuto o negoziare un problema meno complesso?
Qui siamo nell'iperspazio del problem solving: è qui che la maggior parte dei problemi ritenuti irrisolvibili viene risolta. Di solito, questo avviene rimuovendo alla radice le premesse del problema, quindi intervenendo sulla ragione dello stesso. Un altro modo molto laterale per risolvere problemi consiste nel ridurne artificialmente la complessità, facendo assunzioni che fissano alcuni dati mancanti: "in questo specifico caso c'è una soluzione…" In queste situazioni, risolvere almeno un caso su un milione può essere un punto di inizio: magari è possibile persino fare in modo che le altre casistiche non si presentino mai.
Tempo e fatica
Un'altra variabile che non si può ignorare quando si sbatte la testa contro un problema è il tempo. Quando si affronta un problema è bene fissare l'ammontare di tempo e fatica che siamo disposti a spendere su questo problema. Nel caso in cui questi limiti vengano raggiunti, la risoluzione si considera fallita e il problema si considera irrisolvibile (rispetto all'arco temporale dedicato). Un approccio simile è definito tetris strategy, poiché nel gioco del Tetris abbiamo un tempo limitato per:
-
stabilire dove collocare il blocchetto (il termine corretto è tetramino),
-
collocarcelo.
Tale tempo diminuisce man mano che i tetramini scendono sempre più velocemente e, dunque, la nostra fatica aumenta. Quando i nostri limiti in termini di riflessi e velocità d'esecuzione vengono sforati, perdiamo la partita.
Non è una bellissima metafora della vita di un problem solver?
In conclusione, non sappiamo se sia possibile insegnare l'arte di risolvere i problemi, di base è una delle caratteristiche innate del cervello e più che istruirla andrebbe semplicemente esercitata. Pensiamo alla frase: "tu devi esigere che la tua mente comprenda quello che non riesce a comprendere". Di fatto, è così: è lo stress il vero motore della nostra crescita cerebrale.
Siamo dell'avviso che la consapevolezza aiuti la mente a dirigere e correggere i suoi processi. Senza la pretesa di aver distribuito conoscenza o saggezza, speriamo di aver condiviso un po' della consapevolezza accumulata nel tempo.
Non ci resta che augurare buoni problemi a tutti.