Funktionen wurden bereits im Konzept Basics vorgestellt, mit Beispielen wie diesem:
squareit <- function(x) {
x * x
}
squareit(3)
#> [1] 9
# shorter form
squareit_short <- function(x) x ^ 2
Wenn wir uns die Definition von squareit genauer ansehen, können wir verschiedene Teile erkennen:
x entgegen.{ }.squareit zugewiesen.Eine Funktion ist in R ein First-Class-Objekt, ganz ähnlich wie Zahlen und Strings.
squareit <- function... ist also eine Zuweisung, die syntaktisch genauso aussieht wie x <- 42.
Auf die Bestandteile einer Funktion zuzugreifen ist im normalen Gebrauch selten, aber recht einfach.
class(squareit)
#> [1] "function"
formals(squareit)
#> $x
body(squareit)
#> {
#> x * x
#> }
Die Argumente, die du mit formals() erhältst, sehen aus wie eine Liste, mit der $x-Syntax.
Tatsächlich ist der Typ pairlist, eine besondere Art von Liste, die Schlüssel-Wert-Paare enthält.
Der body ist ausführbarer Code mit dem Typ language (etwas, um das du dich wahrscheinlich nicht kümmern musst).
Im Gegensatz zu anderen Skriptsprachen wie Python und Julia unterscheidet R nicht klar zwischen positionellen Argumenten und Schlüsselwortargumenten.
Bei einem Funktionsaufruf kannst du Werte entweder positionell oder per Namen übergeben. Letzteres ist bei komplexen Funktionen mit vielen Argumenten nützlich, bei denen es schwer ist, sich die Reihenfolge zu merken.
f <- function(x, y) x / y
# call positionally
f(4, 2)
#> [1] 2
# call by name
f(y = 2, x = 4)
#> [1] 2
Standardwerte für Argumente kannst du in der Funktionsdefinition angeben, aber sie müssen nach allen Argumenten ohne Standardwert stehen.
Danach können wir entscheiden, ob wir den Standardwert übernehmen oder ihn überschreiben.
g <- function(x, y = 2) x / y
# default y value
g(6)
#> [1] 3
# explicit y value
g(6, 3)
#> [1] 2
Um eine beliebige Anzahl zusätzlicher Argumente zu akzeptieren, verwendest du ein ... (Ellipse) in der Definition.
Es ist möglich, alle zusätzlichen Werte im Funktionsaufruf in einen Vektor umzuwandeln, aber lies weiter, um eine alternative Verwendung dieser „Punkt-Argumente“ kennenzulernen (in mehreren anderen Sprachen „varargs“ genannt).
var_f <- function(x, y, ...) {
print(c(...))
}
var_f(2, 3, "opt1", "opt2")
#> [1] "opt1" "opt2"
Vorher haben wir gesagt, dass die formalen Argumente und der Funktionskörper beides Bestandteile einer Funktion sind.
Tatsächlich gibt es eine dritte Komponente: die Umgebung, in der die Funktion definiert ist.
Das lässt sich am Fall verschachtelter Funktionen veranschaulichen:
outer_func <- function(x) {
inner_func <- function(y) {
x * y
}
inner_func(3)
}
outer_func(5)
#> [1] 15
Der Funktionsaufruf übergibt x = 5 an die äußere Funktion, und dieser Wert ist im Funktionskörper dieser Funktion verfügbar.
Die innere Funktion ist Teil des Funktionskörpers der äußeren Funktion und hat Zugriff auf den Wert von x.
Anders formuliert: x = 5 liegt in der Umgebung der inneren Funktion.
Technisch nennt man das ein Closure.
Die Umgebung ist besonders wichtig bei Punkt-Argumenten, denn alle Werte, die auf diesem Weg übergeben werden, können an Funktionsaufrufe im Funktionskörper weitergegeben werden. Die äußere Funktion muss nicht wissen und sich nicht darum kümmern, was die Punkt-Argumente bedeuten.
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
Diese Technik wird von Tidyverse-Bibliotheken wie stringr ausgiebig genutzt.
Viele der stringr-Funktionen sind ein benutzerfreundlicher Wrapper um Low-Level-Funktionen aus stringi und Base R.
Zusätzliche Argumente, die an die str_*()-Funktionen übergeben werden, werden einfach an diese tiefer gelegenen Funktionen weitergegeben.
Eine äußere Funktion kann eine innere Funktion definieren (entweder benannt oder anonym, wie unten beschrieben) und sie als Rückgabewert verwenden.
Die zurückgegebene Funktion enthält die Umgebung, in der sie definiert wurde.
times_y <- function(x) {
# anonymous function - see next section
\(y) x * y
}
f <- times_y(3)
class(f)
#> [1] "function"
f(4)
#> [1] 12
Wenn wir eine Funktion definieren, binden wir das entstehende Funktionsobjekt normalerweise an eine Variable:
x <- 2
# function definition captures x from the environment
doubleit_short <- function(y) x * y
Das macht es einfach, die Funktion später im Skript zu verwenden, aber eine solche Bindung ist nicht notwendig. Eine kurze Funktion, die nur einmal verwendet wird, kann im unmittelbaren Kontext nützlich sein. Ohne Namensbindung nennt man sie eine anonyme Funktion.
Die Verwendung anonymer Funktionen ist so verbreitet, dass es (seit R v4.1.0) eine Kurzsyntax gibt, um sie zu definieren: Ersetze das Wort function durch einen Backslash \.
Ein Beispiel dafür wurde im vorigen Abschnitt über Rückgabewerte verwendet.
R erlaubt die Zuweisung an einzelne Elemente eines Vektors. Wenn wir einen Vektor als Funktionsargument übergeben und ihn im Funktionskörper ändern, bevor wir ihn zurückgeben, erhalten wir einen geänderten Vektor.
Aber was ist mit dem ursprünglichen Vektor passiert?
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
R ist eine Sprache, die für Data Science entwickelt wurde. Das Sammeln dieser Daten kann viel Zeit, Mühe und potenziell schwindelerregend viel Geld kosten: Es ist wichtig, sie nicht zu beschädigen!
Die allgemeine Regel (mit einigen Ausnahmen) ist copy on modify. Wenn ein Objekt (etwa ein Vektor) so geändert wird, dass es später Probleme verursachen könnte, gibt R eine geänderte Kopie zurück und lässt das Original unberührt.
Das Kopieren großer Datenstrukturen kann rechenintensiv sein, aber das ist im Allgemeinen das kleinere Übel, wenn die Alternative eine Beschädigung der Daten ist.
Vorsicht vor Operationen, die zum wiederholten Kopieren derselben Daten führen.
In Anlehnung an (den Tidyverse-Autor) Hadley Wickham:
Schleifen sind nicht von Natur aus langsam, aber sie machen es gefährlich leicht, langsame Operationen in die Schleife aufzunehmen.
Vektorisierung oder Funktionen höherer Ordnung können dir helfen, dich vor dieser Art von Performance-Killer zu schützen.