3  Függvények

Sok esetben előfordul, hogy egy statisztikai programban ismétlődő részek vannak. Tekintsük például a következő kódot (x egy valós számokat tartalmazó vektor):

x <- rnorm(10)
x
 [1] -1.42186464 -1.02536905  0.15547145 -0.67750679  0.66905183 -0.30870055
 [7] -1.02053916 -0.02961993 -1.03162418 -0.41548497
n <- length(x)
osszeg <- sum(x)
atlag <- osszeg/n
sum((x - atlag)^2)/(n - 1)
[1] 0.4190149

Valószínűleg sokan felismerik, hogy ezzel az x-ben lévő elemek varianciáját számoltuk ki. Erre ezerszer szükség lehet egy statisztikai elemzésben: egyszer kiszámolhatjuk a testtömegek varianciáját, aztán a testmagasságokét, kellhet varianciát számolni, ha valamilyen regressziót futtatunk, ha főkomponens-elemzést hajtunk végre, egyszóval milliónyi feladatnál, akár ugyanazon szkripten belül is, pláne a különböző szkriptekben.

Megtehetnénk természetesen, hogy minden ilyen esetben elhelyezzük ugyanazt a kódrészletet: kimásoljuk a fentit, és mindenhová beillesztjük, x-et a megfelelő változónévre cserélve – de remélhetőleg zsigerből érezhető, hogy ez nagyon-nagyon szerencsétlen megoldás lenne. Azon túl, hogy nagyon hosszúvá tenné a kódot (ami nem a gépnek probléma, hanem nekünk, olvashatóság szempontjából), azon túl, hogy megnehezíti a kód elemzését, értelmezését, mert nem lesz gyorsan és egyértelműen beazonosítható, hogy mely helyeken történik meg ez a számolás, a fő probléma, hogy borzasztóan megnehezítené a kód karbantartását, a hibajavításokat, fejlesztéseket. Képzeljük el például, hogy miután végeztünk a fenti bemásolgatós megoldással, észrevesszük, hogy az osszeg változót teljesen felesleges volt elmenteni, mert csak egyetlen egyszer hivatkozunk rá, nyugodtan beírhattuk volna közvetlenül a következő sorba. Mit csinálunk? Ha a bemásolgatós megoldással éltünk, akkor nincs más lehetőség, mind az ezer helyen át kell írni…!

Mennyivel jobb lenne ezt a kódot csak egyetlen egy helyen megírni, adni neki egy nevet, mondjuk variancia, és minden alkalommal, amikor szükség van rá a programunkban, azt írni ott, hogy „most futtasd le a variancia-t”. Egyetlen rövid utasítás, ami azt jelentené, hogy innentől ugorj át erre az – egyetlen helyen megírt – kódra, futtasd le, ami ott van, majd ha végeztél, térj vissza pontosan oda, ahol mondtuk, hogy futtasd le ezt a kód. Az ilyen kódot hívjuk úgy a programozásban, hogy függvény, az előbbi folyamatra pedig azt mondjuk, hogy meghívtuk a függvényt. Mindezt akárhány alkalommal megtehetjük, mindig ugyanaz az – egyetlen helyen megírt – függvény fut le, és a végén mindig oda ugrik vissza, ahonnan éppen hívtuk. A fenti kódot, a függvény „tartalmát” a függvény törzsének szoktuk mondani.

Ezzel a módszerrel sokkal olvashatóbb lesz a kód, azonnal és egyértelműen látszódni fog, hogy hol használtuk ezt a variancia-számítást, de a legfontosabb, hogy ha valamit módosítanunk kell a variancia-számításban, akkor ezt elég egy helyen megtenni, és ettől automatikusan az összes hívásnál meg fog változni.

3.1 Paraméterátadás és visszatérési érték

Egyetlen egy dolgot kell megoldani, az információk átadását. Mindkét irányban: valahogy el kell juttatnunk a függvényhez, hogy mi az a vektor, aminek a varianciáját számolni akarjuk, illetve fordítva, valahogy vissza kell juttatnunk a kiszámított varianciát a programhoz.

Az előbbi megoldás kinézetét már sugallja a fenti kód is: a függvényben meghatározunk egy változónevet (x), és azon hajtjuk végre a műveleteket a függvény törzsében, az információátadást pedig úgy valósítjuk meg, hogy a függvény hívásakor megadjuk, hogy milyen konkrét érték, például programbeli változó – mondjuk testtomegek – kerüljön át a függvénybe ezen a néven. Az ilyen x-et szép néven paraméternek, a konkrét értéket, amit a függvény híváskor megadunk, argumentumnak nevezzük1; az egész mechanizmus neve pedig paraméterátadás. Lényegében az történik, hogy x szerepét a testtomegek változó fogja játszani – ennél a hívásnál. De később, ha meghívjuk a függvényt a testmagassagok argumentummal, akkor az fogja x szerepét játszani. Nekünk a függvényben azonban általánosan kellett meghatározni, hogy mit kell tenni, erre szolgál az x, azon írjuk le, hogy mi általában az eljárás, arról pedig az R gondoskodik, hogy ez mindig a megfelelő értéken hajtódjon végre. A háttérben az történik, hogy amikor meghívjuk a függvényt a testtomegek változóval mint argumentummal, akkor az R készít egy másolatot a testtomegek-ről, elnevezi x-nek, és lefuttatja rajta a függvény törzsét. A „készít róla másolatot” kitétel fontos, ez ugyanis azt is jelenti, hogy a függvény törzse bármit is csinál az x-szel, az nem fog visszahatni a hívó programban található testtomegek változóra: ha a függvény át is írja x-et, attól még a testtomegek a hívó programban ugyanaz marad (hiszen ilyenkor a függvény csak a testtomegek egy másolatát módosította, aminek semmi köze a testtomegek-hez!). Ezt szokták érték szerinti paraméterátadásnak nevezni2. Egyetlen megjegyzés a végére: ne ijedjünk meg a „készít róla másolatot” megfogalmazástól. Ezt csak az elmagyarázást segítő szófordulat volt a részemről, az R ezt igyekszik ügyesen kezelni, és nem feltétlenül készít3 ténylegesen másolatot, azaz a dolog nem jelenti automatikusan azt, hogy minden függvényhívásnál teljesítmény-veszteség keletkezik a másolgatások miatt, aminek van valamennyi időigénye.

