Pętle i iteratory w Rubim, część 1

Posted by Jacek Galanciak on

Ruby jest językiem dającym programiście niesamowite pole do popisu i ogromną swobodę, jeśli chodzi o pętle i iteratory. Pomimo odmiennego sposobu działania, opisane będą w jednym poście ze względu na podobne zastosowania. Część druga będzie nieco bardziej zaawansowanym spojrzeniem na to zagadnienie, uwzględniającym m. in. budowanie własnych iteratorów.

while i until

Zacznijmy od klasyki.

i = 0
while i < 10
  puts i
  i += 1
end

Tak, jak w innych językach programowania, while wykonuje ciało pętli zero lub więcej razy, gdy warunek pętli jest spełniony. Czasem jednak musimy mieć pewność, że pętla wykona się co najmniej raz. Nie trzeba rozróżniać, tak jak się to robi w C, pętli while.. od do..while, gdyż elastyczność Rubiego sprawia, że podobna konstrukcja jest niczym innym, jak modyfikatorem wyrażenia while (o którym dokładniej i ogólniej za chwilę):

i = 0
begin
  i += 1
  puts i
end while i < 10

W Rubim stawiamy na czytelność i intuicyjność. Czasem będziemy potrzebowali wykonać pętlę o warunku przeciwnym. Nie mówimy przecież “wykonuj wczytywanie gdy nie wczytane równe "koniec”, brzmi to zbyt nieludzko. Bardziej jesteśmy skłonni powiedzieć: wykonuj wczytywanie dopóki wczytane słowo nie będzie równe “koniec” [język polski posiada podwójne zaprzeczenia, musiałem tutaj umieścić słowo “nie”, pomimo że w kodzie nie ma żadnego zaprzeczenia]. A Ruby jest dla ludzi:

puts "Popisz sobie :)"
until input = gets.chomp == "koniec"
  puts "Jak sie znudzisz, wpisz \"koniec\""
end

O wyrażeniach dowiemy się wkrótce, teraz będzie tylko szybkie wyjaśnienie. gets pobiera z wejścia String aż do separatora linii (wraz z tym separatorem). Ponieważ jest to String (pisane celowo wielką literą - pamiętajmy, że to obiekt!), posiada on metodę chomp, która usuwa separatory z jego końca. Powyższy program wczytuje dane i zachęca do zakończenia swojej pracy dopóki nie wklepiemy mu odpowiedniego tekstu.

Podobnie jak pętla while, instrukcja until pozwala nam na sprawdzenie warunku po wykonaniu swojego ciała:

begin
  a = gets
end until a.to_i == 10

Ponieważ a jest obiektem klasy String, można go bez problemu konwertować na liczby całkowite - do tego właśnie służy nam metoda to_i.

Modyfikatory wyrażeń while i until

Niekiedy chcemy wykonywać daną instrukcję, /gdy|dopóki/ spełniony jest jakiś warunek. Umożliwiają nam to modyfikatory pętli, których używa się w następujący sposób:

[instrukcja] while [wyrażenie]

Jeśli [instrukcja] jest całym blokiem, blok ten będzie wykonany co najmniej raz (a-ha! mamy więc wyjaśnienie odpowiednika do..while). Jeśli jednak [instrukcja] jest faktycznie pojedynczą instrukcją, wykonywana będzie tak, jak określa to pętla. Oto mały przykładzik:

a = 0

a += 1 while a < 100
puts a
# 100
a -= 1 until a == 0
puts a
# 0

break, next, redo i retry

Ruby oferuje nam szeroki wachlarz instrukcji sterujących pętlą. Instrukcje break i next działają analogicznie, jak odpowiednio break i continue z C. Pierwsza przerywa wykonywanie całej pętli, druga natomiast powoduje wykonanie iteracji pętli od początku bez ponownego sprawdzenia warunku pętli.

Ciekawą własnością tych instrukcji jest możliwość przekazania przez nie pewnej wartości. Logiczne zatem jest, że dane przekazane do break będą wartością zwracaną przez pętlę po jej przerwaniu:

res = while ln = gets
  break "Skonczylem!" if ln.chomp == "koniec"
end
puts res

