I Blocks e i Procs

Questa è senza dubbio una delle caratteristiche più cool di Ruby. Alcuni altri linguaggi hanno questa possibilità, sebbene la chiamino in altri modo (come closures), ma la maggior parte di quelli popolari non la hanno, il che è un vero peccato.

Ma di che si tratta? E' l'abilità di prendere un block di codice (cioè un blocco di codice compreso tra do e end), impacchettarlo in un oggetto (chiamato proc), salvarlo in una variabile o passarlo a un metodo, ed eseguire il codice nel block ogni volta che ti serve. Quindi assomiglia molto a un metodo, eccetto che non è legato a un oggetto (piuttosto è un oggetto), e puoi salvarlo o passarlo ingiro come puoi fare con qualsiasi altro oggeto. Penso sia arrivato il momento per un esempio:

brindisi = Proc.new do
  puts 'Cin cin!'
end

brindisi.call
brindisi.call
brindisi.call
Cin cin!
Cin cin!
Cin cin!

Quindi ho creato un proc (che penso sia il un diminutivo di "procedure" o "procedura", ma, cosa molto più importante, fa rima con "block") che contiene il block di codice, quindi ho pututo chiamare (che in inglese si dice proprio call) il block di codice tre volte. Come puoi vedere, assomiglia molto a un metodo.

Effettivamente, assomiglia perfino di più a un metodo di quanto non ti abbia finora mostrato, perché i block possono anche prendere parametri:

tiPiace = Proc.new do |unaCosaBella|
  puts 'Mi piace *tanto* '+unaCosaBella+'!'
end

tiPiace.call 'cantare'
tiPiace.call 'ruby'
Mi piace *tanto* cantare!
Mi piace *tanto* ruby!

Ok, ora sappiamo cosa sono i block e i procs e come usarli, ma qual è il punto? Perché non usare semplicemente dei metodi? Bene, è perché ci sono alcune cose che proprio non puoi fare coi metodi. In particolare, non puoi passare metodi in altri metodi (ma puoi passare dei procs), e i metodi non possono restituire altri metodi (ma possono restituire dei procs). Questo semplicemente perché i procs sono oggetti; i metodi invece no.

(A proposito, nulla di questo sembra familiare?) Già, hai già visto dei block... quando hai imparato cosa sono gli iteratori. Ne parleremo ancora fra poco).

Metodi che Prendono Procs

Quando passiamo un proc in un metodo, possiamo controllare come, se o quante volte esso viene chiamato. Per esempio, diciamo che c'è qualcosa che vogliamo fare prima e dopo che una porzione di codice viene eseguita:

# encoding: utf-8
def creaSuspance unProc
  puts 'EHI GENTE ATTENZIONE! Devo fare un annuncio importante...'
  unProc.call
  puts 'Ok gente, ho finito. Proseguite pure quello che stavate facendo.'
end

salutoInformale = Proc.new do
  puts 'ciao'
end

salutoFormale = Proc.new do
  puts 'arrivederci'
end

creaSuspance salutoInformale
creaSuspance salutoFormale
EHI GENTE ATTENZIONE! Devo fare un annuncio importante...
ciao
Ok gente, ho finito. Proseguite pure quello che stavate facendo.
EHI GENTE ATTENZIONE! Devo fare un annuncio importante...
arrivederci
Ok gente, ho finito. Proseguite pure quello che stavate facendo.

Forse non sembra particolarmente favoloso... ma lo è. :-) In programmazione è fin troppo comune avere dei requisiti stringenti su cosa dev'essere fatto e quando. Se vuoi salvare un file, per esempio, devi prima aprire il file, scriverci dentro le informazioni che vuoi, e quindi chiudere il file. Se ti dimentichi di chiudere il file, Brutte Cose(tm) possono succedere. Ma ogni volta che vuoi salvare o caricare un file, devi sempre seguire la stessa procedura: aprire il file, fare quello che davvero vuoi fare, chiudere il file. E' tedioso e facile da dimenticare. In Ruby, salvare (o caricare) file funziona in modo simile al codice qui sopra, così che tu non debba preoccuparti di nulla che non sia ciò che effettivamente vuoi salvare (o caricare). (Nel prossimo capitolo ti mostrerò dove imparare come fare cose come salvare e caricare file.)

Puoi anche scrivere metodi che determinino quante volte, o perfino se chiamare un proc. Ecco un metodo che chiamerà il proc passato circa una volta su due, e un altro che lo chiamerà due volte:

