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).
Noch keine Stimmen abgegeben
Noch keine Kommentare

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.