Nowości i zmiany w Ruby 1.9 #6 - iteratory (klasa Enumerator)
6 komentarzy | Kategorie: Ruby, Techblog | trackbackTagi: 1.9 enumerator ruby ruby1.9
Wpis ten jest jedną z części cyklu pt "Nowości i zmiany w Ruby 1.9". Pełną listę wpisów znajdziesz pod adresem http://radarek.jogger.pl/2008/11/30/nowosci-i-zmiany-w-ruby-1-9/.
Zacznę od (jak mi się wydaje) najważniejszej zmiany. Chodzi mianowicie o zewnętrzne iteratory, których do tej pory jakby nie było. Napisałem jakby, ponieważ mogliśmy skorzystać z klasy Generator, która potrafiła przekonwertować wewnętrzny iterator na zewnętrzny. Niestety było to niewygodne, a przy okazji dość niewydajne (implementacja tej klasy jest oparta na kontynuacjach - sic!). Zewnętrzne iteratory przydają się podczas iteracji kilku kolekcji na raz, zwłaszcza jeśli nie posiadają dostępu indeksowego.
Oto przykład iteracji po dwóch kolekcjach (celowo jedna z nich to hash, który nie ma dostępu po indeksie) z wykorzystaniem zewnętrznego iteratora.
numbers = [10, 100, 123]
pairs = { "one" => "foo", "two" => "bar", "three" => "baz" }
e1 = numbers.each
e2 = pairs.each
loop do
p [e1.next, e2.next]
end
Jeśli zastanawiasz się dlaczego program nie wpada w nieskończoną pętlę to odpowiadam, że metoda Enumerable#next rzuca wyjątek StopIteration, który jest po cichu przechwytywany przez pętlę loop co powoduje jej zakończenie.
Zwróć uwagę, że wywołanie each bez podania bloku powoduje zwrócenie obiektu zewnętrznego iteratora (klasa Enumerator) i to on potrafi wyciągać kolejne elementy kolekcji (metoda next). Obiekt ten jest instancją klasy Enumerator.
enum_for wbudowane w język
Muszę przyznać, że dopiero niedawno przyjrzałem się metodzie Object#enum_for (również dostępna pod aliasem Object#to_enum). Jest to bardzo ciekawa metoda, która pozwala na utworzenie nowego iteratora na podstawie istniejącego. Pozwala to nam na wskazanie nazwy metody, która będzie dostępna pod standardową metodą iteracji each. Daje nam to nowe możliwości, ponieważ cała gama metod z modułu Enumerable będzie pracować z nową definicją tej metody. Być może sam opis nie wyjaśnia problemu, myślę, że prosty przykład pomoże Ci zrozumieć jak to działa.
a = [1, 2, 3, 4]
# wypisuje wszystkie 3 elementowe kombinacje
puts "Kombinacje #{a}"
a.combination(3).each do |c|
p c
end
e = a.enum_for(:combination, 3)
# znajdujemy wszystkie 3 elementowe kombinacje, których suma jest parzysta
p e.find_all {|c| c.inject(:+) % 2 == 0 }
W wersji 1.8 Rubiego by mieć dostęp do enum_for należy dołączyć bibliotekę "enumerator". Od wersji 1.9 jest ona częścią core, więc jest dołączana automatycznie.
Zauważ, że zwrócony obiekt jest także instancją klasy Enumerator (w ruby 1.8 enum_for zwracał instancję klasy Enumerable::Enumerator). Zatem (mam nadzieję że jeszcze pamiętać) możemy go potraktować jako zewnętrzny iterator.
Z iteratorami wiąże się jeszcze jedna bardzo ciekawa zmiana. Otóż większość metod modułu Enumerable zwraca iterator jeśli nie zostanie podany blok kodu, podobnie jak to było w przypadku metody each w pierwszym listingu. Wykorzystując ten fakt w ostatnim przykładzie mogłem obyć się bez enum_for w bardzo prosty sposób:
a = [1, 2, 3, 4]
p a.combination(3).find_all {|c| c.inject(:+) % 2 == 0 }
Czyż to nie piękne? W ten sposób możemy tworzyć własne iteratory i łączyć je we wszystkie możliwe sposoby.
map_with_index, select_with_index - zawsze czegoś brakuje
Kolejną dosyć częstą bolączką iteratorów był brak dostępu do indeksu iteracji. Dostępny mieliśmy tylko each_with_index. Tutaj klasa Enumerator po raz kolejny okazuje się zbawienna. Otóż metoda Enumerator#with_index tworzy nowy iterator, ale wzbogacony właśnie o indeks. A skoro teraz możemy tworzyć iteratory na podstawie każdej metody...
file = File.open(__FILE__)
file.each_line.with_index do |line, i|
puts "#{i}: #{line}"
end
I w ten oto sposób każdy znany Ci iterator może zostać wzbogacony o indeks. Pozostałe metody tej klasy to rewind (przewija pozycję iteracji na początek) oraz with_object (działa jak with_index, ale przekazuje podany obiekt).
Istnieje także możliwość utworzenia iteratora za pomocą konstruktora Enumerable.new. Pokażę przykład, a Wam zostawię już przeanalizowanie go.
fib = Enumerator.new do |y|
a = b = 1
loop do
y << a
a, b = b, a + b
end
end
p fib.take(10) #=> [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
Pierwszy! :)
przyjŻałem ? ;>
fajne:)
oki, gratulacje ;).
szymon, dzięki, poprawione.
Jeszcze nie rozumiem ;-)... gdzie by to zastosowac, ale juz blizej niz dalej do ogarniecia.
ps. nastepny wpis o instance_exec ? ;->