És mi a helyzet fordított irányban? Hogyan adhatunk vissza információt a hívó programnak? Az első dolog, amit tudni kell, hogy R-ben egy függvény csak egyetlen értéket adhat vissza4; ezt hívjuk visszatérési értéknek. Ez elsőre rosszul hangozhat, de ne ijedjünk meg, ez semmilyen érdemi limitációt nem jelent: a visszatérési érték ugyanis nyugodtan lehet egy lista is! Abba pedig akármilyen objektumból akármennyit bepakolhatunk. A visszatérési értéket megadni a return-nel lehet, a fenti példában azt kellene írnunk az utolsó sorban, hogy return(sum((x - atlag)^2)/(n - 1)). Ez azt jelenti, hogy a függvény végrehajtása befejeződik, a vezérlés visszakerül a hívó programhoz, olyan módon, hogy a függvényhívás behelyettesítődik a visszatérési értékkel. Ez utóbbi nagyon fontos: a dolgot úgy kell elképzelnünk, mintha a meghívott függvény helyébe odaírnánk a visszatérési értékét, és úgy menne tovább a végrehajtás. Két technikai megjegyzés a végére. Az egyik, hogy nem kötelező, hogy a return legyen egy függvény utolsó utasítása, de mivel a return elérésekor a vezérlés visszakerül a hívóhoz, így túl sok értelme sincs bármit utána írni. Jellemzően akkor fordul elő, hogy return-t látunk egy függvény közepén, ha valójában egy feltételes részben van, tehát nem biztos, hogy végrehajtódik: a return megvalósítja azt, hogy ha oda kerül a végrehajtás, akkor visszatér (és így a függvény többi része végre sem hajtódik), de ha nem, akkor a return le sem fut, és így mehet tovább a függvényben a végrehajtás. A másik megjegyzést talán még gyakrabban használjuk a gyakorlatban, ez pedig az a szabály, hogy ha a függvényben egyáltalán nincs return, akkor a visszatérési érték az utolsóként kiértékelt kifejezés értéke lesz. Ez a gyakorlatban azért fontos, mert elég tipikus, hogy egy függvény utolsó utolsó utasítása egyszerűen egy változó neve, vagy valamilyen egyszerű művelet egy változóval – ekkor ez az érték fog automatikusan return-ölődni. A fenti példában tehát igazából elég lenne a függvény utolsó sorában annyit írni, hogy sum((x - atlag)^2)/(n - 1), a return nem is kell, az eredmény ugyanaz lesz.

Ha egy függvény bármilyen más hatást vált ki azon kívül, hogy kiszámolja a visszatérési értéket és azt visszaadja, akkor azt mellékhatásnak nevezzük, az ilyen függvényt pedig mellékhatásos függvénynek. Ez elsőre meglepő lehet (mégis mi más hatása lehet egy függvénynek azon kívül, hogy kiszámolja a visszatérési értéket és azt visszaadja?!), de ha meggondoljuk, valójában egészen banális dolgok tartoznak ide – kezdjük például kapásból a kiíratással! Az ugyanis teljes mértékben egy mellékhatás: ha egy függvény hatására valami megjelenik a konzolon, annak nyilván semmi köze a visszatérési értékhez. De az is mellékhatás, ha egy fájlt kiírunk a merevlemezre, vagy mondjuk ha megjelenítünk egy ábrát. Az R programozási filozófiája miatt alapvetően kerülni célszerű a mellékhatásokat5, de mint az előbbi példák mutatják, bizonyos esetben elkerülhetetlenek.

3.2 Saját függvények létrehozása

A fentiekkel szinte már mindent tudunk ahhoz, hogy saját függvényt tudjunk definiálni. Amint láttuk, ez fontos: ez minden alkalommal jól jön, ha ismétlődő kódrészletek vannak a statisztikai számításunkban. Nézzük, hogyan tehetjük ezt meg egész pontosan.

Először is, függvényt ugyanúgy értékadással hozhatunk létre, ahogy létrehozunk egy – korábban még nem létező – változót. Amit a változónak értékül kell adnunk, az a function kulcsszó, ennek hatására a létrejövő változó egy függvény lesz, amit később meghívhatunk. A function után gömbölyű zárójel következik, benne a paraméterek – ennek részleteit a következő pontban fogjuk látni – utána pedig a függvény törzse. Ez egyetlen R kifejezés lehet, ami persze az esetek túlnyomó többségében értelmetlenül kevés, de semmi gond: erre szolgál a blokk-képzés. A blokk-képzés azt jelenti, hogy fogunk utasításokat, és összefogjuk őket egyetlen egységbe – ez lesz a blokk – amik úgy viselkednek, mintha az egyetlen kifejezés lenne. (A végrehajtása természetesen azt jelenti, hogy mindegyik benne lévő utasítás végrehajtódik egymás után, ahogy bármilyen R kódnál történne.) A blokk-képzés jele az R-ben a kapcsos zárójel, ezzel lehet több utasítást egyetlen blokkba összefogni.

Mindezek alapján egy saját függvény létrehozása a következőképp néz ki a fenti példában:

variancia <- function(x) {
  n <- length(x)
  osszeg <- sum(x)
  atlag <- osszeg/n
  sum((x - atlag)^2)/(n - 1)
}

Ezután bármikor meghívhatjuk:

x <- rnorm(10)
variancia(x)
[1] 0.6522467

(Természetesen erre a célra van beépített – és a fentinél sokkal gyorsabb – függvény, a neve var, de ez most csak az illusztráció célját szolgálta.)

Ami feltűnhet a fentiben, hogy sem a paraméterek, sem a visszatérési érték típusát nem kellett megadni. Ez következik abból, hogy az R dinamikus típusrendszerű nyelv, és minden igaz rá, amit a dinamikus típusrendszerről az adattípusoknál elmondtunk: egyszerűsíti az életet (nem kell vacakolni a típusok felsorolásával, nyugodtan előfordulhat, hogy az argumentumok fényében nem mindig ugyanolyan típusú eredményt adunk vissza), de cserében megszüntet egy védelmi vonalat, ami kinyit hibalehetőségeket. Nem csak arról van szó, hogy a fenti függvényt meghívhatjuk például egyelemű vektorral – ami az n-1-gyel való osztás miatt lesz elég nagy baj – de az előzőekben mondottak miatt még az ellen sem lesz beépített védelmünk, hogy valaki mondjuk egy szöveg varianciáját álljon neki kiszámíttatni…! Az R-ben minden ilyenre nekünk kell figyelni. És érdemes is: ilyen esetekben fontos lehet, hogy mielőtt bármit csinálunk, ellenőrizzük a kapott argumentumot vagy argumentumokat; erre vannak bevált eszközök és módszerek6 az R-ben. Ez különösen akkor helyes gyakorlat, ha a függvényt nem csak mi magunk fogjuk meghívni – ekkor még mondhatnánk, hogy a meghívó oldaláról tudjuk biztosítani a megfelelő argumentumokat, bár az igazság az, hogy ilyenkor is sokszor jobb a biztonság – hanem mások is meghívhatják, például, mert egy R csomagba kerül ki.

Az R programozásáról szóló fejezetben látni fogjuk, hogy lesznek olyan függvények amelyeknek az egyik argumentuma maga is függvény. Ez lehet egy fentihez hasonló függvény: létrehozunk egy függvényt, nevet adunk neki, majd ezt a nevet adjuk meg ilyenkor argumentumként. Ilyen esetekben azonban elég gyakori, hogy a függvényt nem akarjuk külön elmenteni, saját néven, ugyanis máshol nincsen rá szükség, és csak ott akarnánk, direkte a kódba beleírni a függvényt az argumentum helyébe, anélkül, hogy előtte külön néven elmentjük. (Lényegében arról van szó, hogy szeretnénk megtenni ugyanazt a függvényekkel, amit megtehetünk más kifejezésekkel. Hiszen ha például gyököt akarunk vonni 3 + 1-ből, akkor egyáltalán nem kötelező előbb bementeni egy külön, névvel rendelkező változóba a 3 + 1-et, majd arra a változóra meghívni a sqrt-ot, egész nyugodtan megtehetjük azt is, hogy azt írjuk, hogy sqrt(3 + 1).) A helyzet az, hogy ez minden további nélkül működik függvényekkel is! A fenti szintaktikával kell létrehozni függvényt, de egyáltalán nem kötelező, hogy azt bármibe belenyilazzuk. Az így létrehozott függvény nyugodtan beírható az argumentum helyébe, a dolog tökéletesen működőképes lesz. Az ilyen függvényt hívjuk szép szóval anonim függvénynek.

