Selbstdefinierte Funktionen in R (UDF = User Defined Functions)

Die Programmiersprache R bietet eine unüberschaubare Vielzahl von Funktionen, die bereits in den Standard-Paketen enthalten sind. Dennoch ist es unerlässlich, selber Funktionen zu implementieren, um selbstgestellte Aufgaben übersichtlich abzuarbeiten. Vorgestellt werden zwar nicht alle, aber die wichtigsten Hilfsmittel und Techniken für selbstdefinierte Funktionen: Die Syntax der Definition einer Funktion, Besonderheiten des Rückgabewertes einer Funktion, die Prüfung der Eingabewerte einer Funktion, das Setzen von default-Werten für die Argumente einer Funktion und einiges über den Mechanismus, wie Funktions-Argumente übergeben werden (call by value).

Einordnung des Artikels

Einführung

In Eigenschaften von Funktionen in R wurde alles Wissenswerte über den Einsatz von Funktionen erklärt:

  • wie eine Funktion aufgerufen wird,
  • die Eigenschaften der Eingabewerte und
  • die Eigenschaften des Rückgabewertes.

In Abbildung 1 ist der typische Aufruf einer Funktion dargestellt:

  • aus einem Skript heraus wird eine Funktion f() aufgerufen,
  • die Eingabewerte x und y werden an die Implementierung der Funktion übergeben,
  • die Implementierung wird abgearbeitet und erzeugt einen Rückgabewert,
  • dieser Rückgabewert wird anstelle des Funktions-Aufrufes im Skript eingesetzt.

Abbildung 1: Aufruf einer Funktion f() mit zwei Eingabewerten x und y und Berechnung des Rückgabewertes.Abbildung 1: Aufruf einer Funktion f() mit zwei Eingabewerten x und y und Berechnung des Rückgabewertes.

Was bisher nicht besprochen wurde und in diesem Kapitel geschieht:

  • Wie erfolgt die Implementierung einer Funktion syntaktisch korrekt? (In Abbildung 1 ist die Implementierung verkürzt und syntaktisch falsch dargestellt.) Dazu gehören:
    • Vereinbarung des Namens der Funktion
    • Eingabewerte
    • Anweisungs-Block
    • Rückgabewert.
  • Wie kann man weitere hilfreiche Funktionalitäten in eine Implementierung integrieren:
    • Prüfung der Eingabewerte
    • Ausgabe von Warn-Hinweisen oder Fehlermeldungen
    • vorzeitiges Verlassen der Funktion, falls ungeeinete Eingabewerte vorliegen.

Erläutert werden all diese Konzepte meist an einem einfachen Beispiel: Die Berechnung eines gewichteten Mittelwertes, die an die Berechnung eines Schwerpunktes angelehnt ist. Dazu ist ein Vektor x mit Koordinaten gegeben sowie (nicht-negative) Gewichte m. Die Gewichte m bilden einen Vektor, der dieselbe Länge wie x haben muss. Die Schwerpunkts-Koordinate könnte man dann einfach mit sum(x * m) / sum (m) berechnen. Dies soll hier mit einer Funktion weightedMean(x, m) geschehen.

Zuletzt noch ein Hinweis zum Begriff selbstdefinierte Funktion: Damit sind Funktionen gemeint, die vom Nutzer selbst implementiert werden und daher von Funktionen zu unterscheiden sind, die bereits in den R-Paketen enthalten sind. In der Literatur werden sie meist als benutzer-definierte Funktionen bezeichnet (im Englischen als user defined functions, abgekürzt UDF).

Die Syntax der Definition einer Funktion

Die Syntax erklärt an einem Beispiel

Zur Definition einer Funktion gehören folgende Elemente:

  1. Es muss ein Name für die Funktion vereinbart werden.
  2. Die Eingabewerte müssen festgelegt werden (aber nicht ihre Datentypen).
  3. Soll die Funktion einen Rückgabewert berechnen, muss die Implementierung ein return-statement besitzen.
  4. Die Definition der Funktion erfolgt dann durch den Aufruf von function():
  • diese Funktion erhält die Argument-Liste der selbstdefinierten Funktion als Eingabewert,
  • in geschweiften Klammern stehen der entsprechende Anweisungs-Block,
  • der eventuell das return-statement mit return(value) enthält.

Die Aufgabe, eine Funktion zu definieren, wird also von der Funktion function() übernommen, die zwei Arten von Eingabewerten besitzt:

  1. In runden Klammern die Liste der Eingabewerte der zu definierenden Funktion.
  2. In geschweiften Klammern die Implementierung der Funktion.

Die Definition der oben kurz vorgestellten Funktion weightedMean() könnte wie folgt aussehen:

weightedMean <- function(x, m){
  y <- sum(x * m) / sum(m)
  return(y)
}

weightedMean(x = c(0, 1), m = c(3, 1))
# [1] 0.25
  1. Vereinbarung des Namens der Funktion: Dies geschieht in Zeile 1, indem weightedMean eine Funktion zugeordnet wird (Aufruf der Funktion function()). Man sollte bei der Vereinbarung des Namens darauf achten, dass der Name nicht in den Standard-Paketen (oder anderen gerade benutzten Paketen) verwendet wird. Man kann diese Namenskonflikte zwar durch Namensräume (namespace) vermeiden, sie erschweren aber das Lesen des Quelltextes. Ob ein Name bereits verwendet wird, lässt sich leicht mit Hilfe der Entwicklungsumgebung feststellen: man beginnt den gewünschten Namen einzutippen und sieht alle Variablen und Funktionen, die mit diesen Buchstaben beginnen.
  2. Die Eingabewerte, hier x und m für die Koordinaten und Gewichte, werden der Funktion function() übergeben (Zeile 1 im rechten Teil der Zuweisung). Diese Variablen können dann im Anweisungs-Block verwendet werden. Wer schon mit einer streng typisierten Programmiersprache gearbeitet hat, wird sich hier wundern, wo die Datentypen der Eingabewerte festgelegt sind. Sie werden nicht festgelegt. Dass es sich um numerische Vektoren handelt, wurde bisher nur bei der verbalen Beschreibung der Funktion weightedMean() verwendet; im Quelltext wird dies nicht ausdrücklich vom Benutzer der Funktion gefordert. Man kann später in die Funktion Warn-Hinweise und derglichen einbauen, um einen Missbrauch der Funktion zu vermeiden.
  3. Der Anweisungs-Block befindet sich innerhalb der geschweiften Klammern (Zeile 1 bis 4). Hier stehen alle Berechnungen, die die Funktion weightedMean() ausmachen. Man erkennt insbesondere (in Zeile 2), dass hier die Eingabewerte x und m verwendet werden; und das obwohl man nicht weiß, welche Werte vom Anwender der Funktion übergeben werden. Da die Funktion keinerlei Prüfung der Eingabewerte vorsieht, muss man sich hier auf den R-Interpreter verlassen, wenn die Berechnungen mit den Eingabewerten nicht durchgeführt werden können.
  4. Das return-statement steht in Zeile 3: hier wird die Funktion return() aufgerufen, die als Eingabewert den Rückgabewert von weightedMean() erhält. Man kann natürlich Zeile 2 und 3 zu einer Anweisung return(sum(x * m) / sum(m)) zusammenfassen; dies wurde oben unterlassen, um das return-statement besser hervorzuheben.
  5. Der Aufruf der Funktion weightedMean() ist in Zeile 4 zu sehen: Als Eingabewerte werden zwei Vektoren identischer Länge eingesetzt. Die Berechnung des Schwerpunktes ist leicht nachzuvollziehen:

