Pętla for i przyjaciele

Mówi się, że R powstał po to, żeby nie używać pętli for. Oczywiście nie była to jedyna motywacja twórców R-a, ale faktycznie, większość rzeczy w R-ze można załatwić bez tego, korzystając np. z funkcji apply. Mimo wszystko czasem niektórym wygodniej skorzystać z fora, a przy tym jest to klasyczna konstrukcja występująca w chyba wszystkich sensownych językach programowania, więc trochę głupio byłoby jej nie wprowadzić.

Idea jest prosta - załóżmy, że chcemy zrobić tę samą rzecz (albo bardzo podobną) wielokrotnie. Na przykład napisać "spam" 20 razy. Możemy oczywiście 20 razy napisać w skrypcie:

print("spam")

ale pamiętajmy jedno z podstawowych przykazań programowania: nie powielajmy kodu. Po pierwsze, przeważnie to strata miejsca. Jak zaraz się przekonacie, można to napisać w dwóch linijkach zamiast w dwudziestu. Strata miejsca to oczywiście nic strasznego, nie mamy reglamentowanych kartek w edytorze tekstu, ale strata miejsca to i strata przejrzystości, a przejrzystość jest bardzo ważna w kodzie, jeśli chcemy rozumieć co się w nim dzieje nie tylko w momencie pisania, ale i parę tygodni / dni / godzin / minut później. Po drugie, jeżeli okaże się, że się pomyliliśmy przy kopiowaniu, albo po jakimś czasie się odmyśliliśmy i jednak chcemy pisać "spam!" zamiast "spam", albo okazało się, że program jednak nie robi tego, co chcemy - jeżeli powielamy kod trzeba potem poprawiać błąd w każdym miejscu, gdzie on występuje, zamiast w jednym. Pół biedy, jeśli jest to po prostu dwadzieścia linijek pod rząd tego samego, można to przeedytować w każdym edytorze co najmniej tak sprawnym jak notatnik zaznaczając dany fragment i używając jakiegoś polecenia w rodzaju "znajdź i zamień". Ale co jeśli beztroskie kopiowanie kodu tak nam wejdzie w krew, że zaczniemy kopiować fragmenty kodu w różne miejsca skryptu, czasem nieznacznie modyfikując, aż wytropienie wszystkich miejsc gdzie chcemy rzeczywiście poprawić "spam" na "spam!" stanie się niemal niemożliwe? Brr. Nie, nie kopiuje się kodu. W rozdziale o funkcjach nauczysz się jeszcze więcej, jak tego unikać, a tymczasem  istotna myśl do zapamiętania, albo wygrawerowania i powieszenia nad łóżkiem: "nie powiela się kodu".

Napisanie "spam" dwadzieścia razy przy użyciu pętli for wyglądałoby tak:

> for (licznik in seq(20)) {
+  print("spam")
+ }

Z rozdziału o wektorach wiesz już, że seq(20) to wektor liczb całkowitych od 1 do 20. Można by też niapisać 1:20 zamiast seq(20). R tworzy sobie ten wektor i zabiera się do roboty. Najpierw na zmienną licznik przypisuje pierwszą wartość z danego wektora, w tym wypadku 1. Potem robi wszystko, co jest wewnątrz pętli for, czyli wewnątrz klamrowych nawiasów. W tym wypadku po prostu drukuje napis "spam". Kiedy zrobi już wszystkie polecenia z pętli, czyli skończy się pierwszy obieg pętli, wraca na początek: tym razem pod zmienną licznik podstawia drugą wartość z wektora seq(20). I znowu wykonuje wszystkie polecenia w pętli. Po drugim obiegu pętli wraca na początek, pod zmienną licznik podstawia kolejną wartość z wektora, i tak dalej, i tak dalej. Kiedy wreszcie skończą mu się wartości w wektorze, kończy.