def forseEsegui unProc
  if rand(2) == 0
    unProc.call
  end
end

def dueVolteEsegui unProc
  unProc.call
  unProc.call
end

ammicca = Proc.new do
  puts '<occhiolino>'
end

seduci = Proc.new do
  puts '<occhiata>'
end

forseEsegui ammicca
forseEsegui seduci
dueVolteEsegui ammicca
dueVolteEsegui seduci
<occhiata>
<occhiolino>
<occhiolino>
<occhiata>
<occhiata>

Questi sono alcuni degli usi più comuni dei procs che ci consentono di fare cose che semplicemente non potremmo fare utilizzando solo dei metodi. Sicuramente, potresti scrivere un metodo per ammiccare due volte, ma non puoi scriverne uno che faccia qualcosa due volte!

Prima di proseguire, vediamo un ultimo esempio. Finora i procs che sono stati passati ai metodi sono stati abbastanza simili fra loro. Questa volta saranno piuttosto differenti, così che tu possa vedere quanto l'esecuzione di un metodo possa dipendere dal proc ad esso passato. Il nostro metodo prenderà un oggetto e un proc, e restituirà il risultato della chiamata del proc su quell'oggetto. Se il proc restituisce false, usciamo; altrimenti chiamiamo il proc con l'oggetto restituito. Continuiamo a fare questa cosa finché il proc non restituisce false (il che è bene che avvenga, prima o poi o il programma andrà in crash). Il metodo restituirà l'ultimo valore prima di false restituito dal proc.

# encoding: utf-8
def faiFinoAFalse primoInput, unProc
  input  = primoInput
  output = primoInput
  
  while output
    input  = output
    output = unProc.call input
  end
  
  input
end

costruisciArrayDiQuadrati = Proc.new do |array|
  ultimoNumero = array.last
  if ultimoNumero <= 0
    false
  else
    array.pop #  Togli l'ultimo numero...
    array.push ultimoNumero*ultimoNumero  # ...e rimpiazzalo col suo quadrato...
    array.push ultimoNumero-1  #  ...seguito dal prossimo numero più piccolo.
  end
end

sempreFalse = Proc.new do |ignoramiEBasta|
  false
end

puts faiFinoAFalse([5], costruisciArrayDiQuadrati).inspect
puts faiFinoAFalse('Sono le 3:00 am e ancora programmo; aiuto!', sempreFalse)
[25, 16, 9, 4, 1, 0]
Sono le 3:00 am e ancora programmo; aiuto!

Ok, questo era un esempio abbastanza strano, lo ammetto. Ma mostra con quale differenza si può comportare lo stesso metodo che riceve proc molto diversi.

Il metodo inspect (ispeziona) è molto simile al metodo to_s, con la differenza che la stringa che restituisce cerca di mostrarti il codice ruby per costruire l'oggetto in questione. Qui ci mostra l'intero array restituito dalla nostra prima chiamata a faiFinoAFalse. Inoltre, potresti aver notato che non abbiamo mai effettivamente calcolato il quadrato di quello 0 alla fine dell'array, ma siccome il quadrato di 0 è sempre e solo 0, non ne avevamo bisogno. E siccome sempreFalse era, guarda caso, sempre false, faiFinoAFalse non ha fatto proprio nulla la seconda volta che lo abbiamo chiamato; ha solo restituito ciò che gli era stato passato.

Metodi che Restituiscono Proc