Egyetlen megjegyzés a végére. Az R 4.1.0-s verziója bevezetett egy rövidítést: a \(x) pontosan ugyanaz mint a function(x). Ezt inkább anonim függvényeknél szokták használni, hiszen ott jobban mutat, ha kevesebbet kell írni, mivel ott a függvény egy másik függvény argumentumába van beírva, ami elég zsúfolt kódot eredményezhet már eleve is.

3.3 R csomagok használata

Volt már róla szó, hogy az R erejét nem kis részt az adja, hogy hihetetlen mennyiségű csomag érhető el hozzá, melyek a legkülönfélébb statisztikai problémák megoldását teszik lehetővé. Most már pontosabb definícióját adhatjuk annak, hogy mi az egyáltalán, hogy R csomag: tartalmilag összetartozó függvények gyűjteménye (dokumentációval és esetleg adatokkal kiegészítve). Nézzük meg részletesebben, hogy miről van szó!

Amikor azt mondtam, hogy „statisztikai problémák megoldása”, akkor azt lényegében így értettem: megfelelő függvények megírása. Hiszen ezeket a statisztikai problémákat éppen úgy oldjuk meg, hogy létrehozunk (egy vagy több) függvényt! A megoldás abban manifesztálódik, hogy létrehozunk függvényt vagy függvényeket, amik a szükséges számításokat elvégzik, tehát tömörítik a statisztikai tudásunkat azáltal, hogy a feladatot – vagy valamilyen részfeladatát – megoldják, ahogy a fenti függvény megoldja a variancia kiszámításának a feladatát. Azzal a nem elhanyagolható eltéréssel, hogy a függvény természetesen nem biztos, hogy 4 sor, lehet éppenséggel 4 ezer is. Elég tipikus, hogy egy adott témakör számításai több függvényt igényelnek, ezeket dokumentációval kell ellátni, néha érdemes adatbázisokat is mellékelni – az R csomag nem más, mint ilyenek gyűjteménye. Amikor betöltünk egy R csomagot, akkor elérhetővé válnak a benne definiált függvények, vagyis azáltal „férünk hozzá az erejéhez”, hogy meg tudjuk hívni ezeket a függvényeket, így egyetlen sorban elérünk néha több ezer sornyi, adott esetben igen komplex statisztikai számítást, amit más, jó esetben a témában jártas szakértők megírtak számunkra, így mi készen használhatjuk.

Kis kitérő: eddig még nem töltöttünk be semmilyen csomagot, és mégis voltak elérhető függvényeink, mint az rnorm vagy a épp az str. Eddig ezt úgy kezeltem, mintha ezek valamiféle „beépített” függvények lennének, amikhez nem kell csomagot betölteni… csakhogy ez nem igaz. Ezek szintén csomagban vannak, ugyanúgy mint bármely más függvény (általában is, ha megnézzük egy függvény súgóját, akkor a bal felső sarokban, kapcsos zárójelben látjuk, hogy melyik csomagban, innen kiderül, hogy az rnorm a stats-ban, az str a utils-ban van), és ezeket a csomagokat nagyon is be kell tölteni, hogy elérjük a függvényeiket – csak épp az R ezt megteszi helyettünk! Van ugyanis hét csomag7 amit az R az indulásakor automatikusan betölt, így a függvényei elérhetővé válnak úgy, hogy látszólag nem kellett semmit sem tennünk – de a valóságban ez is ugyanúgy csomagbetöltés volt.

Még mielőtt a csomagok betöltésére térünk, egy dolgot meg kell beszélni: a csomagok telepítését, betölteni ugyanis csak telepített csomagot8 tudunk. Bárki készíthet R csomagot, egy R csomag lehet egyetlen összecsomagolt zip-fájl, de van egy eszköz, ami miatt mégsem alakul ki káosz ebből a dologból, azaz nem zip-fájlok keringenek össze-vissza, ez az eszköz pedig nem más, mint a CRAN (Comprehensive R Archive Network). Ez az R csomagok egy központi repozitóriuma, ahol szinte minden fontos R csomag megtalálható, így ez kiküszöböli a káoszt: szinte mindenki erre az egy helyre tölti fel az R csomagját, ha készít ilyet, és szinte mindenki erről az egy helyről tölti le az elkészített R csomagokat. (A „szinte” kitétel annak szól, hogy léteznek más repozitóriumok is, megfelelő csomag segítségével lehet Github-repozitóriumból is installálni csomagot, illetve elvileg lehet tényleg zip-fájlból is telepíteni csomagot, de ezek jelentősége eltörpül a CRAN mellett.) A CRAN ráadásul ellenőrzi is a csomagot, egyébként elég szigorúan, tehát ez egyfajta minőségbiztosítás is. (Mindazonáltal az ellenőrzés formai, tehát a csomag szintaktikai helyességére vonatkozik, nem a statisztikai – vagy bármilyen más – tartalmának tudomány helyességére!) A CRAN-ről történő telepítéshez az R külön utasítást tartalmaz, így valójában nem kell semmilyen zip-fájl letöltéssel vesződnünk. Ha RStudio-t használunk, akkor választhatjuk egyszerűen a Tools menü Install packages pontját, itt alapértelmezett a CRAN-ről történő telepítés, így csak a csomag nevét – vagy több csomag esetén a neveiket, vesszővel elválasztva – kell megadni, ebben automatikus kiegészítés segít is, majd az Install gombra kattintva megtörténik a telepítés. A konzolon láthatjuk, hogy ez igazából egyenértékű az install.packages függvény meghívásával, argumentumként megadva a csomag nevét; válaszhatjuk ezt a megoldást is. (Viszont ezt nem illik magába a szkriptbe beleírni, mert ez azt jelentené, hogy ha az egész szkriptet egyben futtatjuk, akkor az minden alkalommal nekiáll telepíteni a csomagot.) Ha egyszer megtörtént a telepítés9, onnantól már betölthetjük a csomagot.

Hogy legyen egy konkrét példánk csomag betöltéséhez, tekintsük azt a feladatot, hogy szeretnénk 10 véletlenszámot generálni, de ezúttal egy standard inverz exponenciálisnak nevezett eloszlásból. Hogy mi ez az eloszlás, az most mindegy is, ami viszont fontos, hogy – szemben a normálissal – az R erre nem tartalmaz „beépített” függvényt. De semmi vész, szerencsére az actuar nevű csomagban van egy függvény, a neve rinvexp, ami pont ezt valósítja meg! Első pont: ha korábban nem tettünk meg, telepítenünk kell az actuar csomagot; e nélkül nem tudunk továbbhaladni. Második pont: ha telepítettük, akkor be kell tölteni.

Ezen a ponton kettéágaznak a lehetőségek. Még mielőtt belevágunk, elsőként állapítsuk meg, hogy betöltés nélkül nem tudjuk lefuttatni a függvényt, hibát fog adni:

rinvexp(10)
Error in rinvexp(10): could not find function "rinvexp"