Wynikiem działania pętli jest więc wypisanie na ekranie 20 razy "spam". Ubocznym efektem jest stworzenie zmiennej licznik, która po zakończeniu wszystkich obiegów pętli ma teraz wartość 20. (Możesz wpisać "licznik" i nacisnąć enter, żeby R pokazał Ci wartość tej zmiennej, i sprawdzić czy nie oszukuję.)

Ta konstrukcja: jakiśtam napis, potem nawias {, potem seria instrukcji, a potem }, powinna coś przypominać. Konkretnie, powinna przypominać instrukcję warunkową if. Rzeczywiście, działa to podobnie jak przy ifie, analogicznie więc z instrukcją warunkową możemy napisać tę pętlę np. tak:

> for (licznik in seq(20))
+      print("spam")                # oczywiscie zadziala tylko jesli mamy jedna instrukcje w petli!

Albo tak:

> for (licznik in seq(20))
+ {
+  print("spam")
+ }

Albo, aż głupio to pisać ale dla porządku napiszę, tak:

> for (licznik in seq(20)) print("spam")

Ale *bardzo* odradzam pisania takich instrukcji w jednej linijce. Czytelność przede wszystkim!

W powyższym przykładzie zmienna licznik przyjmowała kolejne wartości od 1 do 20, ale w ogóle z niej nie korzystaliśmy. Była nam potrzebna tylko po to, żeby pętla wykonała się dokładnie 20 razy; licznik - jak sama nazwa wskazuje - tylko liczył, w którym obiegu pętli jesteśmy. Napiszmy teraz pętlę, w której zmienna "podstawa_potegowania" będzie przyjmować kolejne wartości z pewnego wektora, a potem będzie podnoszona do 3 potęgi.

> for ( podstawa_potegowania in c(1,3,5,10)) {
+  print( podstawa_potegowania**3 )
+ }
[1] 1
[1] 27
[1] 125
[1] 1000

Dla każdej wartości w wektorze po słowie "in" (czyli u nas c(1,3,5,10)) R wykona wszystkie polecenia w klamrowych nawiasach, przyjmując ową wartość jako wartość zmiennej przed słowem "in" (tutaj podstawa_potegowania). Stąd owa składnia "for [coś] in [zbior cosiów]".

Co ciekawe, nie zadziała w ten sposób:

> for ( podstawa_potegowania in c(1,3,5,10)) {
+  podstawa_potegowania**3
+ }

Jest to podobna sytuacja do tej, na jaką zwracałam uwagę w rozdziale o skryptach. Kiedy piszemy R-owi polecenie, R drukuje nam jego wynik. W tym wypadku jednak z punktu widzenia R-a polecenie to cała pętla for, która wyniku żadnego nie ma; to, że po drodze dzieją się jakieś obliczenia (konkretnie podnoszenie do potęgi) niespecjalnie R-a obchodzi, chciałby nam tylko wydrukować wynik całego polecenia. A przecież zdaniem R-a polecenie skończyło się dopiero na zamykającym nawiasie klamrowym, o czym najlepiej świadczą plusy na początkach linii; linia z potęgowaniem nie była jeszcze gotowym poleceniem.

W R-ze mamy jeszcze do dyspozycji pętlę while. "while" oznacza "dopóki" i idea tej pętli jest prosta: R ma wykonywać jakiś ciąg instrukcji, dopóki spełniony będzie jakiś podany przez nas warunek. Przykładowo:

> licznik = 1
> while( licznik <= 20 ) {
+     print("spam")
+     licznik = licznik + 1
+ }

