Ruby/Iteratory

IteratoryEdytuj

Iteratory nie są oryginalnym pojęciem Rubiego. Występują one powszechnie w językach programowania zorientowanych obiektowo. Używane są również w Lispie, choć nie są tam nazywane iteratorami. W tym rozdziale szczegółowo przyjrzymy się wszechobecnym iteratorom Rubiego.

Czasownik "iterować" oznacza wykonywać tę samą czynność wiele razy, tak więc iterator jest czymś co wykonuje tę samą rzecz wiele razy (przykładem może być metoda powtórz z rozdziału o domknięciach).

Podczas pisania kodu potrzebujemy pętli w wielu różnych sytuacjach. W C, kodujemy je używając for lub while. Na przykład:

char *str;
for (str = "abcdefg"; *str != '\0'; str++) {
  /* tutaj przetwarzamy znak */
}

Składnia pętli for (...) z języka C dostarcza pewnej abstrakcji, która pomaga w utworzeniu pętli, ale sprawdzenie czy *str nie wskazuje na znak pusty znak wymaga od programisty znajomości szczegółów o wewnętrznej strukturze łańcucha znakowego. Między innymi dlatego, C jest odbierany jako język niskiego poziomu. Języki wyższego poziomu odznaczają się bardziej elastycznym wsparciem iteracji. Rozważ następujący skrypt sh powłoki systemowej:

#!/bin/sh

for i in *.[ch]; do
  # ... tutaj byłby kod do wykonania dla każdego pliku
done

Wszystkie pliki źródłowe i nagłówkowe języka C w bieżącym katalogu są przetwarzane i powłoka systemowa bierze na siebie detale dotyczące wskazywania i podstawiania po kolei wszystkich nazw plików, jedna po drugiej. To chyba działa na wyższym poziomie niż C, nie sądzisz?

Trzeba zauważyć jeszcze jedno: często język dostarcza iteratorów dla typów wbudowanych, ale budzi rozczarowanie gdy okazuje się, że musimy wracać z powrotem do pętli nisko poziomowych by iterować nasze własne typy danych. W programowaniu zorientowanym obiektowo (OOP - ang. Object-Oriented Programming), użytkownicy zazwyczaj definiują dużo własnych typów danych, więc to może być całkiem poważny problem.

Każdy język wspierający OOP zawiera jakieś udogodnienia dotyczące iterowania. Niektóre języki dostarczają w tym celu specjalnych klas, natomiast Ruby pozwala na definiowanie iteratorów bezpośrednio, używając w tym celu znanych już nam domknięć.

Typ String Rubiego posiada kilka użytecznych iteratorów:

ruby> "abc".each_byte { |c| printf "<%c>", c }
#=> <a><b><c>

each_byte to iterator wskazujący każdy znak w łańcuchu. Każdy znak jest podstawiany do zmiennej lokalnej c. To samo można przełożyć na coś bardziej przypominającego kod C...

s = "abc"
i = 0
while i < s.length
  printf "<%c>", s[i]
  i+=1
end
#=> <a><b><c>

... jednakże iterator each_byte jest koncepcyjnie prostszy, i wydaje się, że działałby nadal nawet gdyby klasa String uległa w przyszłości radykalnym modyfikacjom. Dużą zaletą iteratorów jest to, że zachowują one swoje poprawne działanie na przekór takim radykalnym zmianom. Jest to charakterystyczna cecha dobrego kodu w ogólności.

Innym iteratorem klasy String jest each_line.

"a\nb\nc\n".each_line { |l| print l }
#=> a
#   b
#   c

Zadania które wymagałyby dużego wysiłku w C (wyszukiwanie ograniczników linii, generowanie podłańcuchów, itd.) z użyciem iteratorów można wykonać bardzo łatwo.

Instrukcja for pojawiająca się w rozdziale o instrukcjach sterujących dokonywała iteracji przez użycie iteratora each. Iterator each klasy String działa w ten sam sposób jak each_line, więc przepiszmy powyższy przykład z for:

for l in "a\nb\nc\n"
  print l
end
#=> a
#   b
#   c

Możemy używać struktury sterującej retry w połączeniu z iterowaną pętlą. Spowoduje ona rozpoczęcie iterowania pętli od początku.

c = 0
for i in 0..4
  print i
  if i == 2 and c == 0
    c = 1
    print "\n"
    retry
  end
end
#=> 012
#   01234

Zamienienie retry na redo w powyższym przykładzie spowoduje, że tylko bieżąca iteracja będzie wykonana ponownie, z następującym wynikiem:

012
234

yield jak już wiemy, jest wyrażeniem, które przenosi sterowanie do bloku kodu który został przekazany do iteratora. Używając instrukcji yield i retry można zdefiniować iterator który będzie działał mniej więcej jak standardowa pętla while.

def WHILE(warunek)
  return if not warunek
  yield
  retry
end

i=0
WHILE(i < 3) { print i; i+=1 } #=> 012

Jak więc widzimy, iteratory w Rubim są metodami obsługującymi przekazane do nich domknięcia. Owszem, istnieją pewne ograniczenia, ale możesz pisać własne oryginalne iteratory. Szczególnie, gdy definiujemy nowy typ danych, wygodnie jest zdefiniować odpowiednie iteratory które będą na nim operować. W tym kontekście powyższe przykłady nie są szczególnie użyteczne. Poznajmy zatem bardziej praktyczne iteratory.

Przegląd iteratorówEdytuj

all?Edytuj

Przekazuje do bloku każdy element kolekcji. Zwraca true, jeśli blok nigdy nie zwróci false (lub nil).