Az első lehetőség, hogy a :: (két kettőspont, néha szokták magyarul négyespontnak is hívni) operátort használjuk: először leírjuk a csomag nevét, utána négyespontot teszünk, majd a függvény nevét a csomagból. Ez így már lefut:

actuar::rinvexp(10)
 [1]  7.7459165  3.3740044 22.3284885  1.8742508  0.6204055  0.3868090
 [7]  0.5065756  5.1193000  0.3987887  0.4614239

Magyarra lefordítva az actuar::rinvexp praktikusan azt jelenti, hogy „az actuar csomag rinvexp nevű függvénye”.

Ez nem szó szerint a csomag betöltése, abban az értelemben, hogy ettől nem válik a csomag összes többi függvénye elérhetővé, csak ez az egy (és ez az egy is csak ekkor, ha később újra szükségünk van erre a függvényre, akkor megint elé kell írni, hogy actuar::). A csomag szó szerinti betöltését a library függvénnyel10 hajthatjuk végre:

library(actuar)

Kapcsolódás csomaghoz: 'actuar'
The following objects are masked from 'package:stats':

    sd, var
The following object is masked from 'package:grDevices':

    cm

Ha ezt megtettük, akkor onnantól nincs szükség a :: operátorra, a csomag valamennyi függvénye automatikusan, minden előtag nélkül elérhetővé válik! Azaz innentől működik ez a megoldás is:

rinvexp(10)
 [1]  5.6873696  0.2449313  1.2170611 18.8418381  0.5603865  1.3907276
 [7]  0.9638379  3.0480598  0.5370184  1.3893917

Egy apró megjegyzés a teljes precizitás kedvéért. A négyespont operátor csak azokat a függvényeket teszi elérhetővé, amik ún. exportált függvényei a csomagnak. Az exportálás lényegében azt jelenti, hogy elérhetővé akarjuk-e tenni az adott csomagbeli függvényt. Ez elsőre meglepő lehet (miért raknánk egy függvényt egy csomagba, ha nem akarjuk elérhetővé tenni?!), de elképzelhetőek helyzetek, ahol ennek van értelme, például, mert csak egy belső célokat szolgáló segédfüggvényről van szó – az ilyeneknek lehet szerepe, a felhasználó számára is, mert más csomagbeli függvények meghívhatják, csak közvetlenül nem lehet őket kívülről meghívni. Ha valami oknál fogva mégis ezt szeretnénk, akkor a ::: operátort kell használni, ezzel nem exportált függvény is hívható.

A nyitva maradt kérdés, hogy melyik a jobb megoldás, a :: használata vagy a library-vel történő betöltés? A válaszhoz egy dolgot kell még tudni, azt, hogy mi történik akkor, ha a csomagban van egy olyan nevű függvény, ami már létezik. Hiszen minden további nélkül lehet egy rnorm nevű függvény definiálva egy csomagban! (Vagy akár két különböző csomagban.) A helyzet az, hogy ilyenkor, ha betöltjük a csomagot library-vel, akkor felülíródik a csomag függvényével a korábban már létező, ugyanolyan nevű függvény. Ha betöltünk később egy másik csomagot is library-vel, amiben van ugyanolyan nevű függvény, akkor meg azzal íródik felül. Az rnorm kicsit extrém példa, de mondjuk filter nevű függvény tényleg tömegével van különféle csomagokban. A dolog tehát egy olvashatósági problémát fog okozni: ha egyszerűen annyit látunk egy kódsorban, hogy filter(x), akkor nem tudhatjuk, hogy ez minek a filter-je, melyik csomag filter nevű függvénye fog itt lefutni. A „beépített” filter? (Mert van ez is; igazából a stats csomagé.) A dplyr csomag filter-je? A signal csomag filter-je? A seewave csomag filter-je? A legrosszabb, hogy ezt nem is lehet megmondani pusztán ebből a kódsorból – mert azon fog múlni, hogy melyik csomagot töltöttük be utoljára! Tehát ezt csak úgy tudjuk eldönteni, ha visszamegyünk a kódban (akár több száz vagy ezer sort is adott esetben), megnézzük az összes library hívást, hogy definiál-e filter-t, és ez fogja eldönteni, hogy a kérdéses sorban pontosan mi is fut: a beépített filter vagy valamelyik csomagé? Ha több, filter-t definiáló csomagot is betöltünk library-vel, akkor még az is számítani fog, hogy milyen sorrendben töltjük be őket! Emiatt célszerűbb, ha ahol lehet, inkább a :: operátort használjuk. Ennek az egyetlen hátránya11, hogy többet kell hozzá írni, és ha nagyon sokszor ismétlődik, az zavaró lehet, így végeredményben az ajánlás az, hogy library-vel csak azokat a csomagokat töltsük be, amik alapvetőek, amik függvényeit sokszor és intenzíven használjuk – a többi esetében jobb a :: használata.

Egyetlen megjegyzés a végére: néha érdemes a csomagokat frissíteni! Hiszen a csomagok frissülnek a CRAN-en, ha a szerzőjük feltölt egy újabb változatot, de az nem kerül át automatikusan a gépünkre. Ezért érdemes rendszeresen megnyomni RStudio-ban a Tools / Update packages pontot, és az összes csomagot frissíteni. (Egyébként végeredményben ez is az install.packages-t fogja meghívni.)

3.4 Függvények specifikációja és meghívása

Függvény úgy hívható meg, hogy megadjuk a nevét, majd utána gömbölyű zárójelben az argumentumát, vagy argumentumait:

rnorm(10)
 [1] -0.17004724  0.76587520 -0.09932867 -0.20290450  1.16748023 -0.73989527
 [7]  1.99008018  1.29820532  0.05812769  1.65163447

Elképzelhető, hogy egy függvénynek egy argumentuma sincs, de a zárójelet ekkor is ki kell írni (azonnal becsukva, értelemszerűen). Ez fontos, ugyanis zárójel nélkül beírva a függvény nevét az R kiírja a függvény törzsét:

rnorm
function (n, mean = 0, sd = 1) 
.Call(C_rnorm, n, mean, sd)
<bytecode: 0x00000169ea124628>
<environment: namespace:stats>

Függvényről súgó a kérdőjellel kapható: ?rnorm. Ez a megoldás akkor használható, ha a függvény pontos nevét tudjuk, mert azt kell a kérdőjel után írni, ha nem tudjuk a pontos nevet, csak a név egy töredékét, akkor a két kérdőjel (??rno) használható; ez végigkeresi az összes súgó-oldalt a beírt töredék után, és listát ad róluk12.

Ami nekünk most különösen fontos a súgóban, az a függvény ún. specifikációja. (Ha saját magunk írtuk a függvényt, akkor ezt a specifikációt mi magunk kellett hogy megadjuk – ahogy az az előző pontban szerepelt is.) Az rnorm esetén ez a következőképp néz ki:

rnorm(n, mean = 0, sd = 1)

Az argumentumok az args függvénnyel is megtudhatóak:

args(rnorm)
function (n, mean = 0, sd = 1) 
NULL