Una delle altre cose cool che puoi fare coi proc è crearli nei metodi e quindi restituirli. Questo ci conferisce il potere di programmare a livelli folli (cose con nomi impressionanti come lazy evaluationinfinite data structures e currying, ma il fatto è che quasi mai scrivo questo genere di cose in pratica, né posso ricordare di aver visto nessun altro farlo nel proprio codice. Penso che si tratti proprio del tipo di cosa che non ti trovi mai a dover fare in Ruby, o magari Ruby ti incoraggia a trovare altre soluzioni; Non lo so. In ogni caso, me ne occuperò brevemente.

In questo esempio, componi prende due procs e restituisce un nuovo proc che, quando chiamato, chiama il primo proc e passa il proprio risultato al secondo proc.

def componi proc1, proc2
  Proc.new do |x|
    proc2.call(proc1.call(x))
  end
end

alQuadrato = Proc.new do |x|
  x * x
end

perDue = Proc.new do |x|
  x + x
end

dupilicaEdElevaAlQuadrato = componi perDue, alQuadrato
elevaAlQuadratoEDuplica   = componi alQuadrato, perDue

puts dupilicaEdElevaAlQuadrato.call(5)
puts elevaAlQuadratoEDuplica.call(5)
100
50

Notare che la chiamata a proc1 doveva avvenire tra le parentesi di proc2 per fare in modo che avvenisse per prima.

Passare Blocks (non Procs) ai Metodi

Ok, l'ultimo esempio è stato più per una sorta di interesse accademico che altro, ed è anche abbastanza seccante da usare. Buona parte del problema è che ci sono tre passi da completare (definire il metodo, il proc e chiamare il metodo col proc), mentre la sensazione è che ce ne dovrebbero essere solo due (definire il metodo e passargli il block direttamente, senza scomodarsi a definire un proc), dal momento che la maggior parte delle volte non interessa riutilizzare il proc/block dopo averlo passato al metodo. Ebbene, come ti aspetteresti, Ruby ci ha già pensato per noi! In effetti, hai già visto questo meccanismo ogni volta che hai usato gli iteratori.

Prima un veloce esempio, poi ne parliamo.

# encoding: utf-8
class Array
  
  def ogniPari(&eraUnBlock_oraUnProc)
    èPari = true  #  Cominciamo con "true" perché gli array
                  #  cominciano sempre con 0, che è pari.
    
    self.each do |oggetto|
      if èPari
        eraUnBlock_oraUnProc.call oggetto
      end
      
      èPari = (not èPari)  #  Commuta da pari a dispari, o da dispari a pari.
    end
  end

end

['mela', 'mela cattiva', 'fragola', 'durione'].ogniPari do |fruit|
  puts 'Yummy! Adoro le torte di '+fruit+', e tu?'
end

#  Ricorda, stiamo prendendo gli elementi con indice pari
#  nell'array, dei quali tutti sono numeri dispari,
#  semplicemente perché mi piace causare problemi di questo tipo.
[1, 2, 3, 4, 5].ogniPari do |numDispari|
  puts numDispari.to_s+' NON è un numero pari!'
end
Yummy! Adoro le torte di mela, e tu?
Yummy! Adoro le torte di fragola, e tu?
1 NON è un numero pari!
3 NON è un numero pari!
5 NON è un numero pari!

Quindi per passare un block a ogniPari, basta attaccare il block subito dopo il metodo. In questo modo si può passare un block in qualsiasi metodo, sebbene molti metodi lo ignoreranno. Per fare in modo che il tuo metodo non ingori il blocco, ma invece lo prenda e lo converta in un proc, metti il nome del proc alla fine dei parametri del tuo metodo, preceduto da un ampersand (&). Questa parte è un po' delicata, ma non terribile, e te ne devi occupare solo una volta (quando definisci il metodo). Dopodiché puoi usare il metodo ripetutamente, esattamente come i metodi già inclusi in Ruby che accettano blocks, come each e times. (Ricordi 5.times do...?)

Se ti sentissi confuso/a, ricordati solo come dovrebbe comportarsi ogniPari: chiamare il block passato come parametro con ogni altro elemento nell'array. Una volta che l'hai scritto e che funziona, non hai bisogno di preoccuparti di cosa stia avvenendo dietro le quinte ("quale block è chiamato quando??"); in effetti, questo è esattamente il perché scriviamo metodi come questo: così da non dover pensare mai più a come funzionano. Li useremo e basta.

Mi ricordo di una volta in cui volevo essere in grado di quantificare quanto tempo richiedevano diverse parti di un programma per essere eseguite. (Questa si chiama anche profilazione del codice). Così scrissi un metodo che si leggeva il tempo prima di eseguire il codice, poi lo eseguiva, e quindi leggeva il tempo di nuovo e calcolava la differenza col tempo letto prima di eseguire il codice. Non riesco a ritrovare quel codice adesso, ma non fa nulla; probabilmente era qualcosa di questo genere:

# encoding: utf-8
def profile descrizioneDelBlock, &block
  tempoInizio = Time.now
  
  block.call
  
  durata = Time.now - tempoInizio
  
  puts descrizioneDelBlock+':  '+durata.to_s+' secondi'
end

profile '25000 raddoppi' do
  numero = 1
  
  25000.times do
    numero = numero + numero
  end
  
  puts numero.to_s.length.to_s+' cifre' # Numero di cifre di questo NUMERONE.
end

profile 'conta fino a un milione' do
  number = 0
  
  1000000.times do
    number = number + 1
  end
end
7526 cifre
25000 raddoppi:  0.246768 seconds
conta fino a un milione:  0.90245 seconds

Che semplicità! Che eleganza! Con quel piccolo metodo posso facilmente conoscere il tempo di esecuzione di qualsiasi parte di ogni programma che voglio; Devo solo lanciare il codice in un block e mandarlo a profila. Cosa potrebbe esserci di più semplice? Nella maggior parte dei linguaggi avrei dovuto esplicitamente aggiungere il codice di profilazione (la cose in profila) attorno ad ogni sezione che avrei voluto profilare. In Ruby, invece, è possibile tenere tutta la logica in un solo posto, e (cosa più importante) fuori dalla mia strada.

Un Po' di Cose da Provare

  • Orologio del Nonno. Scrivi un metodo che prende un block come parametro e lo chiama una volta per ogni ora che è passata oggi. In qusto modo, se passassi come block do puts 'DONG!end, scampanellerebbe (più o meno) come un orologio del nonno. Testa il tuo metodo con un po' di block differenti (incluso quello che ti ho appena suggerito). Suggerimento: Puoi usare Time.now.hour per ottenere l'ora corrente. Tuttavia, questo restituisce un numero tra 0 e 23, quindi dovrai alterare quei numeri per trasfromarli in numeri da orologio di una volta (da 1 a 12).
  • Program Logger. Scrivi un metodo chiamato log, che prenda una descrizione in formato stringa di un block e, ovviamente, un block. Analogamente a creaSuspance, dovrebbe putsare una stringa dicendo che ha cominciato a eseguire il block, un'altra stringa che dica che ha finito e infine una stringa che dica il risultato restituito dall'esecuzione del block. Testa il metodo passandogli un block di codice. Dentro al block, metti un'altra chiamata a log, passandogli un altro block. (Questo si chiama annidamento o nesting). In altre parole, il tuo output dovrebbe assomigliare a qualcosa del genere:

     

    Inizio "block esterno"...
    Inizio "un piccolo block"...
    ..."un piccolo block" terminato, ha restituito:  5
    Inizio "un altro block"...
    ..."un altro block" terminato, ha restituito: Mi piace mangiare vegetariano!
    ..."block esterno" terminato, ha restituito:  false
  • Logger migliorato. L'output del precedente logger era abbastanza scomodo da leggere, e la situazione peggiorerebbe all'aumentare dei block annidati. Sarebbe molto più facile da leggere se indentasse le righe dei block interni. Per far questo avrai bisogno di tener tracci di quanto profondamente sei annidato ogni volta che il logger vuole scrivere qualcosa. Per riuscirci usa una variabile globale, per crearla basta farne precedere il nome con un $, come per queste di esempio: $globale, $profonditàAnnidamento, e $bigTopPeeWee. Alla fine, il tuo logger dovrebbe scrivere un output di questo tipo:
Inizio "block esterno"...
  Inizio "un piccolo block"...
    Inizio "un block piccino picciò"...
    ..."un block piccino picciò" terminato, ha restituito: tanto amore
  ..."un piccolo block" terminato, ha restituito:  42
  Inizio "ancora un block"...
  ..."ancora un block" terminato, ha restituito: amo mangiare frutta fresca!
..."block esterno" terminato, ha restituito:  true

Le soluzioni sono disponibili nel Manuale delle Soluzioni :)

Bene, e questo è tutto per questo tutorial. Complimenti! Hai imparato tanto! Magari ora non ti sembra di ricordare tutto, o magari hai saltato alcune parti... davvero, va bene così. Programmare non riguarda ciò che sai; riguarda ciò che puoi capire. Finché sai dove trovare le cose che dimentichi, vai alla grande. Spero non penserai che ho scritto tutto questo senza riguardarmi le cose ogni minuto! Perché l'ho fatto. Ho ottenuto anche parecchio aiuto per il codice che esegue tutti gli esempi di questo tutorial. Ma dov'è che io sono andato a guardare le cose, e a chi chiedevo aiuto? Lascia che ti mostri...

Vorrai mica perderti altri articoli come questo?
Iscriviti per riceverne altri!

Scritto da

Duccio

Duccio Armenise

Corsidia Founder

Aiuto solo i migliori Maestri a trovare i loro prossimi Studenti. Come? Così! :)

Bio - uCV

Per

Corsidia logo

I tuoi prossimi Studenti ti stanno già cercando, tu ci sei?

Materie

Trova un altro corso