Funkcje

W rozdziale o pętlach pisałam, że nie należy kopiować kodu. Być moż pomyślałaś/eś wtedy: "no dobrze, zamiast wklejać ten sam kod 10 razy pod rząd mogę użyć pętli, ale co jeśli chcę zrobić tą samą rzecz w różnych miejscach programu?" Do tego właśnie przydają się funkcje.

Co to jest funkcja? To nazwany jakoś zbiór instrukcji dla R-a (lub innego języka). Kiedy chcieliśmy wielokrotnie używać jakiejś liczby poradziłam, żebysmy ją jakoś nazwali i zrobili z niej zmienną; podobnie można zrobić z kawałkami naszego kodu, z tą różnicą, że fragmentu kodu nie nazywamy już zmienną, a właśnie funkcją.

Funkcję tworzy się następująco:

> napiszInwokacje = function() {
+       print("Litwo ojczyzno moja")
+       print("Ty jestes jak zdrowie")
+       print("Ile cie trzeba cenic")
+       # ...
+ }

Najpierw pojawia się nazwa naszej funkcji. Następnie operator przypisania (= lub <-). Dalej pada słowo function i dwa okrągłe nawiasy - w ten sposób informujemy R-a, że to, co zaraz napiszemy i co ma przypisać na napiszInwokacje to funkcja (do okrągłych nawiasów jeszcze wrócimy). Następnie pojawia się otwarty nawias klamrowy, podobnie jak w ifie czy w pętlach - teraz R wie, że wszystko aż do zamykającego nawiasa klamrowego to nasza funkcja. Po otwartym nawiasie następuje jakiś zbiór instrukcji. Od teraz za każdym razem, jak napiszemy w R-ze napiszInwokacje(), czyli wywołamy naszą funkcję, R wykona wszystkie polecenia, które były zawarte w owych nawiasach klamrowych:

> napiszInwokacje()
[1] "Litwo ojczyzno moja"
[1] "Ty jestes jak zdrowie"
[1] "Ile cie trzeba cenic"

Wracając więc do naszego problemu z rozdziału Pętla for, gdzie chcieliśmy wypisać 20 razy na ekranie "spam": możemy teraz zdefiniować sobie funkcję, która to zrobi, i wywoływać ją w różnych miejscach programu bez konieczności kopiowania pętli:

> zaspamuj = function() {
+       for (licznik in 1:20) {
+               print("spam")
+       }
+ }

Jeśli po jakimś czasie uznamy, że funkcja zaspamuj() robi nie to, co byśmy chcieli, musimy ją poprawić tylko w jednym miejscu - tam, gdzie została zdefiniowana.

Funkcje nie muszą zawsze robić dokładnie tego samego. Zdarza się, że chcemy zrobić coś podobnego, ale na jakichś innych danych - na przykład w jednym miejscu programu chcemy napisać "spam" 20 razy, a w innym 30. Czy w takim razie możemy np. zrobić dwie funkcje, zaspamuj20razy() i zaspamuj30razy(), skopiować treść jednej do drugiej i podmienić tylko 20 w pętli na 30? Nie! ...to znaczy, oczywście - tak, teoretycznie możemy, ale mimo wszystko nie róbmy tego. Po pierwsze, wraca klasyczny argument - jeśli za dwa dni zdecydujemy, że spamować chcemy słowem "mielonka" a nie "spam" będziemy musieli zmieniać kod w dwóch miejscach zamiast w jednym; kiedy zmiany są bardziej szczegółowe zwiększa to szansę na to, że w którejś z wersji zmienimy przez pomyłkę inaczej. Po drugie, byłoby bez sensu definiować funkcje na każdą możliwą liczbę napisów. Dopóki chcemy mieć tylko dwie wersje da się to ogarnąć, ale co jeśli zapragniemy spamować na 50 różnych sposobów? Tworzyć 50 funkcji, zaspamuj1raz(), zaspamuj2razy(), zaspamuj3razy()...? Na szczęście nie musimy tego robić. Wyjście z sytuacji stanowią argumenty funkcji:

> zaspamuj = function(ile_razy) {
+   for (licznik in 1:ile_razy) {
+     print("spam")
+   }
+ }

Argumenty to to, co wpisujemy do nawiasów okrągłych po słowie function podczas definiowania funkcji, i po nazwie funkcji podczas jej wywoływania. Jeżeli teraz napiszemy:

> zaspamuj(3)