Ebben a következő elemek láthatóak:

  • A függvény neve, esetünkben az rnorm. Utána, ahogy már volt róla szó, gömbölyű zárójelben következnek az argumentumok.
  • Szintén volt róla szó, hogy az argumentumok száma tetszőleges lehet, a nullát is beleértve; jelen esetben a függvénynek három argumentuma van.
  • Minden argumentumnak van egy neve, kötelező is, hogy legyen. Ez esetben az első argumentum neve n, a másodiké mean, a harmadiké sd.
  • Egy argumentumnak lehet, de nem kötelező, hogy legyen ún. alapértelmezett értéke; ha van, akkor egyenlőségjel után szerepel az argumentum neve után. Esetünkben az n nevű argumentumnak nincs alapértelmezett értéke, a mean-nek és az sd-nek van, az előbbinek 0, az utóbbinak 1.

Természetesen nagyon fontos, és a súgó többi részéből ez ki is derül, hogy egyáltalán mit csinál a függvény, mire jó, mi a tartalma az egyes argumentumoknak, mi a visszatérési értéke, de mi most fókuszáljunk a szintaktikára.

Kezdjük ott, hogy ha egy argumentumnak van alapértelmezett értéke, akkor azt az argumentumot nem kötelező megadni a hívás során, viszont aminek nincs, azt kötelező. Ezért van az, hogy az rnorm(10) lefut, noha csak egy argumentumot adtunk meg (mert a másik kettőnek van alapértelmezett értéke), viszont ha az n-et nem adjuk meg, akkor hibát ad a függvényhívás:

rnorm()
Error in rnorm(): argument "n" is missing, with no default

Az alapértelmezett értékkel rendelkező argumentumokat tehát nem kötelező megadni, de természetesen lehet:

rnorm(n = 10, mean = 70, sd = 15)
 [1] 64.96590 76.28135 84.86808 96.48757 86.28082 51.34928 84.69204 77.01062
 [9] 73.08756 73.45674

Mint látható, argumentumot úgy adunk meg a hívás során, hogy beírjuk a nevét, egyenlőségjelet teszünk13, majd utána leírjuk az értékét. Ami fontos, hogy a nevek elhagyhatóak, ez esetben az R abban a sorrendben rendeli hozzá a beírt értékeket az argumentumokhoz, amilyen sorrendben a specifikációban szerepelnek14. Vagyis a fenti hívás egyenértékű ezzel:

rnorm(10, 70, 15)
 [1]  75.45965  70.73456  58.15248  90.23122  82.02412  67.22329 112.32970
 [8]  80.04729  53.44822  41.76028

Ezt voltaképp már korábban is láttuk: az rnorm(10) ezért működött minden név megadása nélkül is.

A nevek megadása lehetővé teszi, hogy más sorrendben soroljunk fel argumentumokat, mint a specifikációban szerepelnek:

rnorm(sd = 15, n = 10, mean = 70)
 [1] 46.83275 45.00805 63.47752 61.88837 73.75061 73.55821 62.04609 64.35981
 [9] 59.35848 69.10509

Vagy, hogy átugorjunk egy argumentumot:

rnorm(10, sd = 15)
 [1] -8.453373 -5.696722 -2.660878 17.771419 -5.293101  5.187224 15.155332
 [8] -9.587199 -8.373512 -1.341640

Az R-es gyakorlat az, hogy az első néhány, mondjuk 3-4 argumentumtól eltekintve akkor is írjuk ki a neveket, ha egyébként sorrendben adtuk meg őket a hívás során. Ennek nem a kód futtathatóságához, hanem az olvashatóságához van köze: átlagos R-hez értő embertől elvárható, hogy – különösen a gyakran használt függvényeknél – az első néhány argumentumról tudja, hogy mi a jelentésük, de a továbbiakról már nem feltétlenül, így az olvasónak segítség, ha ezeknél feltüntetjük a nevet, még akkor is, ha az R-nek nem kellene, mert sorrendben jönnek. Ez azért van így, mert ezek általában beszélő nevek (ez az rnorm példáján is jól látszik!); ha majd mi magunk definiálunk függvényt, akkor is törekedjünk rá emiatt, hogy mi is ilyen beszélő neveket adjunk.

Egy utolsó dolgot kell még a fentiek kapcsán megbeszélnünk. Az eddigi leírásból úgy tűnhet, hogy a függvényeknek mindig adott, rögzített, előre ismert számú argumentumuk van – annyi, amennyit a specifikációban felsoroltunk. Az eddigi példákban ez valóban így volt, de gondoljunk bele, mi a helyzet mondjuk a c-vel? (Eddig nem mondtam, de természetesen ez is egy függvény!) Hogyan lehetséges, hogy működik a c(1, 2) és a c(1, 2, 3) is? A válasz az, hogy az R megenged egy speciális argumentumot, a három pontot15: a c függvény specifikációja úgy néz ki, hogy c(...). A három pont azt jelenti: tetszőleges számú argumentum. Ilyen esetben tehát azt mondjuk, hogy mi magunk sem tudjuk, hogy hány argumentumot kapunk, minden teljesen azon múlik, hogy a felhasználó hogyan hívja meg a függvényt – ami szerepel a meghívásban, az fog átkerülni a ... alatt, legyen az 0 argumentum, 1, vagy 100. A felhasználó megteheti, hogy a ... helyén 0 argumentumot ír be a függvény meghívásakor, megteheti, hogy 1-et, megteheti, hogy 100-at. (Ha majd saját függvény írunk: ilyenkor a függvényen belül szintén ... névvel hivatkozhatunk arra, hogy mit kaptunk a hívás során. Annyit kell tudni erről, hogy ez egy elég speciális elem, első lépésben szinte mindig listává alakítjuk: list(...) már egy szokásos lista lesz, amit innentől a hagyományos módon, megszokott listaként használhatunk.) A ...-ban átadott argumentumoknak lehet neve, de ez nem kötelező. A ... vegyíthető a „szokásos” argumentumokkal: egy függvénynek nézhet úgy ki a specifikációja, hogy f(x, ..., y = 1, z = 2). Ez azt jelenti, hogy az első egy kötelező argumentum, x névvel, utána jön tetszőleges, és előre nem ismert számú argumentum (akár 0 is): hogy itt mit kap a függvény, az teljesen azon múlik, hogy a felhasználó hogyan hívja meg. Ezután egy y nevű argumentum következik 1 alapértelmezett értékkel, végül egy z nevű 2 alapértelmezett értékkel. Ennek megfelelően az f(1) esetén az x értéke 1 lesz, a list(...) egy üres lista, y pedig 1 , míg z értéke 2. Az f(1, z = 10) annyiban tér el ettől, hogy z értéke 10 lesz. Végezetül az f(1, 2, a = 3, 4, z = 10) hívásnál x értéke 1 lesz, a list(...) egy háromelemű lista lesz 2, 3 és 4 értékekkel (amiből a középsőnek a a neve, a másik kettőnek nincs neve), y értéke 1, z értéke 10. (Mint látható, ilyenkor, ha y-nak vagy z-nek szeretnénk beállítani az értékét, akkor kötelező megadni a nevét a hívásban, különben az R azt hinné, hogy a megadott argumentum a ... része.)

Zárásként még egy apróság: bizonyos esetekben felmerül a kérdés, hogy mi a teendő akkor, ha úgy kell egy függvényt meghívnunk, hogy mi magunk sem tudjuk előre (tehát a programkód írásakor) az argumentumait, például mert egy másik függvény állítja elő. Ilyenkor célszerű ezeket egy listába helyezni, az R ugyanis kínál egy megoldási lehetőséget erre:

