What if?

Nell’ultima lezione [1] avevo accennato ad uno dei modi in cui i computer prendono decisioni. Mi auguro che questo non abbia alimentato speranze fuori luogo perché il genere di decisioni di cui stiamo parlando non è della stessa specie — in senso in senso linneiano, proprio — della scelta dei vestiti alla mattina, bensì della più semplice scelta di alternative predefinite in base alle condizioni nel momento in cui si effettua la scelta. Ammetto che non sembra molto diverso ma credetemi: in informatica c’è un abisso tra le due cose, e l’altra sponda si chiama intelligenza artificiale [2].

Tornando a noi, siccome so che siete impazienti e vi stufate di leggere tanta roba, tuffiamoci direttamente dentro l’ultima parte della cosiddetta programmazione imperativa [3] e vediamo la prima delle strutture di controllo, la cosiddetta if…else [4]. Recuperando l’esempio della volta scorsa, avevamo

if(distance < 100.0f) {
  r = r + (100.0f - distance)/10.0f;
}

che, se ricordate, aumentava il raggio dei cerchietti solo se questi erano più vicini di 100 unità alla posizione del mouse. A vederlo così però sembra che manchi qualcosa, e infatti la forma generale del costrutto però è un po’ più complessa:

if(condition) {
  doSomething();
} else {
  doOtherwise();
}

che si legge “se (if) la condizione è verificata allora (questo sarebbe il then) esegui il primo blocco, altrimenti (else) esegui il secondo blocco. L’esempio più classico è probabilmente quello in cui l’utente inserisce la propria età e

if(age >= 18) {
  println("Puoi prendere la patente per l'auto!");
} else {
  println("Non puoi prendere la patente per l'auto!");
}

che è un esempio scemo ma rende l’idea. Già che stiamo parlando di strutture di controllo, parliamo delle condizioni che le fanno funzionare.

Una condizione è un’espressione di logica booleana che può essere vera oppure falsa, mai entrambe insieme e mai una terza opzione, che detta così sembra abbastanza noiosa ma riesce ugualmente a incasinare la vita in modi sorprendentemente originali e bizzarri. L’algebra di Boole è un argomento esageratamente vasto considerando l’uso che se ne fa in programmazione, per cui ne vedremo qualche principio fondamentale e per il resto ci ragioneremo se e quando sarà il caso. Tanto per cominciare, l’algebra di Boole lavora con due valori: vero e falso, che in inglese si scrivono true e false, si abbreviano T e F e quando si viene ai computer solitamente si esprimono in termini di 1 e 0, ma fortunatamente in Java, e quindi in Processing, possiamo usare le parole chiave true e false, anche se in effetti le useremo poco.

Come ogni algebra che si rispetti, anche quella di Boole introduce delle operazioni che, sebbene alcune possano ricordarle, non hanno quasi niente a che fare con le quattro operazioni che si studiano alle elementari. Per fortuna però sono ugualmente facili da imparare anche perché a volte le usiamo senza accorgercene nei discorsi e nei ragionamenti quotidiani — in teoria, perché poi in pratica…

Le prime tre operazioni si chiamano congiunzione, disgiunzione e negazione, parole un po’ spaventose finché non le associamo con le particelle e, o e non del discorso, con una piccola precisazione per la disgiunzione, ma ogni cosa a suo tempo.

La congiunzione produce il valore vero quando entrambi gli operandi (le espressioni logiche, in questo caso) sono vere, e falso in tutti gli altri casi. In inglese la congiunzione si dice and e il simbolo che si usa in programmazione è &&, (sì, due). A questo punto potremmo riscrivere l’esempio sopra come

if((age >= 18) && hasLicense()) {
  println("Puoi guidare l'auto!");
} else {
  println("Non puoi guidare l'auto!");
}

che rafforza la condizione sull’età richiedendo che l’utente abbia anche la patente, mediante una ipotetica funzione hasLicense() che restituisce vero se l’utente ha la patente e falso altrimenti [5].