R zadziała następująco: odnajdzie w pamięci co to jest to zaspamuj i znajdzie w ten sposób naszą funkcję. Zauważy, że definiując funkcję napisaliśmy w nawiasach ile_razy - to znaczy, że na zmienną ile_razy zapisze sobie to, co wpisaliśmy do nawiasów podczas wywoływania funkcji (czyli 3). I dalej prosto: pętla for, przy czym pod nazwę ile_razy podstawi sobie 3, tak jak mu kazaliśmy, i drukowanie napisu w każdym obiegu pętli. W efekcie na ekranie pojawią się trzy napisy "spam".

Ważna uwaga. Napisałam, że R zapisze sobie na zmienną ile_razy wartość 3. Jest to prawda, ale jeśli teraz napiszemy:

> ile_razy
Error: object 'ile_razy' not found

...no właśnie, R będzie twierdził, że nie ma takiej zmiennej. To dlatego, że wykonując funkcję R tworzy sobie zupełnie osobne miejsce na różne obliczenia, odgrodzone od reszty. Na tym wyizolowanym kawałku zrobił sobie zmienną ile_razy, przypisał na nią 3, wykonał całą funkcję, po czym wylazł z tego kawałka i już do niego nie wróci. Jeśli wcześniej mielibyśmy zdefiniowaną zmienną ile_razy, zupełnie nijak nie wpłynie ona na przebieg funkcji: R na swoim odgrodzonym poletku i tak stworzy nową zmienną ile_razy, a stara pozostanie nietknięta:

> ile_razy = 5
> zaspamuj(2)
[1] "spam"
[1] "spam"
> ile_razy
[1] 5

Ten zapis, z nazwą funkcji, nawiasami i ewentualnie czymś w tych nawiasach powinien wydawać się znajomy. Tak, przecież wiele funkcji już poznałaś/eś. Przykładowo funkcja sqrt(): jako argument bierze jakąś liczbę i zwraca wynik pierwiastkowania jej. Albo funkcja matrix, tworząca macierz: matrix ma kilka argumentów, jako pierwszy podawaliśmy wartości, które mają być w macierzy, potem liczbę wierszy, a potem kolumn. Przypominam, że nie musieliśmy pamiętać kolejności argumentów: wystarczyło powiedzieć R-owi, jak argumenty, które podajemy się nazywają. Przykładowo:

> podniesDoPotegi = function( podstawa, wykladnik ) {
+  print( podstawa ** wykladnik )
+ }
>
> podniesDoPotegi(2,3)
[1] 8
> podniesDoPotegi(3,2)
[1] 9
> podniesDoPotegi(wykladnik = 3, podstawa = 2)
[1] 8

Dopóki nie mówimy, jak nazywają się podawane argumenty R myśli, że podajemy je w takiej kolejności, w jakiej pojawiły się przy definiowaniu funkcji, ale kiedy już je nazywamy kolejność staje się obojętna.

Klasyczna zabawa - spróbujmy rozzłościć R-a:

> podniesDoPotegi()
Error in podniesDoPotegi() :
  argument "podstawa" methods/html/is.html">is missing, with no default
> podniesDoPotegi(10)
Error in podniesDoPotegi(10) :
  argument "wykladnik" methods/html/is.html">is missing, with no default
> podniesDoPotegi(10,11,12)
Error in podniesDoPotegi(10, 11, 12) : unused argument (12)
> podniesDoPotegi(3, "napis chyba nie moze byc wykladnikiem...")
Error in podstawa^wykladnik : non-numeric argument to binary operator
> funkcjaKtorejNaPewnoNieMa("jakis_argument")
Error: could not find function "funkcjaKtorejNaPewnoNieMa"

R informuje nas, że nie umie wywołać funkcji podniesDoPotegi jak nie podamy argumentu podstawa, nie umie, jak nie podamy wykladnika, nie umie, jeśli podamy za dużo argumentów. Kiedy podaliśmy głupi argument (napis jako wykładnik) nawet spróbował uruchomić funkcję, nie zorientował się w pierwszej chwili, że coś jest nie tak - koniec końców wyglądało wszystko w porządku, w deklaracji funkcji było że mają być dwa argumenty, i podaliśmy dwa argumenty, nikt nigdzie nie twierdził, że muszą być liczbami. Ale kiedy spróbował rzeczywiście funkcję wykonać okazało się, że ma wykonać działanie podstawa^wykladnik, a pod zmienną wykladnik zapisaliśmy mu napis, co mu się nie spodobało. (Zauważ, że R informuje, w którym miejscu napotkał błąd: w trzech pierwszych przypadkach napisał "Error in podniesDoPotegi...", w czwartym: "Error in podstawa^wykladnik"; dzięki temu jeśli funkcja jest długa od razu widzimy, w której linijce mamy jakiś błąd). No i na koniec kazaliśmy mu wywołać funkcję, której nie ma, trudno było się spodziewać innej reakcji.