[1, 2, 5].all? { |element| element <= 5 }                     #=> true
[1, 2, 5].all? { |element| element <= 4 }                     #=> false
%w{RAM CPU GPU DDR}.all? { |element| element.length == 3 }    #=> true

[1, 2, 5].all? do |element|
  puts "Sprawdzam #{element}; #{element} < 5: #{element < 5}"
  element < 5
end                                                           #=> false

Wyjście:

Sprawdzam 1; 1 < 5: true
Sprawdzam 2; 2 < 5: true
Sprawdzam 5; 5 < 5: false

any?Edytuj

Zwraca true, jeśli przekazany do bloku element kiedykolwiek zwróci true.

[1, 2, 5].any? { |element| element > 5 }    # => false
[1, 2, 5].any? { |element| element == 2}    # => true

collect(map)Edytuj

Przekazuje do bloku każdy element kolekcji, następnie tworzy nową - z elementów zwracanych przez blok.

%w{kot tulipan parowka}.collect { |element| element.upcase }
# => ["KOT", "TULIPAN", "PAROWKA"]
[1, 2, 3].collect { |element| element + 1}
#=> [2, 3, 4]

collect!(map!)Edytuj

Działa jak collect, z tą jednak różnicą, że operacji kolekcja dokonuje na sobie, w każdej iteracji zmieniając swoją zawartość.

a = [1, 2, 3]                            #=> [1, 2, 3]
a.collect! { |element| element + 1 }     #=> [2, 3, 4]
a                                        #=> [2, 3, 4]

delete_ifEdytuj

Usuwa z kolekcji elementy, dla których blok zwraca true

[1, 2, 3, 4, 5, 6].delete_if { |i| i%2 == 0 }   # => [1, 3, 5]

detect(find)Edytuj

Zwraca pierwszy element, dla którego blok zwróci true

(36..100).detect { |i| i%7 == 0 }     # => 42

downtoEdytuj

Wykonuje blok, podając w kolejności malejącej liczby od siebie samej do podanej jako parametr.

9.downto(0) { |i| print i } #=> 9876543210

eachEdytuj

Przekazuje do bloku każdy z elementów kolekcji

['pies', 'kot', 'ryba'].each { |word| print word + " " }
(0..9).each { |i| print i }
#=> pies kot ryba 0123456789

each_indexEdytuj

Działa jak each, ale przekazuje sam indeks każdego elementu.

[3, 6, -5].each_index { |i| print i.to_s + " " }
#=> 0 1 2

each_with_indexEdytuj

Przekazuje jednocześnie element i jego indeks do bloku.

["jeden", 2, "trzy"].each_with_index do |element, index|
  puts "Indeksowi #{index} przyporzadkowalem #{element}"
end

#=> Indeksowi 0 przyporzadkowalem jeden
#   Indeksowi 1 przyporzadkowalem 2
#   Indeksowi 2 przyporzadkowalem trzy

find_allEdytuj

Zwraca wszystkie elementy kolekcji, dla których blok zwróci true.

(0..30).find_all { |i| i%9 == 0 }     #=> [0, 9, 18, 27]

grepEdytuj

Zwraca elementy spełniające dopasowanie podane jako parametr. Jeśli podano blok, przekazuje do niego tylko te elementy i zwraca tablicę zbudowaną z wartości zwracanych przez blok.

# Zwraca wyrazy zawierajace litere 'r'
%w{ruby python perl php}.grep(/r/) do |w| 
  print "#{w.upcase} "
  w.capitalize
end                         #=> ["Ruby", "Perl"]

#=> RUBY PERL

injectEdytuj

Przekazuje do bloku każdy element kolekcji. Posiada dodatkowo pamięć, która początkowo jest równa pierwszemu elementowi (lub wartości podanej jako parametr). Po zakończeniu każdej iteracji pamięć jest aktualizowana do wartości zwracanej przez blok.

# Zwraca największą liczbę z tablicy
a = [-5, 2, 10, 17, -50]
a.inject a.first do |mem, element|
  mem > element ? mem : element
end                                 #=> 17

# Silnia
(1..5).inject do |mem, element|
  mem *= element
end                                 #=> 120

partitionEdytuj

Zwraca dwie tablice: jedną z elementami, dla których blok zwraca true i drugą - z resztą.

(1..6).partition { |i| i%2 == 0 }   #=> [[2, 4, 6], [1, 3, 5]]

rejectEdytuj

Odrzuca z kolekcji wszystkie elementy, dla których blok zwróci true.

(1..10).reject { |i| i >= 3 and i <= 7 }    #=> [1, 2, 8, 9, 10]

reject!Edytuj

Wyrzuca z siebie elementy, dla których blok zwraca true.

a = (1..10).to_a                     # => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
a.reject! { |i| i >= 3 and i <= 7 }  # => [1, 2, 8, 9, 10]
a                                    # => [1, 2, 8, 9, 10]

reverse_eachEdytuj

Działa jak each tyle, że podaje elementy w odwrotnej kolejności.

(0..9).to_a.reverse_each { |i| print i }
#=> 9876543210

stepEdytuj

Przekazuje do bloku wartości od, do - z określonym krokiem.

# (1)
0.step(100, 10) { |i| puts i}
# (2)
(0..100).step(10) { |i| puts i }

W obu przypadkach wyjście będzie wyglądało tak:

0
10
20
30
40
50
60
70
80
90
100

timesEdytuj

Wykonuje dany blok określoną ilość razy.

5.times { puts "Hej!" }
5.times { |i| print "#{i} "}

#=> Hej!
#   Hej!
#   Hej!
#   Hej!
#   Hej!
#   0 1 2 3 4

uptoEdytuj

Iteruje blok, przekazując liczby od, do.

1.upto(3) { |i| print i }
#=> 123