The joy of programming

In questo secondo articolo (questo è il primo) dedicato ai linguaggi di programmazione parleremo di concetti come gioia, divertimento, serenità, estetica, pace mentale …

L’idea è di fare una carrellata delle features introdotte dai linguaggi moderni, e vorrei iniziare da quelle che mi sembrano più importanti: quelle che aiutano a creare e mantenere uno stato mentale adatto e a permettere quindi all’inconscio di fare le sue magie.

Photo by Dids ddd on Unsplash

« … a casa ho un libretto di istruzioni che apre grandi prospettive al miglioramento della prosa tecnica. Comincia così: “Il montaggio della bicicletta giapponese richiede una grande pace mentale”».

«Provate a osservare un apprendista o un operaio scadente e paragonate la sua espressione a quella di un artigiano di prim’ordine e vedrete la differenza. L’artigiano non si attiene mai alle istruzioni. Decide man mano quel che deve fare; sarà concentrato e attento senza il minimo sforzo. I suoi movimenti e la macchina sono come in sintonia. È la natura della materia su cui lavora a determinare i suoi pensieri e i suoi movimenti, e questi, a loro volta, cambiano la natura della materia. La materia e i pensieri dell’artigiano si trasformano insieme, cambiando gradualmente, fino al momento in cui la mente è in quiete e la materia ha trovato la sua forma».

Pirsig, Robert M.. Lo Zen e l’arte della manutenzione della motocicletta

Espressività, pulizia sintattica

Provo a raggruppare qui le features che hanno a che fare con l’espressività: quelle che permettono di esprimere concetti con meno rumore possibile.

Il legame con la pace mentale di cui sopra è che meno dettagli dobbiamo tenere in mente più il nostro cervello lavora bene. Quanto maggiore è la sintesi che il linguaggio permette, e minore è il boilerplate 1, tanto maggior è la possibilità di concentrarsi sul problema da risolvere.

String interpolation

Costruire una stringa inserendo il risultato di un espressione. Ad esempio questo codice “C”

int quanti = 3 * 7 * 2 ; 
char buff[200]; 
sprintf(buff, "Essi sono %d", quanti);

diventa in Ruby

"Essi sono  #{ 3 * 7 * 2 }."

Ruby è stato il primo ad introdurre questa feature, ora è presente ad esempio in CoffeeScript, JavaScript, Elixir, Groovy, Kotlin, Nim, Swift, Crystal.

No semicolon, No return statement

Può non sembrare una gran cosa, ma non avere la necessità di chiudere ogni linea di codice con un terminatore (in genere il punto e virgola), o di usare esplicitamente lo statement return ,rende i programmi più leggibili. Molti dei nuovi linguaggi vogliono il semicolon solo per separare due espressioni sulla stessa linea, e il valore ritornato da una funzione è generalmente l’ultima espressione calcolata.

Tuple

Un tipo di dato ormai presente in tutti i linguaggi nati in questo secolo e qualcuno di quello precedente: sono liste di lunghezza finita i cui elementi possono essere di tipi diversi. Permettono, tra le altre cose, di avere funzioni che ritornano valori multipli (cosa che in linguaggi più datati richiede un discreto lavoro e porta a programmi meno leggibili).

Un esempio in Rust:

fn main() {
    let x = (2, 3);
    let y = swap(&x);
    println!("Hello, world! {} {}", y.0, y.1);
}

fn swap(x:&(i32, i32)) -> (i32, i32) {
    (x.1, x.0)
}

// produce: Hello, world! 3 2

Rust quanto a leggibilità non eccelle, ma ha altre virtù.

Pattern Matching / Destruction

Questa è una delle più significative, permettere di smontare strutture dati dichiarando un set di variabili locali che assume i valori dei corrispondenti campi della struttura originale. Si può usare nelle assegnazioni, nei parametri di una funzione o in alcune strutture di controllo (if, case/switch).

Nell’esempio sotto un tuple literal in Elixir viene creato e poi scomposto o nei suoi valori.

{:ok, result} = {:ok, "good"}
# la variabile result a questo punto vale "good"

pippo = case {:ok, "bene"} do
              	{:ok, result} -> "Molto " <> result 
           	{:error, why} -> "Bad " <> why
                _ -> "Nothing matched"
         end
# pippo vale "Molto Bene", l'operatore <> concatena due stringhe, vedete che il case ha riconosciuto lo schema della tupla e ha estratto nella variabile result il secondo elemento.
# La variabile _ significa don't care.

Per quelli-che-sanno: lo so che Elixir non ha variabili 2

Questa feature è presente in parecchi linguaggi con sintassi diverse, ma Elixir qui eccelle. La sintesi che questo tipo di notazione raggiunge, è notevole. Il codice sopra, riscritto in “C” potrebbe, ad esempio, diventare:

enum flag {OK, ERROR};
typedef struct _T {
	flag res;
	char * result;
} Pair;
Pair stuff = {OK, "good"};
char * result;

if (stuff.flag == OK) {
	result = stuff.result;
} else {
	exit(1);
}

char pippo[300];
Pair stuff2 = {OK, "bene"}
if (stuff2.flag == OK) 
	sprintf(pippo, "%s %s", "Molto", stuff2.result);