(3 · 0 + 1 · 1) / (3 + 1) = 0.25.

Aufgabe: In der Implementierung von weightedMean() wird nicht geprüft, ob die beiden Vektoren x und m gleiche Länge haben. Durch den recycling-Mechanismus wird dennoch ein Rückgabewert berechnet. Überlegen Sie sich anhand einiger Beispiele für Eingabewerte, ob dieser Rückgabewert sinnvoll ist oder ob man dafür sorgen sollte, dass Eingabe-Vektoren ungleicher Länge nicht auftreten können.

Der Name der Funktion

Für den Namen einer Funktion gelten dieselben Regeln wie für den Namen einer Variable:

  • Erlaubte Zeichen sind Kleinbuchstaben (a - z), Großbuchstaben (A - Z) und die beiden Sonderzeichen . und _ , die aber nicht am Anfang des Namens stehen sollten.
  • Der Name darf nicht mit einem Schlüsselwort übereinstimmen (wie while oder for).

Eine Besonderheit sollte man bei der Namenswahl beachten: Der Punkt sollte nur mit Bedacht eingesetzt werden. Die Erklärung hierfür kann nur unvollständig erfolgen, da sie ein Verständnis der objekt-orientierten Programmierung voraussetzt.

Früher wurde schon das Beispiel angeführt, in dem alle Funktionen ausgegeben wurden, die mit mean() "verwandt" sind; dies geschieht mit Hilfe von methods():

methods(mean)
# [1] mean.Date     mean.default  mean.difftime mean.POSIXct  mean.POSIXlt 
# see '?methods' for accessing help and source code

Man erkennt an der Ausgabe, dass es mehrere Versionen der Funktion mean() gibt, die unterschiedliche Suffixe besitzen, zum Beispiel mean.Date oder mean.default. Dahinter verbergen sich verschiedene Implementierungen der Funktion mean(), wobei das Suffix angibt, welcher Klasse die Funktion zugeordnet ist. Wird zum Beispiel die Funktion mean() mit einem Date-Objekt aufgerufen, wird vom R-Interpreter die Funktion mean.Date() angesprochen. Wird mean() dagegen mit einem Objekt einer Klasse aufgerufen, das nicht in der Liste oben vorkommt, wird die Funktion mean.default() angesprochen. Funktionen wie mean.Date() oder mean.default() werden aber in der Dokumentation nicht eigens angeführt, so dass man als Anwender oft von ihrer Existenz nichts weiß.

Aus diesem Grund kann es passieren, dass die Verwendung von . in einem Funktions-Namen eine Namensgleichheit mit einer bereits vorhandenen Funktion herstellt, was zu Verwirrung führen kann.

Oben wurde als Name für die implementierende Funktion weightedMean() vorgeschlagen; im Paket stats gibt es bereits eine Funktion namens weighted.mean(), wie man leicht durch Eintippen der Anfangsbuchstaben in die Entwicklungsumgebung feststellen kann. Dagegen gibt es weightedMean() nicht in den Standard-Paketen – der Name ist also "ungefährlich"

Besonderheiten über den Rückgabewert einer Funktion

Mehrere return-statements

Im Beispiel oben wurde ein einziges return-statement in der Implementierung der Funktion weightedMean() verwendet. Man könnte dies in folgendem Sinn falsch verstehen: Da eine Funktion einen Rückgabewert hat, muss es genau ein return-statement geben, das die Implementierung der Funktion beendet und den Rückgabewert an die aufrufende Programmzeile schickt. Das ist natürlich falsch.

Da man je nach Eingabewert womöglich andere Rückgabewerte berechnen möchte, kann es auch mehrere return-statements geben. Das folgende Beispiel zeigt eine Implementierung von weightedMean(), die unsinnige Eingabewerte abfängt. Das gezeigte Skript sollte nicht nachgeahmt werden, da es bessere Methoden gibt, um die Eingabewerte zu prüfen (siehe unten).

weightedMean <- function(x, m){
  if( !is.numeric(x) || !is.numeric(m) ) return(NA_real_)
  
  y <- sum(x * m) / sum(m)
  return(y)
}

weightedMean(x = "a", m = 1)
# [1] NA
weightedMean(x = 1, m = "a")
# [1] NA
weightedMean(x = c(0, 1), m = c(3, 1))
# [1] 0.25

Zeile 2: Werden für x oder m Werte eingegeben, die nicht den Modus numeric haben, wird NA zurückgegeben, andernfalls wird der gewichtete Mittelwert (wie in der ursprünglichen Implementierung) berechnet. Genauer: Ist die Bedingung innerhalb von if() erfüllt, wird das return-statement in Zeile 2 aufgerufen und die Funktion sofort verlassen; die folgenden Anweisungen werden nicht ausgeführt. Ist die Bedingung nicht erfüllt, wird das return-statement in Zeile 5 ausgeführt.

Zeile 8 bis 11: Die beiden Aufrufe der Funktion zeigen, dass jetzt NA zurückgegeben wird.

Zeile 12 und 13: Des eigentlich erwünschte Verhalten von weightedMean() bleibt erhalten.

Unterschiedliche Datentypen des Rückgabewertes

Oben wurde schon gesagt, dass in R – anders als in einer streng typisierten Programmiersprache – unterschiedliche Datentypen für den Rückgabewert erlaubt sind. Auch dies sollte man nur mit Bedacht einsetzen, da der Anwender einer Funktion oft den Rückgabewert in einer Variable abspeichern und damit weiterrechnen möchte. Ist der Datentyp der Variable nicht eindeutig, führt das nur zu Problemen.

Das folgende Skript zeigt eine Version von weightedMean(), die zwar syntaktisch korrekt ist, aber nicht nachgeahmt werden sollte:

weightedMean <- function(x, m){
  if( !is.numeric(x) || !is.numeric(m) ) return("Keine Berechnung möglich")

  return(sum(x * m) / sum(m))
}

weightedMean(x = c(0, 1), m = c(3, 1))
# [1] 0.25
weightedMean(x = "a", m = 1)
# [1] "Keine Berechnung möglich"

In Zeile 2 wird jetzt eine Zeichenkette zurückgegeben. Weiter unten wird gezeigt, wie man diesen Warn-Hinweis geschickter ausgeben kann.

Zuvor soll noch eine andere Variante des obigen Skripts gezeigt werden – die ebenfalls nicht nachahmenswert ist:

weightedMean <- function(x, m){
  if( !is.numeric(x) || !is.numeric(m) ){
    cat("Keine Berechnung möglich \n")
    return()
  } 
  
  return(sum(x * m) / sum(m))
}

weightedMean(x = c(0, 1), m = c(3, 1))
# [1] 0.25
weightedMean(x = "a", m = 1)
# Keine Berechnung möglich 
# NULL

Jetzt erfolgt der Warn-Hinweis als reine Konsolenausgabe mit Hilfe von cat() (Zeile 3). Es wird kein Wert zurückgegeben – die Funktion return() wird ohne Argument aufgerufen (Zeile 4).

Die unerwünschte Verwendung von weightedMean() in Zeile 12 erzeugt die Konsolenausgabe mit dem Warn-Hinweis (Zeile 13) und NULL als Rückgabewert (Zeile 14).

Komplexe Objekte als Rückgabewert

Die Funktion weightedMean() berechnet eine Zahl und gibt sie anschließend zurück. In den meisten Fällen reichen bei selbstdefinierten Funktionen die einfachen Datentypen wie Vektoren, Matrizen oder Dataframes für den Rückgabewert aus. Manchmal möchte man aber mehrere Objekte zurückgeben, die unterschiedliche Datentypen haben und sich nicht in einen Vektor verpacken lassen. Dann sollte man diese Objekte in eine Liste verpacken. Wer mit objekt-orientierter Programmierung vertraut ist, kann auch selbstdefinierte Klassen dafür verwenden (die allerdings auf Listen beruhen).

Das folgende Skript verpackt in den Rückgabewert:

  • die Anzahl der Gewichte,
  • den gewichteten Mittelwert.

(Auf Prüfungen der Eingabewerte wird hier verzichtet.)

weightedMean <- function(x, m){
  return( list(mean = sum(x * m) / sum(m), numberOfWeigts = length(m)) )
}

str( weightedMean(x = c(1,2), m = c(1, 3)) )
# List of 2
# $ mean          : num 1.75
# $ numberOfWeigts: int 2

In Zeile 2 wird eine Liste gebildet, für die Namen gesetzt werden; Rückgabewert ist diese Liste.

Abgekürztes return-statement

Üblicherweise steht am Ende der Implementierung einer Funktion ein return-statement. Hier kann man die Angabe von return() weglassen und nur das Objekt angeben, das zurückgegeben wird.

Die vorletzte Implementierung von weightedMean() kann daher auch lauten:

weightedMean <- function(x, m){
  if( !is.numeric(x) || !is.numeric(m) ) return(NA_real_)
  
  sum(x * m) / sum(m)
}

In Zeile 2 darf return() nicht weggelassen werden.

Zeile 4: Die eigentliche Berechnung und das return-statement sind jetzt in einer Anweisung enthalten.

Aufgabe: Testen Sie, welchen Fehler Sie erhalten wenn Sie in Zeile 2 schreiben: if( !is.numeric(x) || !is.numeric(m) ) NA_real_

Lösung:

Schreibt man im if-Zweig nur NA_real_ anstelle von return(NA_real_) , wird dies nicht als return-statement interpretiert. Bei fehlerfreien Eingaben wird der if-Zweig verlassen und der richtige Rückgabewert berechnet. Dagegen wird bei Eingaben, die im if-Zweig abgefangen werden sollten, auch der if-Zweig (ohne return-statement) verlassen und sum(x * m) / sum(m) berechnet, was aber mit nicht-numerischen Werten zu einem Fehler führt.

Tip: Schreiben Sie das return-statement immer ausdrücklich in den Quelltext: er wird leichter lesbar und es besteht keine Verwechslungsgefahr mit print()-Anweisungen.

Als Gegenargument könnte man anführen, dass das (überflüssige) return-statement unnötig Rechenzeit beansprucht. Bei einem Funktions-Aufruf wird dies kaum auffallen; erst wenn der Aufruf einer sehr kurzen Funktion in jedem Durchlauf einer umfangreichen Schleife vorkommt. Dann sollte man die abgekürzte Version des return-statements einsetzen.

Ein weiteres Argument, warum man das return-statement stets angeben sollte, betrifft Funktionen ohne Rückgabewert. Diese werden meist eingesetzt, um Ausgaben zu erzeugen, wenn mit den berechneten Werten nicht unmittelbar weitergerechnet werden soll.

Eine etwas merkwürdige Implementierung der Betragsfunktion zeigt das folgende Skript. Auf den ersten Blick handelt es sich um eine Funktion ohne Rückgabewert, da

  • im if-Zweig zwar das Vorzeichen von x umgedreht wird, ansonsten aber nichts mit x geschieht und
  • im else-Zweig die Anzahl der Stellen gesetzt wird und x auf der Konsole ausgegeben – auch hier ist kein return-statement.
absolut <- function(x, n){
  if(x < 0){
    x <- -x
  } else {
    options(digits = n)
    cat(x)
  }
}

Der Einsatz der Funktion zeigt, dass die Aussage über den if-Zweig der Funktion falsch ist:

y <- absolut(x = -3.1416, n = 3)
y
# [1] 3.1416

absolut(x = 3.1416, n = 3)
# 3.14

Das Verhalten des else-Zweiges (Zeile 5) ist wie erwartet: x wird mit drei gültigen Stellen ausgegeben.

Aber an der [1] in Zeile 3 erkennt man, dass -x als Rückgabewert erzeugt wird, wenn der if-Zweig durchlaufen wird.

Der Grund für dieses unerwartete Verhalten liegt am abgekürzten return-statement: Die letzte Anweisung einer Funktion ist zugleich das return-statement. Im if-Zweig wird daher -x zurückgegeben, im else-Zweig wird kein Rückgabewert erzeugt, da cat() keinen Rückgabewert hat.

Tip: Falls Sie eine Funktion ohne Rückgabewert erzeugen wollen, geben Sie ausdrücklich return() an. So können Sie derartige Zweifelsfälle vermeiden und man erkennt im Quelltext sofort, wo die Abarbeitung der Funktion endet und dass kein Rückgabewert erzeugt wird.

Prüfung der Eingabewerte

Am Thema "Prüfung der Eingabewerte" kommt man nicht vorbei, wenn man selber Funktionen definiert. Es wird bei jeder Funktion Eingabewerte geben, die zu einem unerwünschten Verhalten der Implementierung führen. Und da man vom Anwender der Funktion nicht erwarten sollte, dass er die Dokumentation der Funktion vollständig liest – sofern sie überhaupt existiert –, sollte man immer davon ausgehen, dass die Funktion anders als vorgesehen eingesetzt wird. Für diesen Fall muss man sich überlegen, wie die Funktion reagieren soll – und dazu muss man die Möglichkeiten kennen, die R bereithält, um die wichtigsten zu nennen:

  • Ausgabe eines Warn-Hinweises,
  • Ausgabe einer Fehlermeldung,
  • vorzeitiger Abbruch der Funktion.

Um die Prüfung der Eingabewerte umzusetzen, muss man sich klarmachen, welche Eingabewerte zu Fehlern führen können und von welcher Art die Fehler sind, etwa:

  • Die Eingabewerte haben Datentypen, mit denen die vorgesehenen Berechnungen nicht durchgeführt werden können.
  • Division durch null.
  • Unsinnige Verknüpfungen von Vektoren verursacht durch den recycling-Mechanismus und so weiter.
  • Die Eingabewerte enthalten NA-Werte und können nach deren Beseitigung sinnvoll verarbeitet werden.

Am Beispiel der Funktion weightedMean() soll diese Analyse (teilweise) durchgespielt werden.

Prüfung der Eingabewerte für weightedMean()

Der Mittelwert eines Vektors x wird dadurch berechnet, dass man die Komponenten von x aufaddiert und durch ihre Anzahl teilt. Bei dem gewichteten Mittelwert wird jede Komponente xi von x mit einem Gewicht mi belegt; addiert werden alle Produkte xi · mi und diese Summe wird durch das Gesamtgewicht geteilt. Das Gesamtgewicht ist die Summe aller mi.