Co się po kolei tu dzieje: najpierw na zmienną licznik przypisujemy 1; będziemy za jej pomocą liczyć, ile razy pętla się wykonała. Zaczynamy pętlę: mówimy R-owi, że ma tak długo wykonywać w kółko instrukcje z pętli, jak długo spełniony jest warunek w nawiasach: czyli jak długo zmienna licznik jest mniejsza od lub równa 20. R więc zanim jeszcze zacznie wykonywać cokolwiek z pętli sprawdza, czy licznik jest mniejszy od 20. Jest, więc wchodzi w pętlę, drukuje "spam", a potem zwiększa zmienną licznik o jeden. Doszedł do końca instrukcji z pętli (wie o tym dzięki nawiasowi klamrowemu), więc sprawdza znowu warunek, żeby wiedzieć, czy dalej ma wykonywać pętlę: licznik dalej jest mniejszy niż 20, więc znowu drukuje "spam", znowu powiększa licznik o jeden etc. Wreszcie w dwudziestym obiegu pętli po raz dwudziesty wydrukuje "spam", po raz dwudziesty zwiększy licznik o 1 (teraz będzie równy 21) i po raz dwudziesty sprawdzi, czy ma zaczynać kolejny obieg pętli. Ale warunek już nie jest spełniony: 21 > 20, więc R przestanie pętlić.

Można zrobić głupią pętlę while, która nie wykona się nigdy,  bo podany w nawiasie warunek będzie zawsze fałszywy, np.:

> while( 1 < 0 ) {
+   print("wow, jeden jest mniejsze od zera!")
+ }

Albo:

> while(FALSE) {
+    print("Bez sensu w ogole wymyslac napis ktory sie nie wydrukuje.")
+ }

Można też (aczkolwiek nie polecam) zrobić głupią pętlę while, która będzie się wykonywać w nieskończoność, bo warunek w nawiasie będzie zawsze prawdziwy:

> while(TRUE) {
+    print("A ten napis lepiej zeby byl ladny, bo bedzie go duzo...")
+ }

Jeżeli zrobiłeś coś takiego i właśnie R wyświetla Ci nieskończoną liczbę napisów można to zatrzymać naciskając control + c.

W nawiasach pętli while może być absolutnie cokolwiek, co po wyliczeniu przez R-a da albo TRUE, albo FALSE: może to być prosty warunek na porównywanie liczb (jak licznik <= 20, albo trochę bezsensowne 1 < 0), ale może być jakaś skomplikowana funkcja, wyliczająca TRUE albo FALSE.

Pętle for, while oraz instrukcję warunkową if można oczywiście zagnieżdżać w sobie. Trzeba uważać wtedy z zamykaniem nawiasów klamrowych. Żeby się nie pogubić bardzo pomaga robienie wcięć na początku linii: wszystkie instrukcje z danego bloku powinny być tak samo wcięte, jeśli jakaś instrukcja zaczyna kolejny blok, będzie on wcięty o kolejną porcję spacji / tabulatorów. Przykład takich pozagnieżdżanych instrukcji poniżej:

for (liczba in 1:5) {
+      print( paste("liczba:", liczba) )
+      licznik = 1
+      while (licznik < liczba) {
+            print( paste( "    Jestem w while'u, bo licznik wynosi", licznik, "czyli mniej od liczby, wynoszacej ", liczba) )
+            licznik = licznik + 1
+            print("sprawdze czy licznik jest podzielny przez dwa")
+            if (licznik %% 2 == 1) {
+                  print("           licznik jest niepodzielny przez dwa")
+            } else {
+                  print("           jest podzielny!")
+            }
+      }
+      print("wyszedlem z while'a, koncze ten obieg petli")
+ }

Funkcja paste skleja napisy w jeden, co jest pomocne, gdy chcemy do napisu dokleić wartość jakiejś zmiennej, jak tutaj. Ten kawałek kodu wiele mądrego nie robi, ale robiąc to przy okazji informuje, gdzie akurat w kodzie się znajduje i dlaczego. Kiedy piszę bardziej skomplikowane kawałki kodu i coś mi w nim przestaje działać, dopisywanie takich printów w różnych miejscach bardzo mi pomaga: dzięki nim wiem, ile wynosiły zmienne w różnych fragmentach skryptu, czy na pewno tyle, ile się spodziewałam, w którym konkretnie miejscu program się pomylił etc. Albo kiedy dostaję od kogoś kod napisany niezbyt czytelnie, a zależy mi na jego zrozumieniu, dopisuję takie charakterystyczne printy i uruchamiam go, dzięki temu program sam mnie informuje co robi. Polecam taki sposób analizy.