La disgiunzione in inglese si dice or ma si comporta un po’ diversamente da come siamo abituati ad usarla quotidianamente, infatti produce il valore vero quando almeno uno degli operandi è vero, mentre quando diciamo “mangi la pasta o la pizza” implicitamente intendiamo che se mangiamo una cosa, non mangiamo l’altra. Ovviamente tutto ciò ha una spiegazione tecnica noiosissima e quindi limitiamoci a ricordare che il simbolo che si usa in programmazione è || (che sono due di quelle sbarre verticali che sulla tastiera italiana stanno sopra il tasto \ e sotto l’Esc) e procedere.

La negazione è, a differenza dei due precedenti, un operatore “unario”, cioè che richiede un solo operando, e il suo effetto è invertirne il valore, cioè se è vero lo trasforma in falso e viceversa. Il simbolo che si usa è il ! e lo mettiamo prima dell’operando che vogliamo invertire, tipo !hasLicense() ci dirà vero se l’utente non ha la patente.

Come ogni algebra che si rispetti, anche quella di Boole ha le sue regole per le operazioni che ci permettono di costruire espressioni complesse legando molte condizioni insieme:

  • la proprietà commutativa, se ci ricordiamo qualcosa dalle elementari, dice che cambiando l’ordine degli operandi, il risultato non cambia, e infatti è vero per entrambe le operazioni binarie [6] che a && b = b && a e a || b = b || a, e questo non dovrebbe sorprendere nessuno;
  • la proprietà associativa, sempre se ci ricordiamo qualcosa dalle elementari, riguarda le parentesi, o per meglio dire l’ordine in cui eseguiamo le operazioni. Dato che le operazioni binarie vogliono due operandi, un’espressione tipo a && b && c non avrebbe alcun senso se non potessimo dire che (a && b) && c = a && (b && c), ovvero che facciamo prima la and tra a e b e poi tra questo risultato e c, oppure che facciamo prima la and tra b e c e poi tra questo risultato e a, non cambia niente. Il giochino vale anche con la or, ovviamente;
  • la proprietà distributiva è quella che ci dice cosa succede quando mischiamo parentesi e operatori, che poi significa che a && (b || c) = (a && b) || (a && c) e a || (b && c) = (a || b) && (a || c) che ricorda un po’ quella coi numeri per cui $a(b+c) = ab + ac$ ma la somiglianza si ferma qui [7].

Ci sono altre due proprietà che ci aiutano a semplificare le nostre espressioni e scovarci eventuali bug, e sono

  • la proprietà di assorbimento dice che a || (a && b) = a e che a && (a || b) = a che se ci ragionate un attimo è quasi evidente e ci permette di semplificare notevolmente;
  • la proprietà dei complementi dice che a || !a = true e a && !a = false che è ancora più evidente della proprietà precedente;

e con queste abbiamo (quasi) tutto quello che ci serve per affrontare le espressioni booleane [8]. Quello che ci manca è sapere cosa sono queste espressioni. Certamente quelle che abbiamo scritto fin’ora sono espressioni, come quando alle medie abbiamo studiato le espressioni in matematica, ma se ci ricordiamo qualcosa dell’algebra delle medie, l’espressione più semplice è quella formata da un solo simbolo, quindi in generale un’espressione è tutto ciò che equivale ad un valore vero o falso, per cui gli stessi valori true e false, oppure variabili di tipo booleano (il nome del tipo è bool) che possono assumere solo valore vero o falso, oppure addirittura funzioni che restituiscono valori booleani, che per esempio potrebbero essere scritte così:

bool hasLicense();

o ancora operazioni di confronto, e tutti i precedenti mischiati insieme attraverso le operazioni che abbiamo visto fin’ora. Ora, cos’è un’operazione di confronto? È un’operazione binaria che confronta due cose tra loro e ci dice se sono uguali, se sono diverse e se una è più grande dell’altra o più piccola. I simboli che si usano sono simili a quelli che si usano in matematica, con alcune ovvie eccezioni grafiche:

  • l’uguaglianza in matematica si esprime con il simbolo = ma noi questo simbolo l’abbiamo già usato per assegnare valori alle variabili, e quindi quello che si usa è == che vi conviene leggerlo come “è proprio uguale”;
  • la disuguaglianza in matematica si esprime coi simboli $\lt, \leq, \gt$ e $\geq$ ma siccome su una tastiera americana standard non sono facilmente accessibili, si usano i simboli <, <=, > e >=. La disuguaglianza generica, o logica, cioè quella che ci dice se due elementi sono diversi senza specificare oltre, in matematica si scriverebbe $\neq$ e in informatica si fa qualcosa di simile, cioè si nega il simbolo di uguaglianza, che però per semplificare si riduce ad un uguale solo, e quindi != [9]. Occhio che non è possibile scrivere !< per intendere >=, anche se ammetto che sarebbe divertente :)