Die Gewichte dürfen nicht negativ sein, denn sonst könnte das Gesamtgewicht gleich null sein und man müsste durch null dividieren. Auch eine Interpretation der (normierten) Gewichte als Wahrscheinlichkeiten wäre dann nicht möglich.

Für die Berechnung des gewichteten Mittelwertes müssen die Vektoren gleiche Länge haben; allerdings kann man den Fall, dass nur ein Gewicht gegeben ist, auch gelten lassen: er steht dafür, dass alle Gewichte identisch sind und man kann den Mittelwert ohne Gewichtung berechnen.

Sowohl in x als auch in m können NA-Werte auftreten; die Komplikationen, die dadurch entstehen, sollen hier aber nicht untersucht werden. Im Folgenden wird davon ausgegangen, dass weder in x noch in m NA-Werte enthalten sind.

Die Berechnung des gewichteten Mittelwertes wurde oben schon angegeben:

weightedMean <- function(x, m){
  return( sum(x * m) / sum(m) )
}

Welche Fehler können bei dieser Implementierung auftreten? Oder anders herum formuliert: welche Forderungen muss man an x und m stellen, damit keine Fehler auftreten?

  1. Beide Vektoren müssen den Modus numeric besitzen.
  2. Die Länge von x und von m müssen übereinstimmen oder m hat die Länge 1 (alle Gewichte sind identisch, allerdings führt jetzt die Formel sum(x * m) / sum(m) zum falschen Ergebnis).
  3. Nicht alle Komponenten von m dürfen gleich null sein; dass eine oder mehrere Komponenten gleich null sind, ist erlaubt.
  4. Keine Komponente von m darf negativ sein.

Falls gegen eine dieser Bedingungen verstoßen wird, muss die Implementierung eine geeignete Reaktion bereithalten. Wie soll diese Reaktion aussehen?

  1. Abbruch der Funktion: aus nicht-numerischen Werten kann man keinen Mittelwert berechnen.
  2. Abbruch der Funktion: der recycling-Mechanismus würde zu einem unsinnigen Ergebnis führen. Ist die Länge von m gleich 1, kann der gewöhnliche Mittelwert von x berechnet werden; hier ist ein Warn-Hinweis sinnvoll.
  3. Abbruch: man kann keinen gewichteten Mittelwert berechnen.
  4. Abbruch: negative Gewichte haben keine Bedeutung.

In allen Fällen sollte der Anwender geeignet informiert werden, dass die Eingabewerte die entsprechende Bedingung verletzen.

Bei der Implementierung der Abbruch-Bedingungen sollte man auf die richtige Reihenfolge achten, damit keine unnötigen Anweisungen abgearbeitet werden. Sind zum Beispiel Eingabewerte nicht vom Modus numeric, müssen die anderen Prüfungen nicht mehr durchgeführt werden. Dagegen ist bei der zweiten bis vierten Bedingung oben schwer, eine sinnvolle Reihenfolge anzugeben; zu empfehlen ist dann, mit dem wahrscheinlichsten Fall zu beginnen. (Hier ist eine Entscheidung schwer möglich.)

In den folgenden Unterabschnitten wird gezeigt, wie man derartige Prüfungen und die Ausgabe der Warn-Hinweise realisieren kann.

Die Funktion stopifnot()

Oben wurde in einer Implementierung für weightedMean() eine eher umständliche Konstruktion vorgeschlagen, wie man eine unsinnige Eingabe abfangen kann:

weightedMean <- function(x, m){
if( !is.numeric(x) || !is.numeric(m) ) return("Keine Berechnung möglich")
 
  return( sum(x * m) / sum(m) )
}

Falls einer der Eingabewerte x oder m nicht vom Modus numeric ist, wird die Abarbeitung der Funktion beendet und als Rückgabewert die Zeichenkette gesetzt.

Man kann diese Logik auch viel einfacher implementieren; dazu gibt es die Funktion stopifnot(). Sie hat als Eingabewert eine Bedingung; ist diese nicht erfüllt, wird:

  • eine Fehlermeldung ausgegeben,
  • die Abarbeitung der Funktion abgebrochen.

Man kann sich leicht überlegen, dass diese negative Sichtweise weniger Aufwand verursacht als die umgekehrte Sichtweise: So wird im Fall der Funktion weightedMean() die Bearbeitung abgebrochen, wenn keine numerischen Vektoren vorliegen; es wäre viel aufwendiger, alle Möglichkeiten anzugeben, die nicht eintreten dürfen.

Obige Implementierung von weightedMean() kann dann einfacher geschrieben werden:

weightedMean <- function(x, m){
  stopifnot( is.numeric(x) && is.numeric(m) )
  
  return( sum(x * m) / sum(m) )
}

weightedMean(x = c(0, 1), m = c(3, 1))
# [1] 0.25

weightedMean(x = "a", m = 1)
# Error: is.numeric(x) && is.numeric(m) is not TRUE 

weightedMean(x = 1, m = "a")
# Error: is.numeric(x) && is.numeric(m) is not TRUE

Die Funktion stopifnot() besitzt als Eingabewert ... : damit können beliebig viele Bedingungen überprüft werden und sie müssen nicht durch das logische Und && verknüpft werden, wodurch der Quelltext vereinfacht werden kann:

weightedMean <- function(x, m){
  stopifnot( is.numeric(x), is.numeric(m) )

  return( sum(x * m) / sum(m) )
}

weightedMean(x = c(0, 1), m = c(3, 1))
# [1] 0.25

weightedMean(x = "a", m = 1)
# Error: is.numeric(x) is not TRUE  

weightedMean(x = 1, m = "a")
# Error: is.numeric(m) is not TRUE

Vergleicht man die Ausgaben mit dem vorhergehenden Skript, erkennt man den Vorteil der Verwendung von stopifnot(...) : Wurden die Bedingungen mit dem logischen Und verknüpft, besagte die Fehlermeldung nur, dass die Bedingung insgesamt verletzt ist; jetzt erkennt man, welche Bedingung verletzt ist.

Aufgabe: Implementieren Sie die weiteren oben genannten Prüfungen der Eingabewerte und testen diese.

Die Funktionen message() und warning()

Die Funktion stopifnot(...) hat folgende Eigenschaften:

  • sie überprüft alle Bedingungen aus ... ,
  • ist eine Bedingung nicht erfüllt, wird die Abarbeitung der Funktion abgebrochen (weitere Bedingungen werden dann nicht mehr geprüft),
  • es wird automatisch eine Fehlermeldung erzeugt, aus der abzulesen ist, welche Bedingung zuerst nicht erfüllt wurde.

Der Nachteil von stopifnot() ist, dass man die Fehlermeldung nicht anpassen kann. Die Funktionen message(...) und warning(...) erlauben, eine selbstdefinierte Nachricht oder einen selbstdefinierten Warn-Hinweis auszugeben, der aus mehreren Teilen zusammengesetzt werden kann. Die Abarbeitung der Funktion wird nach dem Aufruf von message() beziehungsweise warning() fortgesetzt; zum Abbruch der Funktion muss man stop() einsetzen (siehe unten).

Das folgende Skript implementiert wieder weightedMean(); im Fall, dass nur ein Gewicht aber mehrere x-Werte vorliegen, wird ein Warn-Hinweis ausgegeben:

