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:
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:
Albo tak:
Albo, aż głupio to pisać ale dla porządku napiszę, tak:
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.
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:
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:
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.:
Albo:
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:
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:
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:
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() 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:
A różnicę między cat a print na poniższych: