11 Oct
matte

matte il 11 October 2006 parla di Rubylation

Il perché e il come degli Iteratori

Questo articolo è stato precedentemente pubblicato da James Edward Gray II su The Why and How of Iterators. E’ stato rielaborato da Rubylation Network ed è disponibile in più lingue.

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 "#{name.last}, #{name.first}" }
    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 >>   def initialize( head )
    3 >>     @node = head
    4 >>     @next = nil
    5 >>   end
    6 >>   def value
    7 >>     @node
    8 >>   end
    9 >>   def next( value = nil )
   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 >> def fib_seq
    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 >>   def each( limit = nil )
    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 >>   def find( limit = nil )
    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]

Scrivi un commento