Być może zwróciłeś/aś uwagę na napis "with no default" w errorach. R pisze nam, że nie podaliśmy argumentu, a w deklaracji funkcji nie napisaliśmy, ile ten argument domyślnie ma wynosić. Zobaczmy jak to zrobić:

> przywitajSie = function(imie = "Florek") {
+    print( paste("No elo, ", imie, "!", sep="") )
+ }
> przywitajSie("Janina")
[1] "No elo, Janina!"
> przywitajSie()
[1] "No elo, Florek!"

Jeżeli podamy coś w nawiasie, R pod zmienną imie podstawi to, co podaliśmy (oczywiście tylko na odgrodzonym kawałku na którym wywołuje naszą funkcję, nie pojawi się żadna globalna zmienna imie, którą później będziemy mogli użyć, a jeśli już taka była, to nie zostanie nadpisana); jeżeli natomiast nie podamy, R założy, że imie ma wartość "Florek". Przypominam, że funkcja paste() sklejała napisy, a jej argument sep mówił o tym, co ma powklejać między łączone napisy.

Kilkakrotnie używałam w tym tutorialu frazy "funkcja zwróciła [cośtam]". Pora się z niej wytłumaczyć. Spójrzmy na przykład poniższego skryptu:

policzSumeIWydrukuj = function(a,b) {
        cat(a+b)
        cat('\n')
        }
 
policzSumeIZwroc = function(a,b) {
        return(a+b)
        }
 
a = policzSumeIWydrukuj(1,1)
b = policzSumeIZwroc(2,2)
print("***")
print(a)
print(b)

Obie zdefiniowane przez nas funkcje obliczają sumę dwóch liczb, ale różnią się tym, co robią z wynikiem. Pierwsza go tylko i wyłącznie drukuje (razem z enterem: przypominam że '\n' to znak specjalny oznaczający nową linię), druga go *zwraca* za pomocą słowa "return" (czyli zwróć).  Oznacza to, że wynikiem działania funkcji policzSumeIZwroc() jest to, co jest w nawiasach po funkcji return, czyli suma dwóch liczb podanych jako argumenty. Funkcja policzSumeIWydrukuj nie daje żadnego wyniku - tylko drukuje uzyskany wynik. Różnicę w ich działaniu można prześledzić uruchamiając skrypt:

 > source("dwieFunkcje.R")
 2
 [1] "***"
 NULL
 [1] 4

Najpierw wykonała się funkcja policzSumeIWydrukuj(1,1), w związku z czym R wydrukował 2. Na zmienną a przypisał to, co zwróciła funkcja policzSumeIWydrukuj, a na b to, co zwróciła policzSumeIZwroc. Potem wydrukował trzy gwiazdki. Potem drukuje wartości a i b: jak się okazuje, pod a nie jest nic zapisane (NULL), zaś pod b - zgodnie z przewidywaniami - suma liczb 2 i 2.

Krótko mówiąc, żeby móc wynik działania funkcji przypisać na zmienną, musimy użyć w niej słowa kluczowego "return". Return może być tylko jedno na funkcję - kiedy R dociera w funkcji do słowa return, zwraca to, co jest w nawiasach i dalej się nawet nie kłopocze, ignoruje wszystko aż do zamykającego nawiasu klamrowego funkcji.

Uwaga dotycząca różnicy między funkcją print a cat. Poprzednio pisałam, że różnica między print i cat polega na tym, że inaczej interpretują znaki specjalne; przykładowo, gdybyśmy napisali print('\n') zamiast cat('\n') na ekranie wyświetliłoby się po prostu '\n'. Jest to prawda, ale nie cała. Print jest ciekawą funkcją, która nie tylko drukuje na ekran, ale jednocześnie zwraca to, co wydrukowała. To wyjaśnia różnice, które być może już zauważyłeś/aś - to, co drukuje się w wyniku funkcji cat, nie jest wcale wektorem i nie zaczyna się od [1], bo nie jest w ogóle żadnym stworzonym obiektem, R po prostu drukuje nam coś na ekran. Natomiast wydruki wywołane funkcją "print" są jednocześnie zwróconymi przez nią obiektami - czyli dla R-a wektorami. To również uzasadnia, dlaczego dla dobra przykładu w funkcji "policzSumeIWydrukuj" zastosowałam funkcję cat zamiast częściej do tej pory stosowanej w tym tutorialu funkcji print (częściej, bo po prostu nazywa się bardziej intuicyjnie). Z funkcją print przykład by mi nie wyszedł.