weightedMean <- function(x, m){
  stopifnot( is.numeric(x), is.numeric(m) )
  
  if( (length(m) == 1) && (length(x) > 1) ){
    warning("Es liegt nur ein Gewicht vor, obwohl ", length(x), " x-Werte gegeben sind!")
    return(mean(x))
  }
  
  return( sum(x * m) / sum(m) )
}

weightedMean(x = c(1, 2), m = 2)
# [1] 1.5
# Warning message:
#   In weightedMean(x = c(1, 2), m = 2) :
#   Es liegt nur ein Gewicht vor, obwohl 2 x-Werte gegeben sind!

Zeile 4: In der if-Abfrage wird der Fall definiert, in dem nur ein m-Wert aber mehrere x-Werte vorliegen.

Zeile 5: Der Funktion warning(...) können mehrere Zeichenketten übergeben werden, die zu einem einzigen Warn-Hinweis zusammengefasst werde; er enthält:

  • den Text "Warning message:",
  • die Angabe, in welcher Funktion die Warnung erzeugt wurde,
  • den aus ... zusammengesetzten selbstdefinierten Warn-Hinweis.

Man erkennt an der Ausgabe in Zeile 16, dass in ... auch ein Ausdruck wie length(x) eingesetzt werden kann. Er wird ausgewertet (ergibt hier 2) und als Zeichenkette weitergegeben.

Zeile 6 und 13: Man erkennt, dass die Abarbeitung der Funktion durch warning() nicht abgebrochen wird: Der Mittelwert wird in Zeile 6 berechnet und zurückgegeben; er wird in Zeile 13 ausgegeben.

Das Skript wird mit message() anstelle von warning() in Zeile 5 implementiert:

weightedMean <- function(x, m){
  stopifnot( is.numeric(x), is.numeric(m) )
  
  if( (length(m) == 1) && (length(x) > 1) ){
    message("Es liegt nur ein Gewicht vor, obwohl ", length(x), " x-Werte gegeben sind!")
    return(mean(x))
  } else {
    if( !identical(length(x), length(m)) ){
      stop("Die Vektoren x und m haben unterschiedliche Längen: ", length(x), " und ", length(m))
    }
  }
  
  sum(x * m) / sum(m)
}
weightedMean(x = c(1, 2), m = 2)
# Es liegt nur ein Gewicht vor, obwohl 2 x-Werte gegeben sind!
# [1] 1.5

Im Unterschied zur Implementierung mit warning() wird jetzt zuerst die Nachricht ausgegeben, dann der Rückgabewert.

Oft ist es eine Geschmacksfrage, ob man message() oder warning() einsetzt; die Konvention ist:

  • message() steht für reine Benachrichtigungen, etwa Diagnose-Ergebnisse, die auch beim Testen einer Funktion gut eingesetzt werden können,
  • warning() ist für Warnungen und Fehler vorbehalten, also immer wenn im Ablauf der Abarbeitung einer Funktion unerwartete Ereignisse eintreten.

Die Funktion stop()

Die Funktionen message() und warning() können nur Benachrichtigungen oder Warn-Hinweise ausgeben; die Abarbeitung der Funktion, in der sie eingebaut ist, wird fortgesetzt. Die Funktion stop(...) erlaubt es:

  • Das Argument ... wie bei warning() einzusetzen, das heißt alle Eingabewerte werden zu einer einzigen Zeichenkette zusammengesetzt.
  • Die Abarbeitung der Funktion wird abgebrochen.

Das folgende Skript zeigt den Einsatz von stop() in der Implementierung von weightedMean():

weightedMean <- function(x, m){
  stopifnot( is.numeric(x), is.numeric(m) )
  
  if( (length(m) == 1) && (length(x) > 1) ){
    warning("Es liegt nur ein Gewicht vor, obwohl ", length(x), " x-Werte gegeben sind!")
    return(mean(x))
  } else {
    if( !identical(length(x), length(m)) ){
      stop("Die Vektoren x und m haben unterschiedliche Längen: ", length(x), " und ", length(m))
    }
  }
  
  return( sum(x * m) / sum(m) )
}

weightedMean(x = c(1, 2), m = (1:3))
# Error in weightedMean(x = c(1, 2), m = (1:3)) : 
#   Die Vektoren x und m haben unterschiedliche Längen: 2 und 3

Neu im Vergleich zur letzten Implementierung von weightedMean() ist, dass der if-Zweig (Zeile 4) durch eine else-Zweig ergänzt wurde (Zeile 7 bis 11).

An der Konsolen-Ausgabe Zeile 17 und 18 erkennt man, wie die vier Argumente in stop() (Zeile 9) zu einem Warn-Hinweis zusammengesetzt werden. Vor dem selbstdefinierten Warn-Hinweis wird wieder angegeben, wo der Aufruf von stop() stattgefunden hat (Zeile 17).

Aufgabe:

Implementieren Sie die noch fehlenden Prüfungen der Eingabewerte von weightedMean(). Entscheiden Sie, welche der Funktionen stopifnot(), stop() und warning() dabei eingesetzt werden soll.

Lösungsvorschlag:

Es fehlen noch zwei Prüfungen der Eingabewerte:

  • Wenn alle Gewichte m gleich null sind, muss die Funktion vorzeitig verlassen werden (sonst: Division durch null).
  • Ebenso, wenn eine Komponente von m negativ ist.
weightedMean <- function(x, m){
  stopifnot( is.numeric(x), is.numeric(m) )
  
  if( (length(m) == 1) && (length(x) > 1) ){
    warning("Es liegt nur ein Gewicht vor, obwohl ", length(x), " x-Werte gegeben sind!")
    return(mean(x))
  } else {
    if( !identical(length(x), length(m)) ){
      stop("Die Vektoren x und m haben unterschiedliche Längen: ", length(x), " und ", length(m))
    }
  }
  
  stopifnot(any(m != 0))
  
  stopifnot(all(m >= 0))
  
  return( sum(x * m) / sum(m) )
}

weightedMean(x = c(1, 2), m = rep(x = 0, times = 2))
# Error: any(m != 0) is not TRUE

weightedMean(x = c(1, 2), m = c(1, 0))
# [1] 1

weightedMean(x = c(1, 2), m = c(1, -2))
# Error: all(m >= 0) is not TRUE

Zeile 13 und 15 implementieren die fehlenden Bedingungs-Prüfungen, die in Zeile 20 bis 27 getestet werden. Eine Prüfung auf NA-Werte in x und m wird hier nicht eingegangen.

Die Funktion missing()

Funktionen haben bisweilen lange Listen von Eingabewerten. Es kann dann dem Anwender leicht passieren, dass ein Argument nicht gesetzt wird. Mit Hilfe der Funktion missing(), die einen logischen Wert zurückgibt, kann man auf ein fehlendes Argument reagieren.

Um die Funktion angemessen einzusetzen, sollte man wissen, wie der R-Interpreter realisiert, wenn ein Argument fehlt. Dazu dient das folgende Skript:

f <- function(x, y){
  return(x)
}

f(x = 1, y = 1)
# [1] 1

Die Funktion f() erwartet zwar zwei Eingabewerte, in der Implementierung wird aber nur x verwendet. Weder von der Entwicklungsumgebung noch vom R-Interpreter (beim Ausführen des Skriptes) wird eine Fehlermeldung erzeugt.

