Funktionen wurden bereits im Grundlagen-Konzept 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.In R ist eine Funktion ein First-Class-Objekt, ganz ähnlich wie Zahlen und Strings.
Somit ist squareit <- function... eine Zuweisung, die syntaktisch genauso aussieht wie x <- 42.
R unterscheidet nicht klar zwischen positionellen Argumenten und Schlüsselwort-Argumenten, anders als andere Skriptsprachen wie Python und Julia.
Funktionsaufrufe können Werte entweder positionell oder über den Namen übergeben. Letzteres ist nützlich bei komplexen Funktionen mit vielen Argumenten, bei denen es schwer ist, sich ihre 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 können in der Funktionsdefinition angegeben werden, müssen aber nach allen Argumenten ohne Standardwert stehen.
Wir können dann wählen, ob wir den Standardwert akzeptieren 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 „Dot-Args“ kennenzulernen (in einigen anderen Sprachen „Varargs“ genannt).
var_f <- function(x, y, ...) {
print(c(...))
}
var_f(2, 3, "opt1", "opt2")
#> [1] "opt1" "opt2"
Vorhin haben wir gesagt, dass die formalen Argumente und der Kö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 Beispiel 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 innerhalb dieses Funktionskörpers verfügbar.
Die innere Funktion ist Teil des Körpers der äußeren Funktion und hat Zugriff auf den Wert von x.
Anders formuliert: x = 5 befindet sich in der Umgebung der inneren Funktion.
Fachlich spricht man hier von einer Closure.
Die Umgebung ist besonders wichtig bei Dot-Args, da alle auf diese Weise übergebenen Werte an Funktionsaufrufe im Funktionskörper weitergegeben werden können. Die äußere Funktion muss nicht wissen und sich nicht darum kümmern, was die Dot-Args 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 Low-Level-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.
Anonyme Funktionen werden so häufig verwendet, dass es (seit R v4.1.0) eine Kurzschreibweise 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.
Doch 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 möglicherweise eine schwindelerregend hohe Summe Geld kosten: Es ist wichtig, sie nicht zu beschädigen!
Die allgemeine Regel (mit ein paar Ausnahmen) lautet 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 meist das kleinere Übel, wenn die Alternative eine Datenbeschädigung ist.
Hüte dich vor Operationen, die zum wiederholten Kopieren derselben Daten führen.
Frei nach (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.
Dein Unternehmen hat mit Mapping-Daten gearbeitet, um Webseiten zu erstellen. Nach einiger Diskussion wird entschieden, R zu verwenden, um einige Berechnungen dynamisch durchzuführen.
Versuche, direkt mit den Vektorpunkten zu arbeiten: Es ist nicht nötig, sie in getrennte x- und y-Koordinaten aufzuteilen.
Implementiere die Funktion scale(point, s), die einen Punkt mit beliebiger Anzahl von Dimensionen entgegennimmt und ihn um das vordefinierte s skaliert.
# A 2D example
s <- c(0.5, 0.8)
point <- c(10, 5)
scale(point, s)
#> [1] 5 4
Implementiere die Funktion translate(point, ...), die einen neuen Punkt zurückgibt, bei dem jede Koordinate um die Werte in der Ellipse verschoben wird.
Diese Funktion muss Punkte beliebiger Dimension verarbeiten können, wobei die Anzahl der Ellipsen-Argumente zu dieser Dimension passt.
point2d <- c(2, 3)
# supply dx, dy translations
translate(point2d, 0.5, 0.6)
#> [1] 2.5 3.6
point3d <- c(2, 3, 4)
# supply dx, dy, dz translations
translate(point3d, 0.5, 0.6, 0.7)
#> [1] 2.5 3.6 4.7
Einige deiner Teammitglieder haben weniger Erfahrung mit R, deshalb entscheidest du dich, eine Closure zu verwenden, um wiederverwendbare Transformationen für {x, y}-Koordinatenpaare zu erstellen.
Implementiere die Funktion transform2d(dx, dy, s), die eine Funktion zurückgibt, die eine Closure nutzt, um eine wiederholbare 2D-Verschiebung und -Skalierung eines Punktes durchzuführen.
Das Skalierungsargument s sollte optional sein, mit einem Standardwert von 1.
Die zurückgegebene Funktion sollte einen 2D-Punkt entgegennehmen und dann:
dx und dy verschiebens skalieren# scale all coordinates by the same amount (2) in this example
# your code needs to be able to handle a vector `s`
tf2d <- transform2d(0.5, 0.6, 2)
class(tf2d)
#> [1] "function"
tf2d(c(2, 3))
#> [1] 5.0 7.2
# Accept default for `s`
tf2d <- transform2d(0.5, 0.6)
tf2d(c(2, 3))
#> 2.5 3.6
Die Reihenfolge der Operationen ist wichtig: Erst verschieben, dann skalieren ergibt das korrekte Ergebnis, erst skalieren, dann verschieben nicht. Fachlich ausgedrückt: Die Operationen kommutieren nicht.
Mapping-Daten können auch Höhen enthalten, die verwendet werden, um Schattierungen und Konturen auf dem Bildschirm hinzuzufügen.
Implementiere die Funktion transform3d(dx, dy, dz, s), die eine Funktion zurückgibt.
Das Skalierungsargument s sollte optional sein, mit einem Standardwert von 1.
Die zurückgegebene Funktion sollte einen 3D-Punkt entgegennehmen, ihn um die vordefinierten Werte verschieben und ihn dann um s skalieren.
# using default `s`
tf3d <- transform3d(0.5, 0.6, 0.7)
point <- c(2, 3, 4)
tf3d(point)
#> [1] 2.5 3.6 4.7
# using vector `s`
tf3d <- transform3d(0.5, 0.6, 0.7, c(0.5, 0.6, 0.7))
tf3d(point)
#> [1] 1.25 2.16 3.29
Melde dich bei Exercism an, um R mit 21 Konzepte111 Übungen und echtem menschlichen Mentoring zu lernen und zu meistern, alles kostenlos.