Il perché e il come degli Iteratori
Un amico mi ha posto qualche domanda generica via email sugli iteratori. Ho voluto inserire anche qui alcune delle risposte in modo da renderle disponibili ad un pubblico più ampio.
Perché abbiamo gli iteratori?
Prima di tutto inventiamo un po’ di dati con cui effettuare le prove:
1 >> Name = Struct.new(:first, :last) 2 => Name 3 >> names = [ Name.new("James", "Gray"), 4 ?> Name.new("Dana", "Gray"), 5 ?> Name.new("Caleb", "Nordloh"), 6 ?> Name.new("Tina", "Nordloh") ] 7 => [#<struct Name first="James", last="Gray">, 8 #<struct Name first="Dana", last="Gray">, 9 #<struct Name first="Caleb", last="Nordloh">, 10 #<struct Name first="Tina", last="Nordloh">]
Addesso proviamo a visualizzare alcuni nomi. Per farlo possiamo utilizzare l’iteratore each():
1 >> names.each {|name| puts ", " } 2 Gray, James 3 Gray, Dana 4 Nordloh, Caleb 5 Nordloh, Tina 6 => [#<struct Name first="James", last="Gray">, 7 #<struct Name first="Dana", last="Gray">, 8 #<struct Name first="Caleb", last="Nordloh">, 9 #<struct Name first="Tina", last="Nordloh">]
Non c’è troppa differenza da un ciclo, ma proviamo a cercare con find() un nome specifico:
1 >> names.find {|name| name.first == "Caleb" } 2 => #<struct Name first="Caleb", last="Nordloh"
Inoltre potrebbe essere necessario sapere quali sono tutti i cognomi. Lo possiamo fare mappando (map()), un altro iteratore, i nomi con solo i cognomi e usando un semplice helper:
1 >> names.map {|name| name.last }.uniq 2 => ["Gray", "Nordloh"]
Forse vogliamo lavorare solo con alcuni record dell’insieme. Possiamo selezionarli con select():
1 >> names.select {|name| name.first =~ /^(?:J|C)/ } 2 => [#<struct Name first="James", last="Gray">, 3 #<struct Name first="Caleb", last="Nordloh">]
O possiamo ordinarli con sort_by() secondo alcuni criteri:
1 >> names = names.sort_by {|name| [name.last, name.first] } 2 => [#<struct Name first="Dana", last="Gray">, 3 #<struct Name first="James", last="Gray">, 4 #<struct Name first="Caleb", last="Nordloh">, 5 #<struct Name first="Tina", last="Nordloh">]
Adesso tieni presente che in linguaggi incentrati sui cicli abbiamo solo un ciclo nella maggior parte dei casi tipo questo:
1 for (int i = 0; i < ...; i++) { 2 ... 3 }
In questo è necessario fornire tutti i dettagli per ognuno e ogni volta che vogliamo trovare un oggetto nella lista o visualizzare un oggetto. Tracciare gli indici, gestire nuovi array/hash/o qualsiasi altra cosa in cui inserire gli oggentti, interrompere il ciclo quando abbiamo finito, etc… Osserva invece come con in tutti gli esempi in Ruby descritti sopra ho fatto solo attenzione sul singolo oggetto e su che operazione volevo effettuare su di esso. Gli iteratori gestiscono tutti i compiti ripetitivi e noiosi al posto mio, lasciandomi concentrare sull’effettiva operazione da eseguire sull’oggetto.
Posso cercare di indovinare che starete pensando al fatto di dover imparare tutti gli iteratori e quello che fanno piuttosto che imparare un solo ciclo. Il Ruby cerca di ovviare a questo problema inserendo tutti gli iteratori insieme e utilizzando in tutti gli oggetti standard, come Array e Hash. In più, tutto quello che dovete fare è definire il semplice iteratore each() come mix-in Enumerable come già fanno le classi standard per ottenere automaticamente gli altri iteratori. Imparerete in modo semplice e una volta sola gli iteratori e li userete dappertutto.
Come si costruiscono gli iteratori?
Partiamo con un esempio. Vogliamo costruire una lista collegata (LinkedList) in Ruby. Qualcosa del tipo:
1 >> class LinkedList 2 >> 3 >> @node = head 4 >> @next = nil 5 >> end 6 >> 7 >> @node 8 >> end 9 >> 10 >> unless value.nil? 11 >> @next = self.class.new(value) 12 >> end 13 >> @next 14 >> end 15 >> end 16 => nil
Costruiamo adesso una semplice routine per popolare la lista con alcuni dati:
1 >> 2 >> start = LinkedList.new(0) 3 >> first = start 4 >> sec = first.next(1) 5 >> 100.times do 6 ?> new_node = sec.next(first.value + sec.value) 7 >> first = sec 8 >> sec = new_node 9 >> end 10 >> start 11 >> end 12 => nil 13 >> fib = fib_seq 14 => ...
Adesso scriviamo l’iteratore each() per la classe LinkedList, per consentire agli utenti di visualizzare i valori. Utilizzeremo un limitatore opzionale dato che le liste potrebbero diventare abbastanza lunghe:
1 >> class LinkedList 2 >> 3 >> current = self 4 >> until current.nil? or (not limit.nil? and limit == 0) 5 >> yield current.value 6 >> current = current.next 7 >> limit -= 1 unless limit.nil? 8 >> end 9 >> end 10 >> end 11 => nil
Osservate come ho utilizzato la funzione yield per passare i valori al blocco non appena ne ricevo uno.
Vediamo come funziona:
1 >> fib.each(3) {|n| puts n } 2 0 3 1 4 1 5 => nil 6 >> fib.each(10) {|n| puts n } 7 0 8 1 9 1 10 2 11 3 12 5 13 8 14 13 15 21 16 34 17 => nil
Scriviamo adesso un altro iteratore, il find() (tecnicamente avremmo potuto usare un oggetto mix-in per ottenerlo automaticamente):
1 >> class LinkedList 2 >> 3 >> results = Array.new 4 >> each(limit) do |value| 5 ?> results << value if yield value 6 >> end 7 >> results 8 >> end 9 >> end 10 => nil
Qui ho utilizzato lo yield per vedere se l’utente è interessato al valore. Passo il valore nel blocco e mi aspetto di ricevere una risposta positiva o negativa (true/false).
Ad esempio lo possiamo utilizzare per cercare i primi 100 numeri di Fibonacci divisibili per 3:
1 >> fib.find {|n| n % 3 == 0 } 2 => [0, 3, 21, 144, 987, 6765, 46368, 317811, 2178309, 14930352, 102334155, 3 701408733, 4807526976, 32951280099, 225851433717, 1548008755920, 4 10610209857723, 72723460248141, 498454011879264, 3416454622906707, 5 23416728348467685, 160500643816367088, 1100087778366101931, 6 7540113804746346429, 51680708854858323072, 354224848179261915075]