fuggvenyargs <- list(n = 10, mean = 70, sd = 15)
do.call(rnorm, fuggvenyargs)
 [1] 89.23955 82.26327 67.51704 41.56394 97.86803 79.59809 88.41060 74.32425
 [9] 89.91716 76.01268

A do.call tehát meghívja az első argumentumban megadott függvényt a második argumentumban megadott argumentumokkal. A do.call lényegében elválasztja egymástól a függvény nevét és argumentumait (ami az rnorm(sd = 15, n = 10, mean = 70) típusú hívásnál össze van gubancolódva); ezzel megoldást adva a fentiekben említett helyzetre is. Ha a függvény argumentumai között ... van, a do.call akkor is működik, vagyis az átadott lista hossza nem kötelező, hogy mindig ugyanaz legyen.

3.5 Az R-ben minden utasítás függvényhívás

Érdemes még egy dologról beszélni, ami látszólag egy erősen technikai aspektus, de valójában fontos következményei vannak: arról, hogy az R-ben minden utasítás igazából függvényhívás.

Ennek első ránézésre pár dolog ellentmond; nézzük most meg ezeket közelebbről. Kezdjük kapásból ezzel:

1 + 2
[1] 3

Ez hogy lenne már függvényhívás?! Nincs is benne függvénynév, pláne nincs gömbölyű zárójel, meg argumentumok…! – mondhatná valaki. Azonban a helyzet az, hogy de, van benne függvénynév, a +. Ez igenis egy függvény neve, nyugodtan leírhatjuk szokásos formában is, egyedül arra kell figyelni, hogy a + karakter nem felel meg a változónevekre vonatkozó, korábban látott szabályoknak – úgynevezett nem szintaktikus név – ezért a szokásos formájú hívásnál backtick-ek közé kell tenni:

`+`(1, 2)
[1] 3

Egyszerűen arról van szó, hogy az R ad nekünk annyi segítséget, hogy megengedi, hogy a zárójelet nem kell kitenni, de ami még fontosabb, hogy lehetővé teszi, hogy ilyen, úgynevezett operátorok a szokásos módon, az argumentumok között szerepeljenek. (Ezt hívják amúgy a programozáselméletben infix jelölésnek, míg a második, „szokásos függvényhívás” formát prefix jelölésnek.)

Mi a helyzet az értékadással? Mondjuk:

x <- 5
x
[1] 5

Itt már talán nem okozok meglepetést: egyszerűen arról van szó, hogy a <- az igazából egy függvény. Úgyhogy igen, bármilyen meglepő, de a fenti igazából ezzel egyenértékű:

`<-`(x, 10)
x
[1] 10

Az egyetlen, ami fontos, hogy ez egy mellékhatásos függvény, ahol a mellékhatás az, hogy a memóriában valami elhelyeződik.

A végére hagytam a talán legfontosabb példát. Mi a helyzet azzal, amit eddig úgy hívtunk, hogy „kiíratjuk egy változó értékét”? Például:

x
[1] 10

Na itt aztán végképp nincsen semmilyen függvényhívás…! – mondhatná valaki. De! Nagyon is van, a helyzet ugyanis az, hogy az R, miután végzett a kiértékelésekkel, az eredményre automatikusan meghívja a print függvényt. (Meglepő lehet, hogy kiértékelést említettem – mit kell abban kiértékelni, hogy x? – és ez valóban egyértelműbb lenne, ha azt írtam volna, hogy exp(x), de valójában az x is kiértékelhető, egyszerűen azzal a szabállyal, hogy egy változó kiértékeltje saját maga.) A fenti tehát igazából ezt jelenti:

print(x)
[1] 10

A print pedig egy mellékhatásos függvény, ahol a mellékhatás az, hogy az eredmény megjelenik a konzolon.

(Zárójel: tehát az 1 + 2 az nem csak `+`(1, 2) valójában, hanem print(`+`(1, 2))…!)

A dolog azért fontos, mert az R-ben van egy viselkedés, ami csak a fentiek ismeretében érthető meg. Töltsük be a példa-adatbázisunkat, és hajtsunk végre egy egyszerű statisztikai próbát:

data(birthwt, package = "MASS")
t.test(bwt ~ smoke, data = birthwt)

    Welch Two Sample t-test

data:  bwt by smoke
t = 2.7299, df = 170.1, p-value = 0.007003
alternative hypothesis: true difference in means between group 0 and group 1 is not equal to 0
95 percent confidence interval:
  78.57486 488.97860
sample estimates:
mean in group 0 mean in group 1 
       3055.696        2771.919 

Hogy mi ez a próba, az most teljesen mindegy is (\(t\)-próbával vizsgáljuk, hogy a dohányzó és a nem-dohányzó anyukák újszülttjeinek várható születési tömege szignifikánsan eltér-e), a fontos, hogy valamilyen komplexebb statisztikai elemzés. Mit látunk? Egy szép, ízlésesen elrendezett eredményt, benne a legfontosabb számokkal, sőt, még némi magyarázó szöveggel is kiegészítve. Ami most lényeges: úgy tűnik, hogy működik a szabály, hogy „ha nincs nyíl, akkor kiíratunk, de nem mentünk”: a kiíratás valóban megjelent, és új változó valóban ne jött létre. Ellenőrizzük le a fordított irányt:

teszteredmeny <- t.test(bwt ~ smoke, data = birthwt)

Továbbra is stimmel minden: most nem íratódott ki semmi, de létrejött az új változó. Ahogy vártuk! Most nézzük meg, mi van a változóban:

teszteredmeny

    Welch Two Sample t-test

data:  bwt by smoke
t = 2.7299, df = 170.1, p-value = 0.007003
alternative hypothesis: true difference in means between group 0 and group 1 is not equal to 0
95 percent confidence interval:
  78.57486 488.97860
sample estimates:
mean in group 0 mean in group 1 
       3055.696        2771.919 

Ez is pontosan megfelel a várakozásunknak: az van benne, amit korábban a kiíratásnál kaptunk. Mi más lenne? Nos, nézzük kicsit meg közelebbről ezt a változót:

str(teszteredmeny)
List of 10
 $ statistic  : Named num 2.73
  ..- attr(*, "names")= chr "t"
 $ parameter  : Named num 170
  ..- attr(*, "names")= chr "df"
 $ p.value    : num 0.007
 $ conf.int   : num [1:2] 78.6 489
  ..- attr(*, "conf.level")= num 0.95
 $ estimate   : Named num [1:2] 3056 2772
  ..- attr(*, "names")= chr [1:2] "mean in group 0" "mean in group 1"
 $ null.value : Named num 0
  ..- attr(*, "names")= chr "difference in means between group 0 and group 1"
 $ stderr     : num 104
 $ alternative: chr "two.sided"
 $ method     : chr "Welch Two Sample t-test"
 $ data.name  : chr "bwt by smoke"
 - attr(*, "class")= chr "htest"

És akkor itt jön a meglepetés: ez a változó valójában egy 10 elemű lista…! De akkor hogyan lett a kiíratásakor ebből egy ilyen szöveg?! (A kiírt dolgok egy része, például ezek a magyarázó szövegek még csak benne sincsenek a változóban!) Hogy jelenhetett akkor ez meg a kiíratáskor?

