A függvényekkel már az Alapok fogalomnál találkoztunk, ilyen példákkal:
squareit <- function(x) {
x * x
}
squareit(3)
#> [1] 9
# shorter form
squareit_short <- function(x) x ^ 2
Ha közelebbről megnézzük a squareit definícióját, több részt is azonosíthatunk benne:
x formális argumentumot vár.{ }.squareit nevű változóhoz.R-ben a függvény első osztályú objektum, akárcsak a számok vagy a stringek.
A squareit <- function... tehát olyan értékadás, amely szintaktikailag pontosan olyan, mint az x <- 42.
A függvények összetevőihez hozzáférni ritkán szoktunk, de meglehetősen egyszerű.
class(squareit)
#> [1] "function"
formals(squareit)
#> $x
body(squareit)
#> {
#> x * x
#> }
A formals() függvénnyel lekérdezett argumentumok egy listához hasonlítanak, a $x szintaxissal.
Valójában a típusuk pairlist, a listák egy különleges fajtája, amely kulcs-érték párokat tartalmaz.
A body futtatható kód, a típusa language (ez valószínűleg nem az, amivel neked foglalkoznod kell).
Az R nem tesz éles különbséget a pozicionális és a név szerinti argumentumok között, ellentétben más szkriptnyelvekkel, például a Pythonnal vagy a Juliával.
Egy függvényhívásban az értékeket átadhatjuk pozicionálisan vagy név szerint is. Ez utóbbi bonyolult, sok argumentumot váró függvényeknél hasznos, ahol nehéz megjegyezni az argumentumok sorrendjét.
f <- function(x, y) x / y
# call positionally
f(4, 2)
#> [1] 2
# call by name
f(y = 2, x = 4)
#> [1] 2
A függvénydefinícióban megadhatunk alapértelmezett argumentumértékeket, de ezeknek az összes alapérték nélküli argumentum után kell következniük.
Ezután eldönthetjük, hogy elfogadjuk-e az alapértéket, vagy felülírjuk.
g <- function(x, y = 2) x / y
# default y value
g(6)
#> [1] 3
# explicit y value
g(6, 3)
#> [1] 2
Ha tetszőleges számú további argumentumot szeretnénk fogadni, használjunk ... (ellipszis) jelet a definícióban.
A függvényhívásban átadott extra értékeket vektorrá is alakíthatjuk, de olvass tovább, mert mutatunk egy másik módot ezeknek a »pontargumentumoknak« a használatára (más nyelvekben »varargs« a nevük).
var_f <- function(x, y, ...) {
print(c(...))
}
var_f(2, 3, "opt1", "opt2")
#> [1] "opt1" "opt2"
Korábban azt mondtuk, hogy a formális argumentumok és a törzs is a függvény összetevői.
Valójában van egy harmadik összetevő is: a környezet, amelyben a függvényt definiáljuk.
Ezt az egymásba ágyazott függvények példájával szemléltethetjük:
outer_func <- function(x) {
inner_func <- function(y) {
x * y
}
inner_func(3)
}
outer_func(5)
#> [1] 15
A függvényhívás átadja az x = 5 értéket a külső függvénynek, és ez az érték elérhető annak a függvénytörzsén belül.
A belső függvény a külső függvénytörzs része, és hozzáfér az x értékéhez.
Másképp megfogalmazva: az x = 5 a belső függvény környezetében van.
Technikailag ezt closure-nak nevezik.
A környezet különösen fontos a pontargumentumoknál, mert az így átadott értékek továbbadhatók a függvénytörzsben lévő függvényhívásoknak. A külső függvénynek nem kell tudnia, sőt nem is kell érdekelnie, hogy mit jelentenek a pontargumentumok.
f_var <- function(x, ...) {
sum(x, ...)
}
x <- c(1, 2, NA, 6)
# for sum(), na.rm defaults to FALSE
f_var(x)
#> [1] NA
# pass through the na.rm value
f_var(x, na.rm = TRUE)
#> [1] 9
Ezt a technikát széles körben használják a Tidyverse könyvtárai, például a stringr.
A stringr számos függvénye az stringi és a base R alacsony szintű függvényei köré épülő, felhasználóbarát burkoló.
A str_*() függvényeknek átadott extra argumentumok egyszerűen továbbadódnak ezeknek az alacsonyabb szintű függvényeknek.
Egy külső függvény definiálhat egy belső függvényt (nevesítettet vagy névtelent, ahogy azt lentebb leírjuk), és visszatérési értékként használhatja azt.
A visszaadott függvény magában hordozza azt a környezetet, amelyben definiálták.
times_y <- function(x) {
# anonymous function - see next section
\(y) x * y
}
f <- times_y(3)
class(f)
#> [1] "function"
f(4)
#> [1] 12
Amikor függvényt definiálunk, a létrejövő függvényobjektumot általában egy változóhoz kötjük:
x <- 2
# function definition captures x from the environment
doubleit_short <- function(y) x * y
Ez megkönnyíti a függvény későbbi használatát a szkriptben, de az ilyen hozzákötés nem szükségszerű. Egy rövid, csak egyszer használt függvény hasznos lehet az adott helyzetben. Névhez kötés nélkül névtelen függvénynek nevezzük.
A névtelen függvények használata annyira gyakori, hogy (az R v4.1.0-tól) van egy rövidebb szintaxis a definiálásukra: a function szót cseréld le egy visszaperjelre: \.
Erre láttunk példát az előző szakaszban, a visszatérési értékeknél.
Az R-ben egy vektor egyes elemeihez is hozzárendelhetünk értéket. Ha egy vektort argumentumként adunk át egy függvénynek, és a függvénytörzsben módosítjuk, mielőtt visszaadnánk, egy módosított vektort kapunk.
De mi történt az eredeti vektorral?
f <- function(vec) {
vec[1] <- 42
vec
}
vals <- c(1, 3, 4)
# f() returns a modified vector
f(vals)
#> [1] 42 3 4
# the original vector is unchanged.
vals
#> [1] 1 3 4
Az R egy adattudományra tervezett nyelv. Az adatok összegyűjtése sok időbe és energiába kerülhet, és potenciálisan fájdalmasan sok pénzbe: ezért fontos, hogy ne sérüljenek meg!
Az általános szabály (néhány kivételtől eltekintve) a másolás módosításkor. Ha egy objektumot (például egy vektort) olyan módon változtatunk meg, ami később problémát okozhatna, az R egy módosított másolatot ad vissza, az eredetit pedig érintetlenül hagyja.
Nagy adatszerkezetek másolása számításigényes lehet, de ez általában a kisebbik rossz, ha az alternatíva az adatok megsérülése.
Vigyázz azokkal a műveletekkel, amelyek ugyanazon adatok ismételt másolásához vezetnek.
(A Tidyverse szerzőjét) Hadley Wickhamet parafrazálva:
A ciklusok önmagukban nem lassúak, de veszélyesen könnyűvé teszik, hogy lassú műveleteket tegyünk a ciklusba.
A vektorizálás vagy a magasabb rendű függvények segíthetnek megvédeni ettől a fajta teljesítménygyilkostól.