Digi Tales

Garibaldi e i linguaggi napoleonici

Mag
18

I programmatori, a differenza degli eroi, sono persone normali, anche se a volte non sembra. Come tutte le persone normali hanno preferenze, fastidi, passioni, fobie. Queste idiosincrasie del tutto umane vengono applicate ai linguaggi di programmazione, agli strumenti per scrivere programmi, agli stili in cui si scrivono. Per questo, forse, sono stati creati così tanti linguaggi di programmazione (circa 8.000).

Una delle discussioni che durano da più tempo è quella sul modo migliore per programmare. In breve, si tratta della metafora generale con cui si pensa al rapporto tra umano e computer durante la programmazione. Ci sono almeno questi quattro modi maggiori:

Napoleonicol’umano ordina e il computer esegue
Aristotelicol’umano definisce regole e fatti e il computer trae le conseguenze e dimostra teoremi
Leibnizianol’umano progetta funzioni e il computer le calcola
Shakespearianol’umano descrive una situazione in cui degli attori che hanno una conoscenza limitata del mondo sanno compiere alcune azioni e interagire tra loro; il computer sovraintende a questa sessione

Il primo modo è quello che si insegna di solito per primo, ed è anche quello che spesso viene usato per definire il significato di “computazionale”: ci si immedesima nel computer e si cerca di descrivere come farebbe a risolvere il problema con le informazioni che ha. Gli altri modi cercano di venire incontro agli umani, al nostro modo di pensare. Il vantaggio di questi altri paradigmi dal punto di vista didattico è che permettono di scrivere un programma in maniera più naturale, descrivendo il problema, anziché la soluzione. Sono i computer a dover avvicinarsi agli umani, non viceversa. E questo vale anche, e soprattutto, quando si fa coding con dei ragazzini.

Snap! permette molti modi (tranne quello Aristotelico), anche se i suoi autori mostrano una preferenza spiccata per il terzo modo, quello Leibniziano. Nota: il nome tradizionale per questi modi è “paradigma”, e di solito si usano etichette un po’ meno fantasiose per indicarli. Ma qui non stiamo facendo un corso di informatica.

Siccome queste sono parole un po’ astratte, facciamo qualche esempio.

Il contesto è la canzoncina “Garibaldi fu ferito” che una volta i bambini sapevano a memoria e cantavano sull’aria della Canzone dei Bersaglieri. Il gioco era quello di sostituire tutte le vocali delle parole con una sola, ottenendo ad esempio “Garabalda fa farata” oppure “Gurubuldu fu furutu”, eccetera. I bambini sanno farlo, anche se non sanno esattamente dire come. Prima di continuare potete provare anche voi, e interrogarvi su come avete fatto.

Come facciamo per far fare la stessa operazione ad un computer? Per quello che abbiamo detto sopra, non c’è un solo modo, né un modo “giusto” (questo è uno dei motivi per cui fare coding ha un senso forte solo se non ci si limita a ricopiare tutorial e eseguire esercizi). Possiamo provare a seguire almeno tre strade diverse, per poi magari valutarne la semplicità, l’utilità in termini didattici, e se fossimo informatici anche l’efficienza.

I blocchetti realizzati in Snap! delle varie versioni potete vederli e provarli direttamente da qui:

https://snap.berkeley.edu/project?user=stefano63&project=Garibaldi%20fu%20ferito

Prima versione, imperativa

  1. prendi il testo della canzoncina
  2. conta la lunghezza del testo il lettere (sono 90)
  3. prepara una variabile – vuota – dove andrà a finire la versione trasformata
  4. prepara una variabile che servirà a contare le lettere e mettici dentro 1
  5. ripeti novanta volte:
    1. prendi la lettera I del testo
    2. se è una vocale, sostituiscila con la A, e mettila all’inizio della variabile finale
    3. altrimenti, metti la lettera originale all’inizio della variabile finale
    4. aumenta I di 1
  6. quando hai finito, restituisci la versione trasformata

