Ruby language

Range e iterazioni                

Abbiamo rimandato l'argomento relativo alla possiblità di effettuare delle iterazioni su array, hash stringhe ecc... è venuto il momento di rimediare a questa mancanza. Per prima cosa però introduciamo il discorso relativo alla classe range per la quale abbiamo fatto alcune anticipazioni nel capitolo precedente. Qui pertanto ripeteremo alcuni concetti che potrebbero essere già noti, anche per latre cose, non solo per quanto concerne i range. Ma intanto male non fa :-).

Come tutto quanto in Ruby anche i range sono oggetti; tale oggetto rappresenta tutti i valori compreso tra un inizio ed una fine. Si può direi che rappresentino un sottoinsieme di tutti i valori possibili per un certo tipo. Abbiamo due rappresentazioni di base:

1..10    ovvero tutti i numeri da 1 a 10 estremi compresi; gli estremi sono separati da due puntini
1...10   tutti i numeri da 1 a 10 escluso quest'ultimo, quindi tutti i numeri da 1 a 9; gli estremi sono separati da tre puntini.

Relativamnte al primo caso è possibile utilizzare una forma più object-oriented:

Range.new(1,10)

Un facile test per verificare la presenza di un valore in un range (vedremo che un range non si costruisce solo con i numeri e anzi ogni oggetto è utilizzabile purchè su di esso si possa adoperare l'operatore di confronto <=>) è attuabile tramite include? come da esempio:

irb(main):001:0> r1 = 1..10
irb(main):003:0> r1.include?5
=> true
irb(main):004:0> r1.include?50
=> false

In alternativa si può usare ===:

irb(main):011:0> d = 1..10
=> 1..10
irb(main):012:0> d === 5
=> true
irb(main):013:0> d === 11
=> false

Come detto i range si possono costruire utilizzando tipi diversi:

20..53
'a'..'h'
0...x    dove x è una quantità definita. Anche le stringhe vanno bene:

irb(main):014:0> ss = "caa".."caf"
=> "caa".."caf"
irb(main):015:0> ss.to_a
=> ["caa", "cab", "cac", "cad", "cae", "caf"]

Intanto che ci siamo abbiamo utilizzato to_a che trasforma il range in un array in cui ogni elemento è un componente del range stesso. Molto comodo.

Il secondo elemento, quello finale deve essere maggiore del primo; scrivere
5..0
è sintatticamente corretto ma produce un range di lunghezza 0.

Come accennato in precedenza gli elementi di un range devono essere in qualche modo ordinati e pertanto devono poter essere confrontati. In questo senso ecco spiegata la necessità di poter applicare ad essi l'operatore di confronto visto prima, <=>. Questo restituisce -1 se il secondo elemento è maggiore del primo, 0 in caso di uguaglianza e 1 se ad essere maggiore è il primo.

irb(main):005:0> 3 <=> 0
=> 1
irb(main):006:0> 12 <=> 12
=> 0
irb(main):007:0> 8 <=> 9
=> -1

Questo operatore è usato internamente per effettuare i confronti.

Una range può essere utile per contenere elementi e come abbiamo visto si può indagare sulla loro presenza o meno all'interno della sequenza; tuttavia molto utile è anche la possibilità di iterare sui loro elementi interni; per fare questo il sistema più semplice è ricorrere ad una istruzione già vista in precedenza ovvero each:
si tratta di una istruzione molto potente che incontrerete spesso nei programmi scritti in Ruby; la sua sintassi è duplice:

each {|var| blocco} che restituisce un range ed è la forma che incontrerete più spesso
each che restituisce un enumeratore

irb(main):001:0> r01 = 1...10
irb(main):003:0> r01.each{|x| print"#{x} "}
1 2 3 4 5 6 7 8 9 => 1...10

Se vogliamo mettere più istruzioni la cosa è abbastanza semplice:

  Esempio 7.1
1
2
3
4
5
6
x = 1..10
x.each do |y|
puts y
puts y + 1
puts y * y
end

Un parente prossimo è step: che funziona più o meno allo stesso modo ma permette di impostare un passo a piacere nelle iterazioni:

irb(main):004:0> r01.step(2) {|x| puts x}
1
3
5
7
9

In questo caso abbiamo impostato il passo pari a 2, evidentemente. Peraltro abbiamo già visto in azione step nel paragrafo precedente.
Come avrete notato ci sono due sintassi utilizzabili per l'iterazione con each: la prima fa uso delle parentesi graffe, la seconda delle parole do - end. La cosa è normalmente equivalente ma c'è una sottile differenza che vediamo qui di seguito:

irb(main):001:0> print [1,2,3,4,5].each {|i| i}
[1, 2, 3, 4, 5]=> nil
irb(main):002:0> print [1,2,3,4,5].each do |i| i end
#<Enumerator:0x000000033cbf38>=> nil

Capito? Nel primo caso ne risulta quanto in effetti ci potevamo aspettare, nel secondo esce fuori uno strano errore. Questo è dovuto al fatto che print ha una priorità più elevata rispetto alla sequenza begin - end. Perchè tutto funzioni naturalmente bisogna mettere tutta la parte dopo il print tra parentesi tonde.

Ovviamente come tutti gli oggetti in Ruby anche each ha un buon corredo di metodi e ne vediamo alcuni in rapida successione:

irb(main):005:0> r01.first
=> 1
irb(main):005:0> r01.begin
=> 1
irb(main):006:0> r01.end
=> 10
irb(main):007:0> r01.last
=> 10
irb(main):008:0> r01.size
=> 9
irb(main):009:0> r02 = 1..10
=> 1..10
irb(main):010:0> r02.size
=> 10
irb(main):009:0> (0..2).eql?(0..2)
=> true
irb(main):010:0> (0..2).eql?(0...2)
=> false

Dovrebbe essere tutto abbastanza chiaro. Faccio solo notare che size si riferisce agli elementi effettivamente utilizzabili, come dimostra il suo valore prima rilevato su r01 e poi su r02; nela prima sequenza infatti il valore 10 non era in realtà compreso. L'istruzione eql? confronta due range e verifica se sono ugluali (devono coincidere inizio, fine, tipo).

Iterazioni sugli array

Nel capitolo relativo a questo potente strumento, gli array appunto, avevamo omesso di parlare proprio della possibilità di iterare tra i suoi elementi. Anche in questo caso ricorriamo a each come istruzione più semplice per effettuare questo task anche se, come vedremo, esistono anche modi più "classici":

irb(main):002:0> ar1.each {|x| print x}
12345=> [1, 2, 3, 4, 5]

è possibile compiere elaborazioni anche più ampie; in tal caso le istruzioni all'interno del blocco vanno separate tramite il ; (punto e virgola). Ad esempio scrivere istruzioni tipo:

irb(main):011:0> ar1.each {|x| y = x + 2; z = y + 6; print z}

Vediamo un esempio più completo e, a mio avviso, più leggibile:

  Esempio 7.2
1
2
3
4
5
6
7
8
ar1 = [1,2,3,4,5]
ar1.each do |x|
  if x % 2 == 0
    puts "#{x} -> pari"
  else
    puts "#{x} -> dispari"
  end
end

che dà il seguente, ovvio, output:

1 -> dispari
2 -> pari
3 -> dispari
4 -> pari
5 -> dispari

Sinceramente preferisco questa forma, se devo eseguire più istruzioni.

Nulla vieta di usare anche il sistema classico che fa uso di una iterazione con il ciclo for per navigare tra gli elementi di un array (e naturalmente, anche se si vede molto di rado, si possono usare anche while e until, non è difficile modificare l'esempio che trovate qui di seguito); per dimostrarlo riscriviamo l'esempio 7.2 utilizzando questo metodo di iterazione alternativo:

  Esempio 7.3
1
2
3
4
5
6
7
8
ar1 = [1,2,3,4,5]
for y in 0..ar1.size - 1
  if ar1[y] % 2 == 0
    puts "#{ar1[y]} -> pari"
   else
    puts "#{ar1[y]} -> dispari"
  end
end

che fornisce lo stesso identico output. Riguardo ai due sistemi di iterazione credo si possa dire che è una questione di gusti... chi viene da altri linguaggi dove il for è praticamente l'unico, o comunque il miglior sistema di iterazione tra gli elementi di un array noto che ha la tendenza a tenersi tale metodo... chi è nuovo e adotta Ruby normalmente usa each, che in questo linguaggio è molto utilizzato. Da parte mia, pur provenendo da C# e Delphi come linguaggi di elezione, cerco di usare il primo sistema.

Un'altra istruzione molto utile per gestire gli array è collect. Essa viene usata per manipolare un array creandone un altro con il risultato delle manipolazioni praticamente con una sola istruzione. Vediamo un esempio di base:

irb(main):009:0> x = [1,2,3,4,5]
irb(main):011:0> y = x.collect {|x| x + 4}
=> [5, 6, 7, 8, 9]
irb(main):012:0> x
=> [1, 2, 3, 4, 5]
irb(main):013:0> y
=> [5, 6, 7, 8, 9]

E anche qui diamo un esempio con più istruzioni nel formato, secondo me, più leggibile:

  Esempio 7.4
1
2
3
4
5
6
ar1 = [1,2,3,4,5]
ar2 = ar1.collect do |x|
x = x + 1
x = x * 3
end
puts ar2

Esiste anche collect! che distrugge, modificandolo, l'array originale. Un alias di collect è map (o map!) che poi è l'istruzione equivalente in Python.
Solo a titolo informativo diamo un paio di esempi con altre istruzioni di iterazione:

irb(main):001:0> [1,2,3,4,5].reverse_each {|x| puts x}
5
4
3
2
1

reverse_each lavora sugli elementi partendo dall'ultimo.
Abbastanza interessante è each_with_index che espone sia gli elementi dell'array sia gli indici relativi:

irb(main):003:0> ar1 = ["ciao", "hello", "hallo", "bonjour"]
irb(main):013:0> ar1.each_with_index do |valore, indice|
irb(main):014:1* puts "indice: #{indice} -> valore: #{valore}"
irb(main):015:1> end
indice: 0 -> valore: ciao
indice: 1 -> valore: hello
indice: 2 -> valore: hallo
indice: 3 -> valore: bonjour

L'iterazione sugli array ha alcuni altri aspetti che potrebbero essere approfonditi; ritengo che considerando il livello che desideriamo tenere in questi capitoli quanto esposto sia comunque più che sufficiente per lavorare pienamente.

Iterazioni sugli hash

Le istruzioni principali per compiere questa operazione sono each e each_pair che sono praticamente equivalenti:

  Esempio 7.5
1
2
h = {1 => "uno", 2 => "due", 3 => "tre"}
h.each_pair {|chiave, valore| puts "la chiave #{chiave} ha valore #{valore}" }

che, come potrete comprovare facilmente funziona alla stessa identica maniera se mettete each al posto di each_pair, quest'ultimo però, proprio per come è implementato internamente, è leggermente più efficiente e visti i problemi di performance di Ruby usarlo può dare una mano.
Un'istruzione più mirata è each_key che permette di iterare tra le sole chiavi di un hash.

  Esempio 7.6
1
2
3
4
5
6
h = {1 => "uno", 2 => "due", 3 => "tre"}
count = 1
h.each_key do |chiave|
puts "La chiave #{count} e': #{chiave}"
count = count + 1
end

Del tutto analoga è each_value che naturalmente estrae solo i valori.

Come strada alternativa, se preferite lavorare sugli array ricordatevi che è sempre possibile lavorare direttamente su di essi trasorfmando il vostro hash o i soli valori o le sole chiavi, in un array appunto sul quale potrete applicare le tecniche viste appena sopra. Il passaggio, come noto, avviene così:

irb(main):001:0> h1 = {1 => "uno", 2 => "due", 3 => "tre"}
irb(main):003:0> h1.keys
=> [1, 2, 3]
irb(main):004:0> h1.values
=> ["uno", "due", "tre"]
irb(main):005:0> h1.to_a
=> [[1, "uno"], [2, "due"], [3, "tre"]]

Le 3 istruzioni restituiscono rispettivamente: un array contenente solo le chiavi, uno contenente solo i valori ed infine il terzo contiene 3 array composti ciascuno da una coppia di elementi uno corrispondente alla chiave e l'altro alla chiave del hash originario.

Iteratori custom

nb: in questa sezione anticiperemo alcuni concetti relativi alle funzioni: eventualmente potete consultare l'apposito paragrafo

Da un punto di vista pratico un iteratore non è che un particolare costrutto che è in grado di richiamare un certo blocco di codice, sia esso costituito da una singola istruzione o da lunghe sequenze o funzioni. Non è difficile quindi immaginare la possibilità di costruirci qualche cosa che lavori in quel modo senza dover ricorrere a quanto già ci offre il linguaggio. Un blocco non è altro che una porzione di codice con un nome e racchiusa all'interno di una coppia di parentesi graffe:

nome_blocco
{
istruzioni
}

Peraltro vedremo che non è in questo formato che lo useremo. La parola chiave che ci serve è yield. E' questa che si occupa di richiamare il blocco di codice che ci interessa e di mandarlo in esecuzione; terminata questa fase il controllo viene restituito all'istruzione successiva. Questo tipo di funzionamento è proprio tipico degli iteratori e, come vedremo yield può fare lo stesso nelle modalità che vogliamo noi.

  Esempio 7.7
1
2
3
4
5
6
def test
yield
puts("ciao")
yield
end
test {puts("all'interno di test"); puts("Seconda istruzione")}

Con l'output che segue:

all'interno di test
Seconda istruzione
ciao
all'interno di test
Seconda istruzione

La riga 1 presenza la keyword def che introduce una funzione, un metodo per meglio dire. Ne riparleremo, per il momento prendete la cosa così com'è. Alle righe 2 e 4 abbiamo il nostro yield che, come si evince dall'ouput, lancia l'esecuzione del codice alla riga 6. Quest'ultimo ricalca la definizione di blocco scritta prima ma il tutto deve essere immediatamente seguente alla funzione e non ammette alcun "a capo" (provare per credere). Possiamo anche passare uno o più parametri tramite yield stesso:

  Esempio 7.8
1
2
3
4
5
def test
yield 3, 4
puts("ciao")
end
test {|x, y| print("parametro 1: #{x} + parametro 2: #{y} = ", x + y); puts}

Vediamo un esempio più completo che ci permette di capire meglio la vicinanza tra yield ed un iteratore classico:

  Esempio 7.9
1
2
3
4
5
6
def xyz(a,b,c)
for t in 1..a
yield t,b,c
end
end
xyz(3,4,5) {|x, y, z| puts (x + y + z)}

In questo caso yield, all'interno del for, richiama il blocco alla riga 6 e quindi restituisce il controllo all'istruzione successiva (che è un end del for e quindi ricomincia il ciclo). E' anche evidente che l'uso di yield ha forti similitudini con la chiamata a metodo, che vedremo in un prossimo paragrafo.

L'uso di questa tecnica può venire molto utile con le classi, come vedremo.