A válasz a fenti output utolsó sorában rejlik. Amit itt látunk, hogy van egy class nevű attribútuma a változónak, ami htest értékre van beállítva. Mint tudjuk, bármilyen nevű attribútumot definiálhatunk egy változóhoz, de a class különlegesen viselkedik. A részletek megtárgyalása nélkül: valójában nem egyetlen print függvény van, hanem több tucatnyi, és az R-ben van egy belső mechanizmus, ami az alapján választja ki, hogy pontosan melyik fut le, hogy mi a kiíratott változó class-a. Ha htest, akkor igazából a print.htest fog lefutni16. Ez pedig így néz ki (figyeljük meg, hogy a ::: operátort kell használnunk, mert ez a függvény ugyan benne van az – automatikusan betöltődő – stats csomagban, de nem exportált függvény, ami logikus is, mert kívülről direkte nem kell meghívnunk):

stats:::print.htest
function (x, digits = getOption("digits"), prefix = "\t", ...) 
{
    cat("\n")
    cat(strwrap(x$method, prefix = prefix), sep = "\n")
    cat("\n")
    cat("data:  ", x$data.name, "\n", sep = "")
    out <- character()
    if (!is.null(x$statistic)) 
        out <- c(out, paste(names(x$statistic), "=", format(x$statistic, 
            digits = max(1L, digits - 2L))))
    if (!is.null(x$parameter)) 
        out <- c(out, paste(names(x$parameter), "=", format(x$parameter, 
            digits = max(1L, digits - 2L))))
    if (!is.null(x$p.value)) {
        fp <- format.pval(x$p.value, digits = max(1L, digits - 
            3L))
        out <- c(out, paste("p-value", if (startsWith(fp, "<")) fp else paste("=", 
            fp)))
    }
    cat(strwrap(paste(out, collapse = ", ")), sep = "\n")
    if (!is.null(x$alternative)) {
        cat("alternative hypothesis: ")
        if (!is.null(x$null.value)) {
            if (length(x$null.value) == 1L) {
                alt.char <- switch(x$alternative, two.sided = "not equal to", 
                  less = "less than", greater = "greater than")
                cat("true ", names(x$null.value), " is ", alt.char, 
                  " ", x$null.value, "\n", sep = "")
            }
            else {
                cat(x$alternative, "\nnull values:\n", sep = "")
                print(x$null.value, digits = digits, ...)
            }
        }
        else cat(x$alternative, "\n", sep = "")
    }
    if (!is.null(x$conf.int)) {
        cat(format(100 * attr(x$conf.int, "conf.level")), " percent confidence interval:\n", 
            " ", paste(format(x$conf.int[1:2], digits = digits), 
                collapse = " "), "\n", sep = "")
    }
    if (!is.null(x$estimate)) {
        cat("sample estimates:\n")
        print(x$estimate, digits = digits, ...)
    }
    cat("\n")
    invisible(x)
}
<bytecode: 0x00000169ea03d888>
<environment: namespace:stats>

Nem fontos, hogy a fenti kód minden egyes részét pontosan értsük, a fontos az összkép: ez tartalmazza a szép, ízléses, magyarázott kiíratás sémáját! Ebben van benne az elrendezés, az összekötő szövegek stb., természetesen úgy, hogy a dolog hivatkozik a kiírandó változó megfelelő számaira – amik ugyebár a lista egyes elemei. Fix szövegként kiírja, úgy sorba, hogy „sample estimates”, azaz a becslések a mintából, utána pedig jön az x$estimate, vagyis, hogy „vedd ki az x-ből – ami a kiírandó változó – az estimate nevű elemet”. És menjünk vissza, nézzük meg: valóban az lesz a lista estimate nevű elemében, ami megjelenik a „sample estimates” után! A (megfelelő) print tartalmazza a sémát, a konkrét számok pedig kitöltődnek a ki-print-elt változóból, mint listából.

Egyébként kipróbálhatjuk, hogy ha rákényszerítjük, hogy ne a testreszabott, hanem az alapértelmezett print függvényt használja az R a kiíratáshoz, akkor valóban úgy kapjuk meg a teszteredmeny változó tartalmát, mint egy szokásos lista:

print.default(teszteredmeny)
$statistic
       t 
2.729886 

$parameter
      df 
170.1002 

$p.value
[1] 0.007002548

$conf.int
[1]  78.57486 488.97860
attr(,"conf.level")
[1] 0.95

$estimate
mean in group 0 mean in group 1 
       3055.696        2771.919 

$null.value
difference in means between group 0 and group 1 
                                              0 

$stderr
[1] 103.9519

$alternative
[1] "two.sided"

$method
[1] "Welch Two Sample t-test"

$data.name
[1] "bwt by smoke"

attr(,"class")
[1] "htest"

Így már jobban érthető a listák jelentősége is, amit a korábbi fejezetben nem tudtam pontosan elmondani: a listák rengetegszer előfordulnak – akkor is, amikor mi nem látjuk! Hiszen ebben a \(t\)-próbás példában sehol nem bukkantak fel explicite listák, nem kellett listát kezelnünk, azt sem kellett tudnunk, hogy mi az, hogy lista – de a háttérben nagyon is volt szerepük, listák mozogtak a különböző függvények között. A kicsit is komplexebb statisztikai számítások nagyon gyakran listát adnak vissza (még ha ezt mi nem is látjuk), ami teljesen logikus ha meggondoljuk, hiszen tipikus, hogy ezek különböző típusú adatokat tartalmaznak. Ez így van a \(t\)-próbás példában is, ahol előfordul szám, szöveg, egyelemű és többelemű vektor. Az ilyenek tárolására pont a lista a megfelelő adatszerkezet.