A questo punto è ora di divertirsi. Riprendiamo il codice che abbiamo scritto la volta scorsa e aggiungiamo

color bgColor;

tra le dichiarazioni in testa, prima della funzione setup() che a sua volta modificheremo aggiungendo al suo interno

bgColor = color(0, 0, 0);

per avere un colore di partenza, in questo caso il nero, poi modifichiamo una riga nella funzione draw()

background(bgColor);

e infine aggiungiamo la nuova funzione

void keyPressed() {
  if (key == 'r') {
    bgColor = color(0, 100, 25);
  } else if(key == 'g') {
    bgColor = color(90, 100, 25);
  } else if(key == 'b') {
    bgColor = color(240, 100, 25);
  } else if(key == 'k') {
    bgColor = color(0, 0, 0);
  }
}

che è una di quelle funzioni speciali di Processing, alla stregua delle varie setup() e draw() che abbiamo già visto, che verrà chiamata ogni volta che premeremo un tasto sulla tastiera, e il tasto in questione verrà salvato nella variabile speciale key (un’altra di quelle variabili che ci fornisce Processing tipo la posizione del mouse) che è di tipo char, cioè memorizza un solo carattere. Quello che vediamo qui è una cascata di if…else in cui controlliamo di volta in volta quale tasto abbiamo premuto e cambia il colore di conseguenza. Avviamo il programma e proviamo a premere i tasti r, g, b e k… fico, eh? Piccola nota: abbiamo visto che dopo un else ci va un blocco, ma se invece mettiamo un nuovo if non servono le parentesi graffe perché l’if costituisce blocco a sé [10].

In alternativa a questa cascata di if, in alcuni casi particolari potrebbe essere più gradevole da leggere la struttura di controllo switch che funziona più o meno così:

void keyPressed() {
  switch(key) {
    case 'r':
      bgColor = color(0, 100, 25);
      break;
    case 'g':
      bgColor = color(90, 100, 25);
      break;
    case 'b':
      bgColor = color(240, 100, 25);
      break;
    case 'k':
      bgColor = color(0, 0, 0);
      break;
    default:
      break;
  }
}

e i casi particolari sono quelli in cui il confronto da fare è una uguaglianza (gli altri operatori non funzionano) e i valori da confrontare sono int o char [11]. Ogni confronto corrisponde ad una clausola case seguita dal valore corrispondente e i due punti. Nel caso la nostra variabile key per esempio contenesse r, verrebbero eseguite le istruzioni immediatamente successive, e la particolarità dello switch è che, se non ci mettiamo un break alla fine di ogni case, vengono eseguite anche tutte le istruzioni dei case successivi, finché non viene incontrato un break, che ha l’effetto di interrompere, uscire dallo switch e riprendere l’esecuzione dalla prima istruzione che lo segue. Suona un po’ incasinato, e all’inizio lo sarà, ma ha i suoi pregi che impareremo ad apprezzare quando verrà il momento. Per ora ricordiamoci di mettere anche il caso default seguito da un break che in teoria non serve ma per non sbagliare mettiamocelo pure lì. Ah, un’ultima precisazione che vale per i char in generale: si usa l’apice singolo per differenziarli dalle stringhe che usano gli apici doppi, e il motivo è squisitamente tecnico e non vale la pena spiegarvelo [12].