La parte da 5.2 a 5.3 può essere affidata ad una funzione a parte (l’abbiamo chiamata “cambia vocale con…”) per evitare di rendere i blocchetti troppo complicati da leggere. E’ una funzione molto semplice, che si limita a verificare che una lettera sia una di queste: a,e,i,o,u. Non tiene conto delle maiuscole né delle vocali accentate.

Seconda versione, ricorsiva

Un’altra versione possibile è quella che sfrutta una caratteristica di alcuni linguaggi di programmazione, cioè la possibilità di richiamare una funzione al suo stesso interno. In pratica, si usa la tecnica di dividere un problema in sotto-problemi sempre più piccoli finché non si arriva ad un problema risolvibile.

In questo caso, sappiamo come trasformare una lettera (con la funzione “cambia vocale con”) ma non sappiamo come trasformare un intero testo.

  1. se la frase ha lunghezza 1 (cioè è se è composta da una sola lettera) si applica la funzione “cambia vocale con” e si restituisce il risultato (questo è il passo in cui sappiamo come risolvere il problema);
  2. altrimenti, si chiama di nuovo la stessa funzione “cambia frase” ma passando due valori diversi:
  • la prima lettera della frase
  • tutto il resto della frase
  1. si unisce il risultato che proviene da queste due funzioni e lo si restituisce

Una volta superato lo shock di un programma che non si capisce come fa a funzionare, la soluzione è di una semplicità imbarazzante.

Terza versione, funzionale

In questo caso si sfruttano due caratteristiche di Snap!, tipiche dei linguaggi funzionali:

  • la possibilità di applicare una funzione su tutti gli elementi di una lista, uno per uno, ottenendo una nuova lista
  • la possibilità di ridurre una lista ad un solo elemento, applicando un’operazione agli elementi, due alla volta.

Anche in questo caso l’algoritmo è molto semplice ed è basato su una concatenazione di funzioni:

  1. si applica la funzione “cambia vocale ” alla lista ottenuta separando la frase ad ogni lettera
  2. si combinano gli elementi di questa lista in una nuova frase e la si restituisce

Preferenze per uno dei tre? Quale vi sembra più chiaro? Quale vi sta più simpatico?

A me, personalmente, l’ultimo. Proprio perché non si perde a misurare, a tenere il conto del punto in cui siamo arrivati, non ha bisogno di appoggiare il risultato parziale da qualche parte. In fondo non fa altro che trasformare in blocchetti quello che volevamo fare: applicare una trasformazione a tutte le vocali di un testo.

A proposito: che ne è di quel famoso adagio “I computer sono stupidi, sanno fare solo una cosa e la fanno sempre nello stesso modo”?

Pensiero computazionale sì o no? Boh, dipende dal linguaggio.

Gen
27

Una delle motivazioni dietro la spinta all’introduzione della programmazione dei computer in giovane età (=coding) è quella per cui questa pratica insegna il pensiero computazionale, che è un modo di affrontare i problemi in maniera scientifica. Anche se Jeannette Wing si è affannata a dire che non si tratta di insegnare ai bambini a pensare come i computer, e Seymour Papert a predicare che bisogna insegnare ai computer come se fossero bambini, questo tema resta nell’aria. Computazionale significa “razionale, finito, corretto, misurabile” eccetera. In pratica, imparare il pensiero computazionale significa, per ogni problema, saper immaginare un algoritmo che lo risolva. Questo algoritmo deve essere eseguibile praticamente da un computer: non deve contenere errori, non deve basarsi su termini ambigui, non deve richiedere un tempo infinito o una memoria infinita per arrivare alla soluzione.