Przyznam szczerze, że nie znalazłem nigdzie zastosowania przekazywania wartości poprzez next.

Konstruujemy pętlę o specyficznych mechanizmach

Ruby jest na tyle elastyczny, że udostępnia nam iterator loop (tak, jest to metoda modułu Kernel, ale tym nie wypada się w takiej chwili przejmować), w którym możemy zrobić dosłownie wszystko, nawet najmniej logiczne i bezużyteczne akcje:

i = 0
loop do
  i += 1
  next if (3..7).include? i
  break if i > 10
  puts i
end

Na wyjściu otrzymamy:

1
2
8
9
10

Jak widać if również posiada swoje modyfikatory, ale jest to tak intuicyjne, że nie tym się teraz będziemy zajmować. Sporo się natomiast dzieje w linii 4 naszego kodu. 3..7 jest obiektem typu Range, który modeluje przedział zamknięty. Posiada m. in. metodę include? sprawdzającą, czy jej argument zawiera się w przedziale. Ruby pozwala na definiowanie metod zakończonych znakiem zapytania, jeśli służą do pytania o coś, oraz wykrzyknikiem - dla niebezpiecznych akcji.

for, który w Ruby pętlą nie jest

Nawet tutaj rubin potrafi nas zaskoczyć. for jest tak naprawdę pewnym sposobem realizacji metody each, o której za chwilę. Skupmy się najpierw na for.

for i in 1..3
  puts i
end

animals = ['kot', 'pies', 'stokrotka']
for a in animals
  puts a
end

Wyjście skryptu:

1
2
3
kot
pies
stokrotka

Widać tutaj, że możemy iterować zarówno po liczbach z przedziału, jak i po elementach kolekcji (odpowiednik foreach w innych językach).

Skoro for jest realizowany przez Ruby jako metoda each, to jak będzie wyglądał ten sam program przy użyciu tejże metody?

(1..3).each do |i|
  puts i
end

animals = ['kot', 'pies', 'stokrotka']
animals.each do |a|
  puts a
end

Zapamiętajmy tutaj jedną ważną rzecz: iteratory są metodami danych klas. Ich działanie jest proste: wykonują dany im blok (do..end) dla każdego elementu danej kolekcji (np. animals); element ten jest przekazywany do bloku (między pionowymi kreskami, np. |i|) jako zmienna lokalna. Tutaj znów ujawnia się pełna obiektowość Rubiego - klasa Array i Range posiadają metodę each, która umożliwia nam iterację po poszczególnych elementach.

Standardowo Ruby posiada wiele najróżniejszych iteratorów (aczkolwiek nic nie stoi na przeszkodzie, byśmy sami sobie jakieś napisali, co zrobimy niebawem), oto najprostsze z nich:

1.upto 3 do |i|
  puts i
end

Widzimy tutaj kolejną metodę klasy Integer.

3.downto(1) { |j| puts j }

Tak, downto jest również metodą klasy Integer. W językach takich jak C++ wszelkie funkcje i metody wywołuje się z parametrami w nawiasach. Ruby też tak potrafi, ale dla poprawienia czytelności nawiasy pomijamy, jeśli instrukcja jest dostatecznie prosta i nie powoduje niejednoznaczności. Nic nie stoi na przeszkodzie, byśmy napisali puts("Ala ma tyfus"), ale po co, kiedy nie trzeba? :). Kiedy jednak wywoływana instrukcja posiada inne instrukcje w niej zagnieżdżone, albo sąsiaduje z pewnymi elementami składniowymi jak klamra (przykład wyżej) - wtedy używamy nawiasów.

A co, jeśli chcemy wykonać pętlę z krokiem innym niż 1?

0.step(10, 2) do |k|
  puts k
end

co da nam na wyjściu:

0
2
4
6
8
10

Klasa Integer posiada bardzo prostą, ale niezwykle użyteczną metodę times. Można jej używać z licznikiem, lub bez:

3.times do |i|
  puts i
end

3.times { print "bla" }

0
1
2
blablabla

Druga część wpisu wymaga wiedzy o takich elementach jak bloki i tablice, pojawi się zatem za jakiś czas.

A czy Wy wyszlifowaliście już jakąś pętlę w rubinie? :-)