Questa volta non ci siamo divertiti tantissimo, è vero, e non ho neanche messo i consueti disegnini, ma questa è anche l’ultima lezione dei fondamentali e quindi dalla prossima cominceremo a vedere cose molto più incasinate, molto più interessanti, e indubbiamente molto più divertenti. Promesso.

  1. Dieci giorni fa, lo so…
  2. Senza contare il buon gusto, cosa di cui i computer in ogni caso sono sprovvisti.
  3. Tecnicamente stiamo affrontando un ibrido di imperativa e procedurale, ma francamente sia questa distinzione che l’intera classificazione ha poco senso, quindi facciamo finta di niente e tiriamo dritto.
  4. Alcuni amano chiamarla if…then…else e in teoria sarebbe più corretto in senso logico, ma in Java la parola chiave “then” non si usa e quindi… oh, vabbè, tanto alla fine lo chiamiamo tutti if e basta :)
  5. Aggiungere parentesi non fa alcun male e aiuta a capire meglio quello che scriviamo, senza contare che in alcuni casi è necessario, come vedremo più avanti.
  6. Ci tengo a ricordare, nel caso qualcuno fosse tratto in inganno, che queste non si chiamano “operazioni binarie” perché lavorano coi valori falso e vero, o 0 e 1, che in cultura popolare sono universalmente noti come “sistema binario” — che anche qui coi nomi… vabbè — ma perché operano su due operandi, come le quattro operazioni sui numeri decimali che si imparano alle elementari, mentre la negazione si chiama “operazione unaria” perché vuole un solo operando.
  7. Non è vero, ma per non fare confusione fermiamoci qui.
  8. Il resto, se siete curiosi o vi trovare incasinati da impazzire, potete leggerlo su Wikipedia.
  9. In alcuni linguaggi tipo BASIC può capitare di trovare <> per la disuguaglianza ma vabbè, ve lo dico solo per curiosità.
  10. La regola è che se mettiamo un’istruzione di una sola riga dopo l’if o dopo l’else, non servono le parentesi graffe, e in questo caso l’if viene interpretato come uno statement unico, quindi è come se fosse una riga sola… vabè, tecnicismi.
  11. Non è del tutto vero ma per il momento va bene così.
  12. Ma siccome so che siete nerd nel profondo, il motivo è che se scriviamo "r", il compilatore occuperà non uno ma due spazi, uno per il carattere r e uno per infilarci uno zero che è storicamente l’indicatore che la stringa è finita, mentre se scriviamo 'r' il compilatore occuperà un solo spazio per metterci la r perché sa che sta ragionando su char che indica un solo carattere e quindi non ha bisogno di indicarne la fine.

Commenti

gianduja » 

Moppo, anzitutto un errore nella scrittura. Nel file della scorsa volta non abbiamo usato nessuna funzione “update()” ma “draw()”, per cui a eventuali allievi inesperti suggerisco la correzione seguente.

Dove c’era

void draw() {
float t, r, distance;

background(0, 0, 0);
t = frameCount/fps;
// eccetera

mettete

void draw() {
float t, r, distance;

background(bgColor);
t = frameCount/fps;
// eccetera

e chiedo scusa per essere stato pedissequo.

Poi un paio di cose che non capisco io.
keypressed() e key sono rispettivamente una funzione e una variabile standard del linguaggio/libreria? Perché non vedo da dove saltino fuori, pur risultando evidente cosa fanno.

E poi: ma possibile che con il background a (0, 100, 25), cioè col rosso a zero, mi viene rossiccio – marroncino? E’ uno standard di colori diverso? O_O

… ok, questa l’ho capita mentre scrivevo, e ho notato che come colormode c’è HSB, che mi sa tanto è Hue, Saturation, Brightness. E che mi rende impossibile prevedere che tinta verrà fuori. Come non detto…

Andrea Franceschini » 

Ah, ma allora esisti! Temevo che saresti apparso per correggermi le proprietà dell’algebra di Boole, francamente, ma ogni correzione è benvenuta, quindi ho già fatto le dovute modifiche, compreso segnalare che keyPressed() e key sono roba di libreria.

E sì, nella lezione scorsa avevo già accennato al modo HSB e avevo detto che l’avrei spiegato più avanti in dettaglio, l’ho usato più per comodità per evitare mille conti che sarebbero risultati in monti di codice in più per niente.

gianduja » 

Tranquillo, io il blog lo seguo sempre (e questo ti sia d’avvertimento).
Sono solo in attesa che avvenga l’inevitabile salto quantico tra “roba che so già” e “roba che non capirò mai”, e quel momento spero non sia troppo vicino :D

Andrea Franceschini » 

Nope, non sarà troppo vicino, tranquillo :) Ogni tanto mi toccherà buttarci dentro qualche bizzarria quantistica, ma cerco di diluire il più possibile (senza scendere sotto potenza D, magari…)

Rispondi