Però gli algoritmi si pensano (e poi si scrivono) diversamente a seconda del linguaggio, e a seconda del tipo di linguaggio che si ha in mente. Usare un linguaggio semplice quando si è ai primi passi è sicuramente una buona idea; ma cosa significa “semplice”? Può significare che costringe ad usare dei concetti e delle operazioni di base, vicine a quelle che sa fare il computer; oppure che spinge a nascondere i dettagli e a concentrarsi “semplicemente” sulla struttura dei dati e sugli obiettivi.

Insegnare ai bambini una forma di pensiero computazionale che li abitua a pensare nei termini del primo significato di “semplicità” rischia di creare più problemi – in futuro – di quelli che risolva adesso.

Voglio provare a spiegare questa differenza con un piccolo esempio.

Partiamo da Snap!, un ambiente di programmazione che pochi conoscono, che assomiglia molto a Scratch (ne è derivato) ma ha delle differenze importanti. Intanto è nato all’Università di Berkeley, dall’altra parte degli USA rispetto a Stanford, dove è nato Scratch. Poi ha un target più ampio, nel senso che può essere usato da bambini ma anche da ragazzi più grandi, perché è basato su un modello di linguaggio più potente di quello che sta sotto Scratch. Le differenze tra Snap! e Scratch sono poco visibili ad un primo approccio (si possono disegnare girandole o creare scenette animate con entrambi), ma sono molto profonde. Ne elenco solo tre fondamentali:

  1. A differenza di Scratch, in Snap! si possono creare funzioni, cioè procedure che alla fine restituiscono un valore, e questo è molto comodo perché permette di costruire delle catene di funzioni che si applicano ai risultati di altre funzioni.
  2. In secondo luogo, in Snap! ci sono delle funzioni per creare e gestire vere liste (di numeri, di lettere, di parole, ma anche di sprite, o di qualsiasi oggetto).
  3. Inoltre le funzioni possono essere usate come oggetti da altre funzioni. In altre parole: una funzione si può applicare non solo a oggetti semplici (come numeri, lettere) ma anche ad altre funzioni. In aritmetica conosciamo questo tipo di cose: la moltiplicazione è l’applicazione ripetuta della funzione somma.

Il terzo punto è particolarmente utile quando si vogliono trattare delle serie di dati. Se vogliamo trasformare una lista di numeri, o di lettere, o di parole, in qualche altra cosa dobbiamo applicare una trasformazione ad ognuno degli elementi della lista per ottenere una nuova lista; avere un linguaggio che permetta di fare direttamente questo tipo di operazioni permette di concentrarsi sul problema, e non sul meccanismo delle soluzione.

Mettiamo, per esempio, di voler generare la lista dei quadrati dei primi 10 numeri interi. Partendo da una lista di numeri interi, vogliamo moltiplicare ogni numero per se stesso. Cioè da 1,2,3,4,5… vogliamo ottenere 1,4,9,16,25 …

Questo in Scratch si può fare così:

  • crea una lista di numeri interi e dagli il nome “numeri”
  • crea una seconda lista e dagli un nome “quadrati”
  • crea un contatore e dagli il nome “i”, e valore 0
  • ripeti tante volte quanto è la lunghezza della lista “numeri”:
    • aumenta “i” di 1
    • moltiplica l’elemento i della lista “numeri” per se stesso
    • aggiungi il risultato alla lista “quadrati”

Saltando la parte di creazione delle variabili, ecco i blocchetti:

E’ semplice, sì, ma è anche un modo di pensare molto meccanico, che segue esattamente quello che succede “sotto il cofano”. Siamo noi che ci adeguiamo al modo di lavorare del computer, e non viceversa. Arrivare a questa formulazione non è banale: bisogna imparare a pensare in un certo modo. Bisogna imparare il concetto di contatore, di indice di una lista, quello di ciclo, di test di terminazione, eccetera. Sono gli elementi di base della programmazione classica.

Proprio questo è uno dei limiti dell’approccio “computazionale” di cui parlavamo sopra: si impara a pensare come un computer, o meglio secondo gli schemi che il linguaggio di programmazione che usiamo (in questo caso Scratch) mette a disposizione.