Was passiert, wenn f() mit nur einem Argument aufgerufen wird? Die Reaktion hängt davon ab, welches Argument fehlt:

f <- function(x, y){
  return(x)
}

f(x = 1)
# argument 'y' is missing with no default
# [1] 1

f(y = 1)
# Error in f(y = 1) : argument "x" is missing, with no default

Zeile 5: Nur die Entwicklungsumgebung zeigt an, dass der Funktions-Aufruf nicht korrekt ist (siehe Zeile 6). Da das Argument y in der Abarbeitung der Implementierung nicht benötigt wird, gibt es keine Fehlermeldung bei der Ausführung von Zeile 5 und der Rückgabewert wird korrekt berechnet (Zeile 7).

Zeile 9: Dagegen kann der Funktions-Aufruf nur mit y nicht abgearbeitet werden; jetzt wird zusätzlich zur Fehlermeldung der Entwicklungsumgebung (nicht nochmal gezeigt) ein Fehler vom R-Interpreter ausgegeben (Zeile 10)

Die Funktion missing() bietet jetzt die Möglichkeit, zusätzlich zu den besprochenen Reaktionen in die Implementierung einer Funktion eine geeignete Ausgabe einzubauen. Das folgende Skript zeigt eine Abwandlung von f():

f <- function(x, y){
  if(missing(x)) stop("Das Argument x fehlt!")
  if(missing(y)) message("Na und?!")

  return(x)
}

f(x = 1)
# Na und?!
# [1] 1

f(y = 1)
# Error in f(y = 1) : Das Argument x fehlt!

Neu sind Zeile 2 und 3: Wenn x fehlt, wird die Abarbeitung der Funktion mit stop() angehalten und eine Fehlermeldung ausgegeben. Wenn y fehlt, wird eine Benachrichtigung mit message() erzeugt.

Zeile 8: Es wird f() ohne das Argument y aufgerufen. Die Entwicklungsumgebung zeigt wieder eine Fehlermeldung (nicht gezeigt). Der if-Zweig aus Zeile 3 wird ausgeführt und die Nachricht aus Zeile 9 erscheint; der Rückgabewert wird unabhängig davon korrekt berechnet (Zeile 10).

Zeile 12: Jetzt fehlt das unerlässliche Argument x; es wird der if-Zweig in Zeile 2 durchlaufen und die dort definierte Fehlermeldung ausgegeben. Die Fehlermeldung, die sonst vom R-Interpreter erzeugt wird, wird jetzt nicht angezeigt (siehe vorheriges Skript).

Weitere Funktionen zur Prüfung der Eingabewerte

Die bisher vorgestellten Funktionen zum Überprüfen von Eingabewerten, sind die am häufigsten benötigten Funktionen, aber bei Weitem noch alle Funktionen, die in den Standard-Paketen enthlten sind. Weitere Funktionen zur Fehlerbehandlung finden sich unter Condition Handling and Recovery (zu finden unter ?coditions im Paket base) und unter Asserting Error Conditions (zu finden unter ?assertCondition im Paket tools).

Eingabewerte mit default-Werten

Setzen eines default-Wertes für einen Eingabewert einer Funktion

Es wurden schon zahlreiche Funktionen besprochen, die für einen Eingabewert einen default-Wert gesetzt haben. Ein Paradebeispiel ist die Funktion mean(x, trim = 0, na.rm = FALSE, ...) :

  • Das Argument trim sorgt dafür, dass die größten und kleinsten Werte von x beseitigt werden; dazu wird ein Bruchteil (zwischen 0 und 1/2) angegeben, wie viele Werte an den "Rändern" von x beseitigt werden. Wird das Argument trim nicht gesetzt, wird der default-Wert 0 verwendet und es werden auch keine Werte aus x entfernt.
  • Das Argument na.rm gibt, ob NA-Werte aus x entfernt werden sollen; wird na.rm nicht gesetzt, wird der default-Wert FALSE verwendet.

Oft möchte man für selbstdefinierte Funktionen ebenfalls default-Werte für einige Argumente anbieten, da sich damit die Funktions-Aufrufe vereinfachen lassen.

Im folgenden Beispiel wird weightedMean() so implementiert, dass der default-Wert m = 1 gesetzt wird (der gewichtete Mittelwert ist dann der gewöhnliche Mittelwert); es wird die im letzten Skript gegebene Implementierung von weightedMean() verwendet und entsprechend erweitert:

weightedMean <- function(x, m = 1){
  stopifnot( is.numeric(x), is.numeric(m) )
  
  if( (length(m) == 1) && (length(x) > 1) ){
    warning("Es liegt nur ein Gewicht vor, obwohl ", length(x), " x-Werte gegeben sind!")
    return(mean(x))
  } else {
    if( !identical(length(x), length(m)) ){
      stop("Die Vektoren x und m haben unterschiedliche Längen: ", length(x), " und ", length(m))
    }
  }
  
  stopifnot(any(m != 0))
  
  stopifnot(all(m >= 0))
  
  return( sum(x * m) / sum(m) )
}

weightedMean(x = (1:3))
# [1] 2
# Warning message:
#   In weightedMean(x = (1:3)) :
#   Es liegt nur ein Gewicht vor, obwohl 3 x-Werte gegeben sind!

Die einzige Änderung gegenüber der letzten Implementierung von weightedMean() ist in Zeile 1 enthalten: im Argument von function() steht jetzt m = 1 anstelle von m .

Die Auswirkung ist im Test in Zeile 20 zu sehen: Die Funktion weightedMean() wird jetzt ohne das Argument m aufgerufen. Da im Test x eine Länge ungleich 1 hat, wird der if-Zweig in Zeile 4 betreten und der Warn-Hinweis aus Zeile 5 erzeugt; als Mittelwert wird der gewöhnliche Mittelwert von x berechnet (Ausgabe in Zeile 21).

Das folgende Skript zeigt eine weitere Möglichkeit, wie man einen default-Wert für ein Argument setzen kann; da der Eingabewert x bereits vorliegt, kann man dessen Eigenschaften verwenden, um den default-Wert für m zu setzen. Im folgenden Beispiel wird anstelle von m = 1 der default-Wert m = rep(x = 1, times = length(x)) gesetzt (Zeile 1). Da jetzt die Eingabe-Vektoren gleiche Länge haben, darf beim Test mit weightedMean(x = (1:3)) der Warn-Hinweis nicht mehr erscheinen (Zeile 20):

weightedMean <- function(x, m = rep(x = 1, times = length(x))){
  stopifnot( is.numeric(x), is.numeric(m) )
  
  if( (length(m) == 1) && (length(x) > 1) ){
    warning("Es liegt nur ein Gewicht vor, obwohl ", length(x), " x-Werte gegeben sind!")
    return(mean(x))
  } else {
    if( !identical(length(x), length(m)) ){
      stop("Die Vektoren x und m haben unterschiedliche Längen: ", length(x), " und ", length(m))
    }
  }
  
  stopifnot(any(m != 0))
  
  stopifnot(all(m >= 0))
  
  return( sum(x * m) / sum(m) )
}

weightedMean(x = (1:3))
# [1] 2