Funkcja for przerywa działanie, kiedy skończą jej się elementy w wektorze, funkcja while, kiedy podany jej warunek przestaje być spełniony; czy są inne sposoby przerwania funkcji? Są. Można przerwać funkcję poleceniem break:

> oceny = c("florek" = 1, "florcia" = 2, "hermenegilda" = 5, "pszemek" = 3, "rainbow dash" = 3)
> for (osoba in names(oceny) ) {
+    print( paste(osoba, ":", oceny[osoba] ) )
+    if (oceny[osoba] == 5) {
+       print("jest ktos z ocena bdb! No to konczymy")
+       break
+    }
+ }

Podobnie można przerwać pętlę while. Wydaje się dosyć intuicyjne, że raczej zawsze z instrukcji "break" będziemy korzystać wewnątrz jakiegoś ifa - jaki sens ma wprowadzać break bez instrukcji warunkowej, czyli takiego, który nie potrzebuje żadnego warunku, żeby się wykonać? W ten sposób pętla wykona się zawsze tylko raz, to po co w ogóle robić z niej pętlę.

Jest jeszcze jedna pętla w R-ze, chyba najrzadziej używana. Jest to pętla repeat, która działa jak while(TRUE), czyli wykonuje się w nieskończoność, i jedyny sposób, w jaki można ją przerwać, to za pomocą instrukcji break:

repeat {
+   print(paste("to juz", ktory_obieg_petli, ". obieg petli", sep = ""))
+   ktory_obieg_petli = ktory_obieg_petli + 1
+   if (ktory_obieg_petli > 100) {
+     print("dosc, ile mozna! Konczymy!")
+     break
+   }
+ }

Zauważ, że w funkcji paste dopisałam argument sep, czyli separator: jest to napis, który funkcja paste wkleja pomiędzy wszystkie napisy, które daliśmy jej do posklejania. Domyślnie jest to spacja, ale głupio by wyglądała spacja między numerem obiegu a kropką, więc kazałam jej wstawiać pusty napis, czyli po prostu nic.

Teraz, kiedy wiesz wszystko, co warto wiedzieć o pętlach, spróbuj zmierzyć się z zadaniem Szachownica.

PS. Być może ciekawi Cię, jak napisać "spam" 20 razy nie korzystając z pętli for. O tak:

> cat( paste( rep("spam", 20), collapse = "\n" ) )

cat() to funkcja podobna do print, ale interpretuje znaki specjalne, czyli znaki, które znaczą trochę co innego, niż wyglądają: np. znak "\n" jest znakiem specjalnym oznaczającym nową linię. paste(), jak już było mówione służy do sklejania kilku napisów w jeden. Argument collapse informuje ją, co ma powciskać między kolejne elementy wektora rep("spam",20) - wciskamy tam znak specjalny "\n", który oznacza nową linię. Różnicę między collapse a sep widać na poniższych przykładach:

> paste("a","b","c", sep = "_", collapse = "--")
[1] "a_b_c"
> paste( c("a","b","c"), sep = "_", collapse = "--")
[1] "a--b--c"
> paste( c("a", "b", "c"), c("A","B","C") )
[1] "a A" "b B" "c C"
> paste( c("a", "b", "c"), c("A","B","C"), sep = "_", collapse="--" )
[1] "a_A--b_B--c_C"

A różnicę między cat a print na poniższych:

> napis = "To \t jest \n jakiś \\ napis ze \"znakami specjalnymi\" \n"
> print(napis)
[1] "To \t jest \n jakiś \\ napis ze \"znakami specjalnymi\" \n"
> cat(napis)
To       jest 
 jakiś \ napis ze "znakami specjalnymi"