else if (stuff2.flag == ERROR)
	strcpy(pippo, "Bad"
else
	strcpy(pippo, Nothing matched)

Enum con valori

Il miglioramento qui è permettere di dichiarare enumerativi che contengono dati. Anche questa è stata introdotta in parecchi linguaggi.

Al posto di

enum DayFitness {SVEGLIA, COLAZIONE, CORSA, WORKOUT}

posso dire

enum DayFitness {Sveglia(ora: DateTime), Colazione(calorie: i32), Corsa(km: f32), Workout(minuti: i32}

Di fatto funziona un po’ come le union del C: con l’aggiunta che l’enum dice anche qual’è l’assetto corrente della union.

I linguaggi che hanno questo tipo di enum le combinano col pattern-matching, per cui, sempre in Rust posso scrivere:

let oggi:DayFitness = Corsa(20.0);
match oggi {
        Corsa(km) if km > 0.5 => 
		println!("Naaa, {} km di corsa oggi te li fai te !", km),
        Corsa(km) => 
		println!("Dai che un po' di attività fisica fa sempre bene"),
	_ => println!("non dovevamo andare a correre ?")
    }

e ottenere un secco rifiuto.

Closures

Una volta qualcuno ha detto che le closures erano le classi dei poveri, e gli hanno risposto che erano le classi a essere le closures dei poveri.

Sono uno strumento espressivo straordinario. Nascono col Lisp, sono tipiche di un approccio funzionale, si tratta infatti di assegnare una funzione a una variabile, ci hanno messo un po’ ad affermarsi, ma ora le hanno praticamente tutti i linguaggi moderni.

Un piccolo esempio in JavaScript, tanto per cambiare:

<!DOCTYPE html>
<html>
<body>

<button id="Click" type="button">Click Me!</button>
<button id="Increment" type="button">Click Inc!</button>

<p id="demo"></p>

<script>
function buildClosures() {
  var a = 4;
  
  myF = () => {
    document.getElementById("demo").innerHTML = a * a;
  } 
  incF = () => {
    a = a + 1;
  } 
return [myF, incF]
}
data = buildClosures()
myFunction = data[0]
incFunction = data[1]
document.getElementById("Click").onclick = myFunction
document.getElementById("Increment").onclick = incFunction
</script>

</body>

La pagina che viene fuori da questo HTML (potete caricarlo in un browser per provare) crea due bottoni. Premendo il primo viene visualizzato il quadrato della variabile a (inizialmente 16). Ogni volta che si preme il secondo bottone la variabile a viene incrementata di 1. Alternando quindi il click sui due bottoni vedrò la sequenza 16, 25, 36, 49, 64 …

La funzione buildClojures, che viene chiamata al caricamento della pagina ritorna due closures (myF e incF) che vengono assegnate ai due bottoni, verranno eseguite ad ogni click.

Guardate dentro a buildClosures. La variabile a, inizializzata a 4, è una variabile locale, esiste solo finché la funzione buildClosures è in esecuzione, eppure viene usata/modificata dalle due funzioni. Le due funzioni sopravvivono alla fine della buildClosures (essendo depositate nei bottoni), e quindi continuano a usare la variabile a anche quando questa è uscita di scena.

Questo ritornare un puntatore a una variabile locale sarebbe stato un peccato mortale in “C”, per chi c’è passato, diventa invece qui un valore. Una closure cattura le variabile a cui accede, assieme al loro valore e le conserva in un posto sicuro.

E’ notevole, tra l’altro, che siano state implementate anche in linguaggi statici e compilati, come Rust, Go e Crystal.

Easy thread creation syntax / Green threads

E quale uso migliore di una closure si può trovare che farne il corpo di un thread ?

La sintassi con cui questi nuovi linguaggi permettono la creazione di thread è sempre molto limpida. In genere è della forma

thread_id = spawn(closure)

esegui questa closure in un thread separato.

Ma parlando di thread non possiamo tralasciare un accenno ai green thread o light thread. Il problema è quello di voler usare molti thread per esigenze espressive del programma, ma non voler usare troppi thread veri (quelli offerti dal sistema operativo) che sono risorsa preziosa. I green thread sono la risposta a questo: il runtime (o la virtual machine) del linguaggio alloca un pool di thread veri e permette al programma di crearne molti di più di tipo green, gestendo lui (il runtime/VM) la concorrenza interna e allocando con uno scheduling l’esecuzione dei thread green su quelli veri.

What’s next

Nella prossima puntata parleremo delle features più caratteristiche dell’approccio funzionale (ma che ormai in buona parte sono presenti in molti linguaggi non rigorosamente functional). Sono features che considero a metà tra espressività e robustezza/preformance del programma.

In quella dopo pensavo di parlare delle features che hanno più a che vedere con l’organizzazione del codice, gestione della macro-complessità anziché della micro (l’espressività appartiene a quest’ultima categoria).

E, infine, vorrei fare una piccola passerella dei linguaggi che a me sembrano più significativi: quelli che bisogna assolutamente imparare quest’anno 😜.

  1. Viene chiamato così l’insieme di istruzioni che devo inserire nel programma per far contento il linguaggio, non finalizzate al mio lavoro.
  1. Per i curiosi, a questo punto. In Elixir non ci sono assegnazioni: i dati sono assolutamente immutabili, ne parleremo la prossima volta. Quell’uguale è ancora un pattern-matching, sta dicendo: “Se il primo parametro della tupla è :ok associa al simbolo ‘result’ il valore del secondo parametro, altrimenti fai crashare il programma. Che sembra drastico, ma Elixir ha una filosofia di eutanasia rapida che prescrive di far morire un task alla prima anomalia (in genere si tratta di green thread il cui lifecycle è sotto il controllo del programma). Per errori previsti il pattern-matching si fa in un case come mostrato nell’esempio.

Rispondi