Aufgabe: Implementieren Sie eine Funktion scalarProduct(), die das Skalarprodukt zweier Vektoren berechnet. Falls die Vektoren ungleiche Länge haben, wird der längere Vektor geeignet abgeschnitten und dann das Skalarprodukt berechnet. Welche Prüfungen für die Eingabewerte sind angemessen? Implementieren Sie eine geeignete Behandlung von NA-Werten in den Eingabe-Vektoren.

Der default-Wert kann einen von mehreren Werten annehmen

Oft ist es sinnvoll, dass für einen Eingabewert mehrere mögliche Werte vereinbart werden; wird ein Eingabewert eingegeben, der nicht mit diesen Werten übereinstimmt, soll ein Fehler erzeugt werden.

Um ein Beispiel hierfür vorzubereiten, wird zunächst eine sehr umständliche Implementierung von weightedMean() angegeben; sie enthält ein weiteres Argument interimResult, das festlegt, ob Zwischenergebnisse der Berechnung ausgegeben werden. Als default-Wert für interimResult wird "none" gewählt:

weightedMean <- function(x, m, interimResult = "none"){
  if(interimResult == "input" || interimResult == "all"){
    message("x = ", paste0(x, collapse = " "))
    message("weights = ", paste0(m, collapse = " "))
  }
  z <- sum(x * m)
  n <- sum(m)
  if(interimResult == "all"){
    message("weighted sum = ", z)
    message("sum of weights = ", n)
  }
  mean_weighted <- z / n
  return( mean_weighted )
}

weightedMean(x = (1:4), m = c(2, 2, 2, 4))
# [1] 2.8

weightedMean(x = (1:4), m = c(2, 2, 2, 4), interimResult = "input")
# x = 1 2 3 4
# weights = 2 2 2 4
# [1] 2.8

weightedMean(x = (1:4), m = c(2, 2, 2, 4), interimResult = "all")
# x = 1 2 3 4
# weights = 2 2 2 4
# weighted sum = 28
# sum of weights = 10
# [1] 2.8

weightedMean(x = (1:4), m = c(2, 2, 2, 4), interimResult = "extended")
# [1] 2.8

Man erkennt an der Implementierung:

  1. Ist interimResult = "none" , also gleich dem default-Wert, werden keine Zwischenergebnisse ausgegeben (siehe Test aus Zeile 16).
  2. Ist interimResult = "input" , werden die Eingabewerte für x und m ausgegeben (siehe Test aus Zeile 19).
  3. Ist interimResult = "all" , werden zusätzlich zu den Eingabewerten die gewichtete Summe sum(x * m) und die Summ der Gewichte sum(m) ausgegeben (siehe Test aus Zeile 24).

Um die Ausgabe zu erzeugen, wird die Funktion paste0() verwendet, die hier nicht erklärt wird (kurz: sie hängt die Komponenten eines Vektors aneinander und schreibt dazwischen je ein Leerzeichen).

Wird die Funktion weightedMean() mit dem Argument interimResult = "extended" aufgerufen, ist dies gleichbedeutend mit interimResult = "none" , da für "extended" in der Implementierung keine Anweisungen vorgesehen sind. (Siehe Test in Zeile 31.)

Dass das Argument interimResult nur einen der drei erlaubten Werte "none", "input", "all", erkennt man nicht an der Eingabeliste der Funktion weightedMean() sondern nur an ihrer Implementierung – die sich der Anwender üblicherweise nicht ansieht.

Man kann aber die Verwendung eines der drei Werte "none", "input", "all" erzwingen. Dies zeigt die nächste Implementierung von weightedMean():

weightedMean <- function(x, m, interimResult = c("none", "input", "all")){
  iR <- match.arg(arg = interimResult)
  if(iR == "input" || iR == "all"){
    message("x = ", paste0(x, collapse = " "))
    message("weights = ", paste0(m, collapse = " "))
  }
  z <- sum(x * m)
  n <- sum(m)
  if(iR == "all"){
    message("weighted sum = ", z)
    message("sum of weights = ", n)
  }
  mean_weighted <- z / n
  return( mean_weighted )
}

weightedMean(x = (1:4), m = c(2, 2, 2, 4))
# [1] 2.8

weightedMean(x = (1:4), m = c(2, 2, 2, 4), interimResult = "input")
# x = 1 2 3 4
# weights = 2 2 2 4
# [1] 2.8

weightedMean(x = (1:4), m = c(2, 2, 2, 4), interimResult = "all")
# x = 1 2 3 4
# weights = 2 2 2 4
# weighted sum = 28
# sum of weights = 10
# [1] 2.8

weightedMean(x = (1:4), m = c(2, 2, 2, 4), interimResult = "extended")
# Error in match.arg(interimResult) : 
#   'arg' should be one of “none”, “input”, “all”

Neu sind jetzt nur zwei Kleinigkeiten:

Zeile 1: Hier werden für das Argument interimResult die drei möglichen Werte festgelegt, wobei der erste Wert in der Liste "none" der default-Wert ist.

Zeile 2: Mit Hilfe von match.arg(arg = interimResult) wird abgefragt, welcher Wert für das Argument interimResult gesetzt wurde. Dieser Wert wird in iR abgespeichert. Die restliche Implementierung unterscheidet sich von der vorhergehenden Version nur dadurch, dass die Abfragen mit der Variable iR formuliert werden.

Die Tests zeigen das identische Verhalten wie oben (Zeile 17, 20, 25), mit dem Unterschied: Wird jetzt weightedMean() mit dem Argument interimResult = "extended" aufgerufen, erhält man eine Fehlermeldung (Zeile 32 bis 34).

Die Übergabe von Argumenten: call by value

Die Problemstellung

Inzwischen sollte das Verhalten einer selbstdefinierten Funktion und wie man sie implementiert besser verständlich sein. Dennoch wirft Abbildung 1 einige Fragen auf, die bisher noch nicht behandelt wurden, die aber zum Verständnis selbstdefinierter Funktionen nötig sind.

In Abbildung 1 kommen die Variablen x und y in mehreren Bedeutungen vor, die man klar voneinander trennen muss:

  • Die Variablen x und y sind die Eingabewerte der Funktion f() und in der Implementierung von f() werden diese Variablen zur Berechnung des Rückgabewertes verwendet und dabei womöglich verändert. Diese Implementierung verwendet x und y tatsächlich als Variable, also nicht mit konkreten Werten.
  • Die Variablen x und y werden im Skript definiert (in Abbildung 1 nicht dargestellt). Dies erfolgt vor dem Aufruf von f() und an f() werden x und y als Eingabewerte weitergegeben. Dazu müssen x und y konkrete Werte annehmen, um sie in die Implementierung von f() einsetzen zu können.
  • Ebenfalls nicht dargestellt ist, dass die Variablen x und y nach dem Aufruf von f() im Skript womöglich nochmal verwendet werden.

Aber genau hier ergibt sich ein Problem: Wenn in der Implementierung von f() die Werte von x und y verändert werden, betreffen diese Veränderungen auch die Variablen x und y im Skript?

Und man kann diese Frage auch umdrehen: Ist es möglich, innerhalb der Implementierung einer Funktion Variablen zu verändern, die nicht an die Funktion übergeben wurden?

In beiden Fällen würde man von Seiteneffekten sprechen und die Antwort kann schon vorweggenommen werden: In R gibt es keine Seiteneffekte oder sie sind als solche leicht erkennbar.

Einfaches Beispiel zur Demonstration von (fehlenden) Seiteneffekten

rm(list = ls())