Da un lato questo apprendimento ha degli aspetti positivi: ogni volta che ci poniamo un limite impariamo nuovi modi di risolvere problemi. Dall’altro, restare dentro quei limiti non ci permette di andare molto lontano. Potrebbe esserci (non è mai stato studiato a fondo) una forma di imprinting per cui il primo modello di programmazione imparato resta quello fondamentale, un po’ come la lingua madre. Se i bambini imparano a usare i cicli per scorrere le liste, poi continueranno a farsi venire in mente questa modalità anche in futuro.

Ma usare i cicli è solo uno dei possibili modi di risolvere il problema dei quadrati. Ce ne sono altri, che dipendono dal linguaggio che si usa.

In Snap! si possono disporre i blocchetti esattamente nello stesso modo che in Scratch:

Ma si può fare anche qualcosa di diverso, più semplice e soprattutto più vicino alla struttura del problema.

Per risolvere il problema dei quadrati dei numeri interi non siamo costretti a pensare in termini di contatori e di indici, non dobbiamo per forza preoccuparci di sapere quando fermare il processo, dove mettere i risultati. Queste sono cose che riguardano la meccanica dell’algoritmo, non il problema in sé.

Qual era la cosa che volevamo fare, l’idea iniziale? Applicare la funzione “moltiplica per se stesso” a tutti gli elementi di una lista. Bene, allora cominciamo dalla lista

Questo blocchetto è una funzione che restituisce una lista costruita con gli oggetti che mettiamo negli spazi bianchi, in questo caso i primi cinque numeri interi. Non abbiamo bisogno di creare una variabile perché la funzione restituisce sempre quello che ci serve. Ora possiamo disporre i blocchetti in modo da rispettare esattamente l’idea originale di applicare la moltiplicazione alla lista:

Finito. Non c’è altro da fare. Questo blocchetto (“applica”) fa da solo tutto il lavoro: prende il primo elemento della lista, lo inserisce nei due buchi del blocchetto verde, esegue la moltiplicazione e mette da parte il risultato, poi passa al successivo. Alla fine restituisce una nuova lista con i risultati.

La presenza del blocchetto “applica” è una delle caratteristiche di Snap! (e dei linguaggi funzionali a cui si ispira) a cui facevamo riferimento prima. E’ una funzione che lavora su altre funzioni. Usando questo approccio, non ci servono variabili, contatori, cicli. Per certi versi, è molto più intuitivo e vicino al problema questo approccio di quello precedente, in cui dovevamo introdurre una serie di concetti di supporto.


In Snap! c’è un’altra maniera di ottenere questo risultato, ancora più semplice di quella che abbiamo visto adesso. Torniamo all’idea di partenza: “moltiplicare ogni numero di una lista per se stesso”. Ma invece di moltiplicare ogni numero per se stesso, possiamo partire da due liste identiche, e moltiplicare ogni elemento della prima lista per il corrispondente elemento della seconda.

Proviamo a fare esattamente questo:

Il risultato è lo stesso. In sostanza, l’operazione “moltiplicazione” normalmente si applica a dei numeri; ma in Snap! è stata estesa per applicarsi anche a delle liste. Naturalmente potremmo pensare di utilizzarla anche per moltiplicare due liste diverse, o per moltiplicare una lista per un numero singolo. Forse funziona anche con altre operazioni? Proviamo con l’operazione “elevamento a potenza”:

Funziona. Ed è abbastanza chiaro che è molto più semplice di tutto l’ambaradam che avevamo dovuto mettere in campo prima. Questa modalità ci libera di tutta la parte meccanica e ci spinge invece verso una maniera diversa di concepire il problema che volevamo risolvere, che a sua volta ci apre la strada verso altre possibilità.

In conclusione: attenzione al linguaggio che si sceglie, e non confondiamo la razionalità con la meccanicità.