A class-t természetesen mi magunk is kihasználhatjuk a saját függvényeinkben. A fentiek ugyanis azt jelentik, hogy ha beállítunk egy class-t a függvényünk által visszaadott listára és definiálunk egy print-et arra a class-ra, akkor a függvényünk eredménye rögtön szépen megformázva fog megjelenni – anélkül, hogy a felhasználónak bármit tennie kellett volna (vagy akár csak tudna erről az egészről)!


  1. Az R a paramétert formális argumentumnak hívja; ezt a szóhasználatot egyébként a magyar szaknyelv is megengedi mint a paraméter szinonimája. De sokszor nem vesződnek ezzel a különbségtétellel, és egyszerűen argumentumot mondanak.↩︎

  2. Az alternatívája a cím szerinti paraméterátadás, ami nagyon leegyszerűsítve az, ha a függvény nem a változó egy másolatát kapja meg, hanem azt a memóriacímet, ahol a változó van. Ez más, mert bár kiolvasva a memóriacímet megkapjuk a változó tartalmát, ugyanúgy mint az érték szerinti átadásnál, viszont a memóriacím ismeretében akár módosíthatjuk is azt: ha valamit átírunk ott, az módosítja a hívó program számára is a változót. Cím szerinti átadáshoz hasonló mechanizmus megvalósítható R-ben is, de külön erőfeszítést igényel, az alapvető működési mód az R-ben az érték szerinti átadás. A valóságban az érték és cím szerinti átadásnak sok egyéb finomsága van, más paraméterátadási módok is léteznek, de ezek számunkra nem lesznek most fontosak.↩︎

  3. Ezt hívják copy-on-modify mechanizmusnak, ez szabályozza, hogy mikor készül másolat, és ha nem készül, akkor hogyan oldja meg a paraméterátadást.↩︎

  4. És az érték szerinti paraméterátadás miatt argumentumot sem használhatunk információ visszajuttatására, ahogy az más nyelvben előfordulhatna.↩︎

  5. Ezen a ponton visszatérhetünk egy pillanatra arra a korábbi megállapításra, hogy egy függvény lecserélhető a visszatérési értékére. Ha ez megvalósul, akkor azt mondjuk, hogy fennáll a hivatkozási átlátszóság; ennek a feltétele, hogy a függvény mellékhatásmentes legyen, és adott argumentumokra mindig, determinisztikusan ugyanazt a visszatérési értéket szolgáltassa. Az ilyeneket tiszta függvénynek szokták hívni a programozáselméletben.↩︎

  6. Legegyszerűbb esetben használhatjuk a stopifnot függvényt, de van külön csomag is erre a célra, például az assertthat vagy a checkmate.↩︎

  7. A base, a stats, a methods, a graphics, a grDevices, a utils és a datasets. Egész pontosan az a mechanizmus, hogy a base csomag mindenképp betöltődik induláskor, utána pedig azok töltődnek még be ezen felül automatikusan, amik a defaultPackages nevű opcióban szereplnek; az előbbi lista ennek az alapértelmezését tükrözi.↩︎

  8. Az R szokásos installációja a korábban említett, induláskor automatikusan betöltődő csomagokon kívül még kb. egy tucatnyi csomagot telepít, tehát ezeket be kell ugyan kézzel tölteni, de külön telepíteni nem kell. Ezen kívül minden más csomagot először telepíteni kell.↩︎

  9. Egyetlen megjegyzés ehhez. A CRAN-en a csomagok fent vannak forráskóddal, illetve Windows-ra előre lefordított (binary) verzióban is. Ez azt jelenti, hogy Linux-on a CRAN-ről történő telepítés mindenképp a forráskód fordítását fogja jelenteni, Windows-on viszont nem feltétlenül. Néha azonban az a helyzet, hogy a forráskódú verzió frissebb, mint a lefordított; ilyenkor a Windows-on is jól jöhet a forráskódból fordítás. Ehhez azonban fordítóeszköz fog kelleni! Szerencsére ehhez van kész eszköztár az R-hez, a neve RTools, érdemes telepíteni.↩︎

  10. Ennek van egy alternatívája, a require függvény. A kettő nagyon hasonlít egymásra, az egyetlen különbség akkor bukik ki, ha nem telepített könyvtárat akarunk betölteni: ekkor a library hibát ad, a require csak figyelmeztetést, és visszatérési értékként FALSE-ot (sikeres betöltés esetén ezzel szemben TRUE-t). Ebből következik, hogy interaktív munkamenetnél – magyarán, ha kézzel pötyögünk, meg hajtatunk végre kódsorokat – a library tökéletes, viszont ha csomag-betöltést egy futó program maga végzi, akkor a require jobb lehet, mert gépi úton tudjuk kezelni a helyzetet, hogy volt-e az adott gépen telepítve a csomag, sikerült-e betölteni, és ennek fényében továbbmenni. De vigyázat: ha ezt a kezelést nem hajtjuk végre, akkor a require veszélyes lehet, mert azt okozhatja, hogy a futás nem áll meg azonnal, és csak később – praktikusan amikor az adott könyvtárból akarunk egy függvényt hívni – lesz baj, amit viszont így nehezebb lesz beazonosítani, hogy mi okoz. Mivel azonban egy csomag elérhetőségének az ellenőrzésére van más megoldás, a requireNamespace függvény, így van, aki azt javasolja, hogy require-t egyáltalán ne használjunk.↩︎

  11. Elvileg egy nagyon minimális teljesítmény-veszteséget jelent a :: (mivel a háttérben egy függvényhívást jelent igazából), de ennek a legtöbb gyakorlati esetben nem lesz érdemi jelentősége: én most tettem egy próbát, az actuar::rinvexp(10) medián futásideje a gépemen 1,9 \(\mu\)s, az actuar csomag library-vel betöltése után az rinvexp(10)-é 1,6 \(\mu\)s. A különbség 300 ns, azaz ha egymillió alkalommal használjuk a :: operátort egy kódban, az összesen 0,3 másodperc veszteséget fog jelenteni… Egy fokkal talán nyomósabb érv lehet a library mellett, hogy ha a szkriptünket azzal kezdjük, hogy az összes használt csomagot be-library-zzük, akkor egyrészt azonnal látszik, hogy mikre lesz szükség, másrészt, ha valamelyik nincs meg a futtató felhasználó gépén, akkor az rögtön az elején kiderül a hibából. A library-k használata nélkül ez csak az első ::-nál bukna ki, ami nem elegáns: általános programtervezési elv, hogy ha valami baj van, akkor jobb, ha az minél előbb kiderül.↩︎

  12. Ez a helyi gépen keres, ebből fakadóan természetesen csak azokat a csomagokat tudja végigkeresni, amik telepítve vannak helyileg. Létezik három kérdőjeles változat (???rno) is, ami az R központi weboldalán keres, így – többek között – valamennyi csomagot végignézi, függetlenül attól, hogy helyileg telepítve van-e. Ez azonban egy külön csomag, az sos telepítését igényli (a három kérdőjel ugyanis igazából nem más, mint egy hozzárendelt rövidítés, ún. alias az sos::findFn függvényhez, ami ilyen keresést hajt végre).↩︎

  13. Az értékadásnál említettem, hogy a nyíl helyett szinte felcserélhetően használhatnánk egyenlőségjelet is, és hozzátettem, hogy az R-es szokás az, hogy az egyenlőségjelet másra használjuk. Akkor most már elárulhatom: erre! A szokás az, hogy az értékadásnál nyilat írunk, függvény argumentumának megadásakor egyenlőségjelet. A kettő között a különbség minimális.↩︎

  14. Hogy teljesen precíz legyek, az algoritmus a következő: először fogja az elnevezett argumentumokat, és megkeresi, hogy pontosan olyan névvel van-e paraméter, ha igen, akkor megfelelti őket egymásnak. Ha marad még hívásban szereplő argumentum, aminek nem feleltetett meg paramétert, akkor tovább megy: ezt követően – és ez meglepő lehet – részleges egyezéseket is keres az elnevezett argumentumok között: megnézi, hogy van-e olyan, ahol a hívásban szereplő név egyezik a paraméter nevének az elejével. Például ha a hívásban atl néven hivatkozunk egy argumentumra az át fog adódni az atlag paraméternek (feltéve, hogy nem adtunk át atlag nevű argumentumot is, mert az az első lépésben már „megenné” az atlag nevű paramétert). Végezetül, ha még így is van nem összerendelt argumentum és paraméter, akkor az összes megmaradt argumentumot sorrendben osztja ki a megmaradt paramétereknek.↩︎

  15. Angolul a neve ellipsis; semmi köze az ellipszishez, egyszerűen arról van szó, hogy angolul, mármint a nyelvészetben, így hívják a – tipikusan mondat végi – három pontot. Az R nemes egyszerűséggel „dot-dot-dot”-nak is hívja.↩︎

  16. A print-nek természetesen van egy alapértelmezett variánsa is, a print.default, ez fut akkor, ha nincs class, vagy az adott class-hoz nincs külön definiált print.↩︎