8 Powtarzanie
W sekcji 1.2 zostały wspomniane różne istniejące paradygmaty programowania.
Pętle for
czy while
(sekcje 8.1 i 8.2) są przykładami programowania imperatywnego, gdzie program komputerowy postrzegany jest jako ciąg poleceń dla komputera.
Alternatywą do tego sposobu działania jest programowanie funkcyjne, w którym rozwiązanie pewnego problemu jest oparte o użycie lub stworzenie odpowiedniej funkcji (sekcja 8.3).
Język R pozwala na stosowanie zarówno paradygmatu imperatywnego jak i paradygmatu funkcyjnego37.
8.1 Pętla for
Pętla for
jest jednym z najczęściej używanych wyrażeń w językach programowania38, którego celem jest powtórzenie pewnej operacji o znaną liczbę razy.
8.1.1 Składnia
Pętla for
jest zbudowana z dwóch elementów: nagłówka określającego powtórzenia, oraz ciała zawierającego obliczenia.
for (element in wektor) {
przetwarzanie elementu }
8.1.2 Przykład działania
Zobaczmy jak działa pętla for
na uproszczonym przykładzie zamiany wartości odległości z mil lądowych na kilometry.
Nasze dane wejściowe to lista składająca się z trzech wartości - 142, 63, oraz 121.
Wiemy też, że jedna mila lądowa to 1,609 kilometra.
list(142, 63, 121) odl_mile =
Pętla for
może być użyta w tym przypadku na kilka sposobów.
Na początku warto zastanowić się w jaki sposób można zamienić tylko jedną wartość z powyższej listy.
Wiemy, że do wybrania jednego elementu z listy służy operator [[]]
(sekcja 7.3.4), więc przeliczenie i wyświetlenie tylko pierwszego elementu można wykonać poprzez:
print(odl_mile[[1]] * 1.609)
#> [1] 228
Teraz naszym celem jest potworzenie tej operacji dla każdego elementu.
print(odl_mile[[1]] * 1.609)
#> [1] 228
print(odl_mile[[2]] * 1.609)
#> [1] 101
print(odl_mile[[3]] * 1.609)
#> [1] 195
W powyższym przypadku mamy tylko trzy elementy, ale jeżeli mielibyśmy takich elementów 1000 musielibyśmy powtórzyć niemal tą samą linię kodu tysiąc razy jedynie zamieniając numer elementu.
Jednym z celów programowania jest ułatwienie szybkiej powtarzalności pewnych czynności.
Dlatego w tym przypadku moglibyśmy uniknąć wielokrotnego pisania podobnego kodu używając pętli for
.
Ciałem tej pętli będzie sposób przeliczania i wyświetlania wartości na kilometry, ale zamiast wydzielać kolejne elementy listy ([[1]]
, [[2]]
, [[3]]
), użyjemy nowego obiektu i
.
W efekcie nowe ciało pętli for
będzie przedstawiać się jako print(odl_mile[[i]] * 1.609)
.
Kolejnym krokiem jest odpowiednie ustawienie jakie wartości będzie przyjmować i
w kolejnych powtórzeniach.
for (i in 1:3) {
print(odl_mile[[i]] * 1.609)
}#> [1] 228
#> [1] 101
#> [1] 195
Powyższy nagłówek pętli for
, for (i in 1:3)
, określa, że nasz obiekt i
przyjmie najpierw wartość 1, wykona obliczenie wewnątrz pętli, następnie i
przyjmie wartość 2, znów wykona obliczenie, a na końcu i
przyjmie wartość 3 i obliczenie zostanie wykonane po raz ostatni.
for
nazywana jest i
, a w przypadku zagnieżdżonych pętli druga zmienna nazywana jest j
.
Nazywanie zmiennych w ten sposób nie jest jednak obowiązkowe.
W powyższym przypadku możliwe byłoby nazwanie zmiennej, np. pomiar
: for (pomiar in odl_mile) {...}
.
Użyty wyżej kod wykonuje nasz cel, ale wymaga od nas zawsze deklaracji dotyczącej tego jakie wartości ma przyjąć obiekt i
.
W przypadku, gdy obiekt wejściowy odl_mile
będzie krótszy lub dłuższy niż trzy elementy, będziemy musieli ręcznie zmienić nagłówek pętli for
.
Aby tego uniknąć możemy automatycznie określić wszystkie pozycje elementów w liście odl_mile
używając funkcji seq_along()
.
Ta funkcja zawsze wyświetli numery położenia kolejnych elementów danego wektora lub listy.
seq_along(odl_mile)
#> [1] 1 2 3
Poniższy kod nie wymaga już od nas ręcznego wprowadzania kolejnych położeń elementów wejściowej listy.
for (i in seq_along(odl_mile)) {
print(odl_mile[[i]] * 1.609)
}#> [1] 228
#> [1] 101
#> [1] 195
1:length()
, np. 1:length(old_mile)
.
Zadziała ona poprawnie w powyższym przypadku, ale nie jest ona uniwersalna.
Konstrukcja 1:length()
może wywołać problemy w kodzie, gdy wejściowy obiekt jest pusty.
for (i in 1:length(NULL)){...}
wykona pętlę for
dwa razy, podczas gdy w rzeczywistości nie powinna ona zostać w ogóle wykonana.
Funkcja seq_along()
jest odporna na ten problem - seq_along(NULL)
nie wykona pętli ani razu.
Wcześniejsze przykłady wyświetlały przeliczone na kilometry kolejne elementy listy odl_mile
.
Możliwe było zobaczenie nowych wartości, ale nie zostawały one w pamięci komputera - w efekcie nie można było wykorzystać wyników działania pętli for
w przyszłości.
Co w takim razie należy zrobić, aby wynik dało się użyć dalej?
Jednym z podejść jest modyfikacja istniejącej list odl_mile
.
Poniższa pętla for
zastępuje kolejne wartości z obiektu odl_mile
na kilometry.
for (i in seq_along(odl_mile)) {
odl_mile[[i]] * 1.609
odl_mile[[i]] =
}
odl_mile#> [[1]]
#> [1] 228
#>
#> [[2]]
#> [1] 101
#>
#> [[3]]
#> [1] 195
Niestety, w efekcie stracone zostały oryginalne wartości w milach lądowych.
list(142, 63, 121) odl_mile =
Aby zostawić oryginalne wartości w milach lądowych, ale też stworzyć nowy obiekt określony w kilometrach musimy stworzyć nowy, pusty obiekt, a następnie wypełnić go wartościami.
Poniżej nazwany on został odl_km
- jest to pusta lista.
Następnie kolejne wykonania pętli for
doklejają kolejne elementy do tej listy.
vector("list", length = 0)
odl_km =for (i in seq_along(odl_mile)) {
c(odl_km, odl_mile[[i]] * 1.609)
odl_km =
}
odl_km#> [[1]]
#> [1] 228
#>
#> [[2]]
#> [1] 101
#>
#> [[3]]
#> [1] 195
Efektem jest poprawne rozwiązanie naszego problemu, ale niestety posiada ono istotną wadę - to rozwiązanie nie jest bardzo wydajne. Za każdym przejściem pętli następuje bowiem alokacja pamięci, co zabiera niepotrzebnie czas. Więcej informacji na ten temat można znaleźć w rozdziale 11.
Lepszym rozwiązaniem w takiej sytuacji jest od razu stworzenie listy, o długości zgodnej z naszym oczekiwaniem. Następnie kolejne elementy stworzonej listy są zamieniane na oczekiwane przez nas wartości.
vector("list", length = length(odl_mile))
odl_km =for (i in seq_along(odl_mile)) {
odl_mile[[i]] * 1.609
odl_km[[i]] =
}
odl_km#> [[1]]
#> [1] 228
#>
#> [[2]]
#> [1] 101
#>
#> [[3]]
#> [1] 195
8.1.3 Zastosowanie w funkcjach
Pętle for
, podobnie jak wyrażenia warunkowe (sekcja 4.4), w naturalny sposób są stosowane w funkcjach.
Przykładowo, możemy stworzyć nową funkcję mile_na_km()
, która przyjmuje listę z wartościami w milach lądowych jako obiekt wejściowy, a później zwraca listę z wartościami w kilometrach.
function(odl_mile) {
mile_na_km = vector("list", length = length(odl_mile))
odl_km =for (i in seq_along(odl_mile)) {
odl_mile[[i]] * 1.609
odl_km[[i]] =
}
odl_km }
Sprawdźmy działanie funkcji na prostym przykładzie listy z pięcioma elementami.
list(0, 1, 10, 55, 160)
odleglosci_mile =mile_na_km(odleglosci_mile)
#> [[1]]
#> [1] 0
#>
#> [[2]]
#> [1] 1.61
#>
#> [[3]]
#> [1] 16.1
#>
#> [[4]]
#> [1] 88.5
#>
#> [[5]]
#> [1] 257
Zgodnie z oczekiwaniami zero mil lądowych to również zero kilometrów, a jedna mila lądowa to 1,609 kilometra.
8.2 Pętla while
W przypadku pętli for
znana jest liczba powtórzeń przed rozpoczęciem jej działania.
Inny rodzaj pętli, pętla while
, jest natomiast stosowany gdy nie wiadomo ile potworzeń jest koniecznych.
W efekcie pętla while
jest bardziej elastyczna, co jest zarazem jej atutem i wadą.
Bardziej elastyczne metody charakteryzuje większa liczba potencjalnych sytuacji do których mogą zostać użyte, ale w efekcie też więcej potencjalnych problemów.
Pętla while
powinna być używana tylko gdy rozwiązanie z użyciem pętli for
nie jest możliwe.
Pętla while
składa się z nagłówka definiującego pewien warunek oraz ciała określającego operację do wykonania.
Pętla ta będzie tak długo powtarzana jak długo warunek będzie spełniony - dlatego też w ciele pętli musi być jakiś mechanizm zmieniający wartość wpływającą na warunek.
while (warunek){
wykonuj operację tak długo jak warunek jest spełniony }
Wyobraźmy sobie poniższą sytuację.
Mamy 1000 zł (obiekt budzet
) i chcemy zainwestować te pieniądze na giełdzie w celu ich pomnożenia.
Interesują nas tylko dwa scenariusze - jeden w którym tracimy całą kwotę, oraz drugi w którym udaje się nam podwoić tę kwotę.
Wiemy też jedną dodatkową rzecz - losowe wahania na giełdzie mogą pozwolić nam na stratę maksymalnie 100 zł aż do zysku 100 zł każdego dnia.
Poniższy kod wykonuje pętlę while
tak długo jak obiekt budzet
ma wartość większą od zera i mniejszą od 2000.
1000
budzet = 0
liczba_dni =while(budzet > 0 && budzet < 2000){
budzet + sample(-100:100, size = 1) # losowa strata lub zysk
budzet = liczba_dni + 1
liczba_dni = }
Po jego wykonaniu możemy dowiedzieć się czy udało się nam zarobić czy też stracić całe pieniądze. Dodatkowo możemy sprawdzić ile zajęło to dni.
budzet#> [1] -33
liczba_dni#> [1] 522
repeat
oraz pętla do
.
Pętla repeat
powtarza pewien kod aż do momentu przerwania go przez użytkownika (np. użycie klawisza Esc) lub do pojawienia się komendy break
.
Działanie pętli do
natomiast wygląda w następujący sposób:
do {wykonuj operację} while (warunek)
.
Pętla do
nie występuje w R.
Dodatkowe informacje na temat pętli for and while można znaleźć w sekcji Loops książki Advanced R (Wickham 2014)
8.3 Programowanie funkcyjne
Sprawdźmy działanie programowania funkcyjnego na dwóch przykładach.
W pierwszym posiadamy listę pomiary_f_lista
składającą się z trzech elementów - każdy z nich to wektor z trzema pomiarami temperatury w stopniach Fahrenheita.
list(
pomiary_f_lista =miastoA = c(61, 14, 21),
miastoB = c(43, 52, 30),
miastoC = c(41, 42, 33)
)
pomiary_f_lista#> $miastoA
#> [1] 61 14 21
#>
#> $miastoB
#> [1] 43 52 30
#>
#> $miastoC
#> [1] 41 42 33
Naszym celem jest zamiana tych wartości na stopnie Celsjusza.
Używając paradygmatu imperatywnego, moglibyśmy zastosować pętlę for
i zastosować przeliczenie wartości dla kolejnych elementów listy.
W paradygmacie funkcyjnym natomiast naszym pierwszym krokiem jest stworzenie funkcji wykonującej podstawową operację:
function(temperatura_f){
konwersja_f_to_c =- 32) / 1.8
(temperatura_f }
Funkcje użyte w programowaniu funkcyjnym muszą spełniać dwa warunki:
- Wynik działania funkcji musi zależeć od obiektu wejściowego, czyli gdy dwa razy uruchomimy tą samą funkcję na tych samych danych musimy dostać ten sam wynik. Taka funkcja nie może mieć w sobie, np. elementu losowego.
- Funkcja nie może mieć efektów ubocznych (ang. side-effects), czyli wykonywać jakiegoś działania w tle, jak np. wyświetlanie czy zapisywanie na dysk.
Powyższa funkcja konwersja_f_to_c()
działa poprawnie na wektorach wartości, ale niestety nie jest w stanie zwrócić wyniku w przypadku listy, co obrazuje komunikat błędu.
konwersja_f_to_c(pomiary_f_lista)
#> Error in temperatura_f - 32: non-numeric argument to binary operator
Języki obsługujące programowanie funkcyjne posiadają jednak szereg narzędzi do przetwarzania funkcji, które zbiorczo są nazywane funkcjonałami (ang. functional). Funkcjonały to funkcje, które przyjmują inne funkcje jako argumenty.
lapply()
i apply()
, istnieją również takie funkcje jak sapply()
, vapply()
, tapply()
, mapply()
i inne.
Jednym z podstawowych funkcjonałów w R jest lapply()
.
Funkcjonał lapply()
przyjmuje jako pierwszy argument wektor atomowy lub listę, a następnie przetwarza go używając funkcji podanej jako drugi argument FUN
.
Poniżej, lapply()
wykonuje funkcję konwersja_f_to_c()
na kolejnych elementach listy pomiary_f_lista
i zwraca nową listę zawierającą wyniki
lapply(pomiary_f_lista, FUN = konwersja_f_to_c)
pomiary_c_lista =
pomiary_c_lista#> $miastoA
#> [1] 16.11 -10.00 -6.11
#>
#> $miastoB
#> [1] 6.11 11.11 -1.11
#>
#> $miastoC
#> [1] 5.000 5.556 0.556
Programowanie funkcyjne można też stosować do innych klas obiektów.
Poniższa ramka danych pomiary
zawiera trzy kolumny z pomiarami temperatury dla kolejnych miast.
Dla każdego miasta wykonano jeden pomiar dziennie.
data.frame(
pomiary =miastoA = c(6.1, 1.4, -2.1),
miastoB = c(4.3, 5.2, 3.0),
miastoC = c(4.1, 4.2, 3.3)
)
pomiary#> miastoA miastoB miastoC
#> 1 6.1 4.3 4.1
#> 2 1.4 5.2 4.2
#> 3 -2.1 3.0 3.3
Naszym celem jest wyliczenie średnich - zarówno średniej wartości dla każdego miasta (kolumny) oraz średniej wartości dla każdego dnia (wiersze).
Możemy to zrobić używając pętli for
.
Najpierw tworzymy pusty wektor sr_miasto
o długości oczekiwanego wyniku, a następnie wyliczamy średnią dla kolejnych kolumn i dodajemy ją do tego wektora.
vector("numeric", length = ncol(pomiary))
sr_miasto =for(i in seq_len(ncol(pomiary))){
mean(pomiary[, i])
sr_miasto[i] =
}
sr_miasto#> [1] 1.80 4.17 3.87
W kolejnym kroku tworzymy pusty wektor sr_dzien
również o długości oczekiwanego wyniku, a następnie wyliczamy średnią dla kolejnych wierszy i dodajemy ją do tego wektora.
vector("numeric", length = nrow(pomiary))
sr_dzien =for(i in seq_len(nrow(pomiary))){
mean(unlist(pomiary[i, ]))
sr_dzien[i] =
}
sr_dzien#> [1] 4.83 3.60 1.40
Alternatywą w takich przypadkach jest użycie programowania funkcyjnego, a w szczególności funkcjonału apply()
.
Oczekuje on co najmniej trzech argumentów, X
- obiektu wejściowego którym mogą być między innymi ramki danych czy macierze, MARGIN
określającego czy wartości będą grupowane po wierszach czy kolumnach, oraz FUN
zawierającego używaną funkcję.
W poniższym przypadku obiektem wejściowym jest ramka danych pomiary
, MARGIN = 2
oznacza wyliczanie oddzielnie dla kolejnych kolumn przy użyciu zdefiniowanej funkcji mean()
.
apply(pomiary, MARGIN = 2, FUN = mean)
#> miastoA miastoB miastoC
#> 1.80 4.17 3.87
Podobne obliczenie, ale dla kolejnych wierszy można uzyskać zamieniając argument MARGIN
na 1
.
apply(pomiary, MARGIN = 1, FUN = mean)
#> [1] 4.83 3.60 1.40
lapply()
w pakiecie purrr jest funkcja map()
.
Ma ona dodatkowo kilka kolejnych wariantów, np. map_df()
- która przyjmuje jako wejście listy, ale zwraca ramki danych, czy map_dbl()
- która również przyjmuje listy, ale zwraca wartości zmiennoprzecinkowe.
8.4 Zadania
- Spójrz na poniższy kod, ale nie wykonuj go.
Ile razy zostanie wyświetlony tekst
"Działa!"
?
for (i in c(1, 2, 4, 5, 6)){
if (i < 2 | i >= 5)
print("Działa!")
}
- Spójrz na poniższy kod, ale nie wykonuj go.
Ile razy zostanie wyświetlony tekst
"Działa!"
?
for (i in c(1, 2, 4, 5, 6)){
for (j in 6:3){
if (i < 2 | i >= 5)
print("Działa!")
} }
- Spójrz na poniższy kod, ale nie wykonuj go.
Ile razy zostanie wyświetlony tekst
"Działa!"
?
for (i in c(1, 2, 4, 5, 6)){
for (j in 6:3){
if (i < 2 & j >= 5)
print("Działa!")
} }
- Spójrz na poniższy kod, ale nie wykonuj go.
Ile razy zostanie wyświetlony tekst
"Działa!"
?
for (i in c(1, 2, 4, 5, 6)){
for (j in 6:3){
if (i < 2 | j >= 5)
print("Działa!")
} }
- Spójrz na poniższy kod, ale nie wykonuj go.
Ile razy zostanie wyświetlony tekst
"Działa!"
?
for (i in c(1, 2, 3)){
for (j in 6:3){
i + j
i =if (i < 4 | i >= 9)
print("Działa!")
} }
Posiadasz listę zawierającą wartości temperatury w stopniach Fahrenheita, która została przedstawiona na początku sekcji 8.3. Stwórz pętlę
for
, która zamieni te wartości na stopnie Celsjusza.Rozwiąż powyższe zadanie używając również pętli
while
. Dodatkowo: spróbuj rozwiązać to zadanie używając pętlirepeat
.Posiadasz listę zawierającą wartości odległości w milach lądowych, która została przedstawiona na początku sekcji 8.1.2. Użyj metod programowania funkcyjnego, żeby przeliczyć jej wartości na kilometry.
Posiadasz ramkę danych
pomiary
zawierającą wartości temperatury dla kolejnych miast, która została przedstawiona w sekcji 8.3. Napisz dwie funkcje używając pętlifor
- jedna, która znajdzie drugą najwyższą wartość w każdym wierszu oraz druga, która znajdzie drugą najwyższą wartość w każdej kolumnie. (Tutaj może pomocna okazać się funkcjasort()
.)Rozwiąż poprzednie zadanie korzystając z metod programowania funkcyjnego zamiast pętli
for
.Posiadasz macierz o wymiarach czterech wierszy na sześć kolumn składającą się z kolejnych liter alfabetu.
matrix(LETTERS[1:24], ncol = 6, nrow = 4)
m =
m#> [,1] [,2] [,3] [,4] [,5] [,6]
#> [1,] "A" "E" "I" "M" "Q" "U"
#> [2,] "B" "F" "J" "N" "R" "V"
#> [3,] "C" "G" "K" "O" "S" "W"
#> [4,] "D" "H" "L" "P" "T" "X"
Napisz funkcję wykorzystującą pętle for
aby określić “sąsiadów” kolejnych liter wykorzystując sąsiedztwo oparte tylko o wspólną krawędź (ang. 4-neighborhood).
Przykładowo, sąsiadem litery “A” są litery “E” i “B”.
- Przepisz powyższą funkcję, aby wykorzystywała sąsiedztwo oparte także o wspólne punkty (ang. 8-neighborhood). Przykładowo, sąsiadem litery “A” są litery “E”, “B” i “F”.
Bibliografia
Henry, Lionel, and Hadley Wickham. 2019. Purrr: Functional Programming Tools. https://CRAN.R-project.org/package=purrr.
Wickham, Hadley. 2014. Advanced R. Chapman and Hall/CRC.
R również obsługuje paradygmat obiektowy.↩︎