dubiousFunction <- function(x){
  x <- 2 * x
  a <- a + 1
  return(x)
}
# no symbol named 'a' in scope

a <- 1
b <- 2

dubiousFunction(x = b)
# [1] 2

a
# [1] 1

b
# [1] 2

Zeile 1: Um die Seiteneffekte zu testen, muss man natürlich alle bereits vorhandenen Objekte löschen (sonst ist eventuell eines der zu untersuchenden Objekte bereits vorhanden).

Zeile 3 bis 7: Es wird eine Funktion dubiousFunction() definiert, die einen Eingabewert x hat. In der Implementierung von dubiousFunction() wird:

  • Der Wert von x verdoppelt und wiederum als x abgespeichert.
  • Der Wert von a um 1 erhöht – obwohla innerhalb der Funktion nirgends definiert war. Es ist daher unklar, was die rechte Seite von Zeile 5 zu bedeuten hat.

Zeile 8: Die Entwicklungsumgebung zeigt eine Warnung und den Hinweis, dass keine Variable namens a im Gültigkeitsbereich (scope) enthalten ist. Was unter einem Gültigkeitsbereich oder scope zu verstehen ist, wird weiter unten erklärt.

Zeile 10 und 11: Jetzt erst werden die beiden Variablen a und b mit 1 beziehungsweise 2 initialisiert.

Zeile 13 und 14: Die Funktion dubiousFunction() wird mit x = b aufgerufen. Der Rückgabewert ist gleich 4: klar, b = 2 wurde verdoppelt.

Zeile 16 und 17: Die Variable a wurde nicht verändert; die Anweisung in Zeile 5 hat offensichtlich keine Auswirkung auf die Variable a, die vor dem Aufruf von dubiousFunction() definiert wurde.

Zeile 19 und 20: Die Variable b hat immer noch den Wert 2 (so wie sie in Zeile 11 initialisiert wurde).

Erklärung der fehlenden Seiteneffekte: call by value und Gültigkeitsbereiche

Die Erklärung für das Verhalten des letzten Skriptes wirkt eher etwas abstrakt, ist aber für das Verständnis eines Funktions-Aufrufes enorm wichtig. Man sollte dazu nochmals Abbildung 1 betrachten.

Dort wird die Funktion f() mit den beiden Variablen x und y aufgerufen. Damit dieser Aufruf sinnvoll ist, müssen die Variablen zuvor im Skript initialisiert worden sein. Die Abbildung 1 suggeriert, dass die Objekte x und y an die Implementierung von f() übergeben werden und dort weiterverarbeitet werden. Diese Vorstellung ist falsch: Es werden Kopien der Objekte x und y angelegt und diese Kopien werden an die Implementierung von f() weitergegeben. Egal was nun mit diesen Kopien innerhalb der Funktion f() geschieht, die ursprünglichen Objekte werden dadurch nicht verändert.

Diesen Übergabe-Mechanismus nennt man call by value und die zugehörigen Variablen nennt man Wertparameter. Ein anderer Übergabe-Mechanismus – der aber in R nicht realisiert ist, heißt call by reference und die Variablen nennt man dann Referenzparameter.

Verknüpft mit dem Übergabe-Mechanismus ist der Begriff des Gültigkeitsbereiches (im Englischen wird er als scope bezeichnet). Damit ist gemeint, dass jede Variable einen Bereich besitzt, in dem sie verändert werden kann.

So sind zum Beispiel die Kopien, die an die Funktion übergeben werden nur innerhalb der geschweiften Klammern der Implementierung der Funktion definiert. Man nennt daher auch die Variablen x und y in f <- function(x, y){ ... } lokale Variable: Sie haben ihren Gültigkeitsbereich innerhalb der geschweiften Klammern; sie können dort verändert werden, diese Veränderungen sind aber nach außen (also außerhalb der geschweiften Klammern) nicht sichtbar.

Damit sollte auch das Verhalten des letzten Skriptes besser verständlich sein:

  • Die Variablen a und b werden außerhalb der Funktion dubiousFunction() initialisiert (Zeile 10 und 11), ihr Gültigkeitsbereich liegt außerhalb der Implementierung von dubiousFunction().
  • Wird nun die Funktion dubiousFunction() mit b aufgerufen, wird eine Kopie angelegt und an die Funktion übergeben. Mit dieser Kopie wird die Implementierung von dubiousFunction() abgearbeitet und der Rückgabewert berechnet.
  • Diese Kopie hat ihren Gültigkeitsbereich innerhalb der Implementierung von dubiousFunction(); wird also an b eine Veränderung vorgenommen (wie in Zeile 4), ist diese außerhalb der Implementierung von dubiousFunction() wirkungslos. (Siehe Zeile 20: b ist immer noch gleich 2.) Ebenso ist die Veränderung von a nach außen wirkungslos.

Erzeugen von Seiteneffekten mit assign()

Möchte man Seiteneffekte ausdrücklich erzeugen, so kann dies mit der Funktion assign() geschehen. Dies erfordert aber einige Kenntnisse darüber, wie in R Umgebungen (environments) aufgebaut und angeordnet sind. Dies soll hier nicht erläutert werden.

Zusammenfassung

Die Definition einer Funktion

Zur Implementierung einer Funktion werden function() und return() eingesetzt:

Funktion Beschreibung
function( arglist ) expr Erzeugt eine Funktion mit Eingabewerten arglist; der Ausdruck expr steht für die Implementierung der Funktion.
return(value) In der Implementierung einer Funktion können mehrere return-statements enthalten sein; dabei wird value an die Stelle zurückgegeben, an der die Funktion aufgerufen wurde. Falls value nicht angegeben wird, gibt die Funktion NULL zurück.

Die Implementierung einer Funktion f() mit Eingabewerten x und y sieht grob wie folgt aus:

f <- function(x, y){
  # Anweisungen, die x und y verwenden und einen Rückgabewert z berechnen
  return(z)
}

Prüfung von Eingabewerten

Achtung: Bei den hier gezeigten Funktionen sind nicht immer alle Eingabewerte angeführt, sondern nur diejenigen, die oben besprochen wurden. Für eine ausführliche Beschreibung der Funktionen ist die Dokumentation zu Rate zu ziehen.

Funktion Beschreibung
stopifnot(...) Das Argument ... steht für beliebig viele logische Ausdrücke; falls einer davon nicht erfüllt ist, wird die Abarbeitung der Funktion abgebrochen und ein entsprechender Warn-Hinweis erzeugt.
stop(...) Wird die Funktion stop() aufgerufen, wird die Abarbeitung der Funktion abgebrochen und es wird ein Warn-Hinweis ausgegeben, der aus den Argumenten ... zusammengesetzt wird (sie müssen in eine Zeichenkette verwandelt werden können).
message(...) Ausgabe einer Benachrichtigung, die aus den Argumenten ... zusammengesetzt wird.
warning(...) Ausgabe einer Warnung, die aus den Argumenten ... zusammengesetzt wird; die Abarbeitung der Funktion wird fortgesetzt.
missing(x) Prüft, ob das Argument x im Aufruf der Funktion fehlt; Rückgabewert ist der entsprechende logische Wert.
Alle Kommentare
Durch die Nutzung dieser Website erklären Sie sich mit der Verwendung von Cookies einverstanden. Unsere Datenschutzbestimmungen können Sie hier nachlesen