Eigenschaften von Funktionen in R

Eine Funktion ruft einen Quelltext-Abschnitt auf, der durch Eingabewerte konfiguriert werden kann und der einen Rückgabewert berechnet. Allgemeine Eigenschaften und Besonderheiten über die Eingabewerte und den Rückgabewert einer Funktion werden besprochen. Dies soll auf das nächste Kapitel vorbereiten, in dem gezeigt wird, wie man Funktionen selbst definiert.

Einordnung des Artikels

Einführung

Programme oder Skripten bestehen üblicherweise aus einzelnen Anweisungen. Im Sinne der strukturierten Programmierung werden Blöcke von Anweisungen zu einer Funktion zusammengefasst, um

Den Aufruf einer Funktion kann man sich dann folgendermaßen vorstellen (siehe auch Abbildung 1 unten):

  1. Durch den Namen der Funktion wird der entsprechende Anweisungs-Block angesprochen und ausgeführt.
  2. Die Funktion kann Eingabewerte besitzen, damit der Anweisungs-Block von außen konfiguriert werden kann.
  3. Die Funktion berechnet einen Rückgabewert, der an der Stelle des Aufrufs der Funktion eingesetzt wird.
  4. Eine Funktion kann zusätzliche Operationen ausführen, etwa Konsolenausgaben erzeugen (zum Beispiel Warnungen).

Beispiel: Im folgenden Skript wird die Funktion mean() mit dem Vektor v aufgerufen, der vorher definiert wurde. Das Ergebnis der Berechnung von mean(v) wird in der Variable x abgespeichert und ausgegeben:

v <- (1:4)
x <- mean(v)
x
# [1] 2.5

Die Funktion mean() berechnet den Mittelwert des Vektors v und daher hat Zeile 2 dieselbe Wirkung wie die Zuweisung x <- 2.5 . Weitere Aktionen führt die Funktion mean() hier nicht aus.

Beim folgenden Aufruf von mean() wird zusätzlich zur Berechnung des Rückgabewertes eine Warnung ausgegeben:

v <- c("A", "B")
x <- mean(v)
x
# Warning message:
#   In mean.default(v) :
#   Argument ist weder numerisch noch boolesch: gebe NA zurück
# [1] NA

Da man von Zeichen keinen Mittelwert berechnen kann, ist in der Implementierung von mean() vorgesehen, dass der entsprechende Warn-Hinweis auf der Konsole erscheint. Als Rückgabewert wird jetzt NA berechnet (Ausgabe in Zeile 7).

♦ ♦ ♦ ♦ ♦

Abbildung 1 versucht den Aufruf einer Funktion f() darzustellen:

  1. In einem Skript erfolgt der Funktions-Aufruf in einer Zuweisung z <- f(x, y) .
  2. Dazu werden die Eingabewerte x und y der Funktion an die Implementierung der Funktion weitergereicht.
  3. Die Implementierung von f() ist nur durch { ... } angedeutet. Hier befindet sich ein Block von Anweisungen, der x und y verwendet und einen Rückgabewert berechnet.
  4. Der Rückgabewert wird an der Stelle des Funktions-Aufrufs im Skript eingesetzt und wird somit der Variable z zugewiesen.

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.

So wie Funktionen bisher beschrieben wurden, werden sie in nahezu allen Programmiersprachen eingesetzt. In diesem Kapitel über Funktionen werden einige Besonderheiten der Sprache R bezüglich Funktionen beschrieben. Dabei wird noch nicht auf selbstdefinierte Funktionen eingegangen; dies geschieht im nächsten Kapitel.

Der Aufruf einer Funktion

Funktionsaufrufe wurden bisher in allen Kapiteln verwendet, aber nicht eigens erklärt. Dies soll hier nachgeholt werden. Dabei werden einige Eigenschaften von Funktionen ausdrücklich genannt und erläutert, die so selbstverständlich sind, dass sie eigentlich keiner Erklärung bedürfen. Um später selbst Funktionen zu definieren, ist die Kenntnis der Eigenschaften aber unentbehrlich.

Funktionen werden auf eine der beiden im Folgenden beschriebenen Arten eingesetzt:

  1. Entweder wird eine Funktion direkt aufgerufen: in der entsprechenden Zeile eines Skriptes steht nur der Funktionsaufruf.
  2. Oder die Funktion wird aufgerufen und das Ergebnis der Berechnung wird einer Variable zugewiesen.

Das folgende Skript zeigt diese beiden Möglichkeiten für die Funktion mean(x) :

x <- c(3, 5)

mean(x)
# [1] 4

y <- mean(x)

y
# [1] 4

In Zeile 1 wird ein Vektor x mit 2 Komponenten definiert.

Zeile 3: Der direkte Aufruf von mean(x) berechnet den Mittelwert der Komponenten von x und gibt den Mittelwert sofort auf der Konsole aus.

Zeile 6: Jetzt wird wieder mean(x) berechnet, aber das Ergebnis wird in der Variable y abgespeichert. Da in der Implementierung von mean() keine Konsolenausgabe vorgesehen ist, erscheint keine Ausgabe.

Zeile 8: Erst wenn y ausgegeben wird, erscheint das Ergebnis der Mittelwert-Berechnung auf der Konsole.

Das folgende Skript wiederholt diese Schritte für die Funktion str(); sie hat keinen Rückgabewert, in ihrer Implementierung ist nur eine Konsolenausgabe vorgesehen:

x <- c(3, 5)

str(x)
#  [1:2] 3 5

y <- str(x)
# num [1:2] 3 5

y
# NULL

Zeile 3: Der Aufruf von str(x) erzeugt die Konsolenausgabe aus Zeile 4. Man beachte den Unterschied zu Zeile 4 im Skript mit der Funktion mean(): Dort wurde die Ausgabe mit [1] eingeleitet, da das Objekt ausgegeben wurde, das die Funktion mean() zurückgegeben hat. Jetzt erscheint keine [1] , da str() keinen Rückgabewert besitzt.

Zeile 6 und 7: Speichert man str(x) in einem Objekt y ab, so wird beim Aufruf von str(x) wieder die Konsolenausgabe wie in Zeile 4 erzeugt; daran, dass keine führende [1] vorhanden ist, erkennt man die Konsolenausgabe.

Zeile 9: Gibt man y aus, erhält man NULL, da die Funktion str() keinen Rückgabewert besitzt; die Konsolenausgabe erscheint nicht.

Das folgende Skript zeigt nochmal allgemein die beiden Möglichkeiten, wie eine Funktion f(x) eingesetzt werden kann:

# das Objekt x ist bereits gesetzt

f(x)

y <- f(x)

In Zeile 3 wird die Funktion f() mit dem Eingabewert x aufgerufen. Was wird dabei auf der Konsole ausgegeben? Dies hängt davon ab, wie f() implementiert ist:

Worin besteht der Unterschied zwischen den Zeile 3 und 5? In Zeile 5 wird wieder die Funktion f() mit dem Eingabewert x aufgerufen; das Ergebnis wird in der Variable y abgespeichert. Welche Konsolenausgaben werden dabei erzeugt?

Die folgende Skripte sollen den Unterschied von Zeile 3 und 5 (aus obigem Skript) für die Funktionen print() und cat() demonstrieren; dazu muss man wissen, dass print() den Eingabewert als Rückgabewert besitzt, das heißt der Aufruf von print(x) sorgt dafür, dass das Objekt x auf der Konsole ausgegeben wird. Dagegen hat cat() keinen Rückgabewert; der Aufruf von cat(x) sorgt für die Ausgabe von x auf der Konsole. Wo soll dann der Unterschied sein?

x <- (1:3)

print(x)
# [1] 1 2 3

y <- print(x)
y
# [1] 1 2 3

cat(x)
# 1 2 3

z <- cat(x)
# 1 2 3

z
# NULL

Zeile 1: Testobjekt für die folgenden Funktionsaufrufe ist der Vektor x.

Zeile 3 und 4: Rückgabewert von print() ist der Eingabewert, daher wird nach der [1] der Vektor x ausgegeben.

Zeile 6 bis 8: Wird print() in der Zuweisung auf der rechten Seite eingesetzt, erfolgt noch keine Ausgabe. Erst die Ausgabe von y gibt den Vektor aus, der zuvor als x gespeichert wurde.

Zeile 10 und 11: Der Aufruf von cat() gibt den Vektor x aus. Da hier kein Rückgabewert ausgegeben wird, fehlt [1] in der Ausgabe.

Zeile 13 und 14: Speichert man cat(x) in einer Variable z ab, erfolgt dennoch die Konsolenausgabe – sie wird in der Implementierung von cat() ausgelöst. Wieder fehlt die [1] in der Ausgabe.

Zeile 16 und 17: Gibt man jetzt z aus, erhält man NULL, da cat() keinen Rückgabewert besitzt.

Die Eingabewerte einer Funktion

Funktionen in der Programmierung haben viele Eigenschaften mit mathematischen Funktionen gemeinsam, es gibt aber auch einige wichtige Unterschiede, so dass die Namensgleichheit "Funktion" zu voreiligen Schlüssen führen kann.

Die Eingabewerte einer Funktion haben in der Mathematik und in der Programmierung viele Gemeinsamkeiten und sollen zuerst näher beleuchtet werden.

In der Mathematik kann man etwa eine Funktion f durch die Angabe eines Definitionsbereiches und einer Zuordnungsvorschrift definieren. So ist etwa mit

f: R × RR, f(x, y) = x2 + y2

eine quadratische Funktion auf dem Definitionsbereich Df = R × R gegeben.

Hier sind x und y die Eingabewerte, die jeden beliebigen Wert aus den reellen Zahlen R annehmen dürfen. Durch den Funktionsterm f(x, y) wird der Funktionswert an der Stelle (x, y) berechnet.

In R können Funktionen beliebig viele Eingabewerte besitzen. Dass zur Vereinbarung einer Funktion eine Angabe des Definitionsbereiches der Eingabewerte gehört, ist nicht so streng geregelt wie in der Mathematik.

Um ein Beispiel zu nennen: In der Dokumentation unter Set Operations findet man etwa die Funktion union(x, y) . Hier sind x und y die Eingabewerte. Laut Dokumentation sind x und y Vektoren mit identischem Modus, die als Mengen interpretiert werden (doppelte Elemente werden nicht berücksichtigt); die Funktion union(x, y) berechnet die Vereinigungsmenge von x und y und gibt sie als Vektor zurück.

Im Folgenden werden die Eingabewerte einer Funktion genauer untersucht:

Datentypen der Eingabewerte

In streng typisierten Programmiersprachen müssen die Datentypen der Eingabewerte einer Funktion eindeutig festgelegt werden. Dagegen werden in R Datentypen dynamisch erkannt. Dies hat folgende Konsequenz: Man kann für jeden Eingabewert einer Funktion im Prinzip jeden beliebigen Datentyp verwenden; werden die Anweisungen der Funktion ausgeführt, wird Schritt für Schritt entschieden, ob die entsprechende Anweisung sinnvoll ausgeführt werden kann oder nicht.

Man kann dies etwa am Beispiel der Funktion union() erklären: Die Eingabewerte sollen eigentlich Vektoren sein, die als Mengen interpretiert werden; der Rückgabewert ist dann der Vektor, der der Vereinigungsmenge entspricht. Auf den ersten Blick ist es daher unsinnig, für die Eingabewerte einen Vektor und eine Matrix zu wählen. Im folgenden Skript geschieht dies und man erhält einen sinnvollen Rückgabewert:

m <- matrix(data = (1:6), nrow = 2, byrow = TRUE)
m

v <- (7:9)

union(x = m, y = v)
# [1] 1 4 2 5 3 6 7 8 9

Da eine Matrix intern wie ein Vektor abgespeichert wird (wie die Zahlen zweidimensional anzuordnen sind, regelt das dim-Attribut), ist auch der Funktions-Aufruf aus Zeile 7 sinnvoll; es wird die Menge (1:9) – in ungewöhnlicher Sortierung – gebildet.

Etwas allgemeiner kann man jetzt fragen: Welche Möglichkeiten sind denkbar, wenn eine Funktion nicht die Eingabewerte erhält, die eigentlich vorgesehen sind? Die Antwort hängt natürlich von der Implementierung der Funktion ab – als Programmierer hat man hier einen gewissen Spielraum. Um die wichtigsten Möglichkeiten zu nennen:

  1. Man kümmert sich bei der Implementierung nicht um die Datentypen der Eingabewerte, sondern implementiert die Funktion so wie es der gewünschte Anwendungsfall erfordert. Die Entscheidung, was bei "unerwünschten" Datentypen geschieht, wird dem R-Interpreter überlassen, der eventuell einen Fehler oder eine Warnung anzeigt.
  2. Man sorgt selbst dafür, dass bei "unerwünschten" Datentypen eine Warnung erscheint und ein geeigneter Rückgabewert berechnet wird (etwa NA). Der Aufruf von mean() mit einem character-Vektor, der oben gezeigt wurde, ist ein Beispiel hierfür.

Welche Variante man wählt, sollte man in der Dokumentation der Funktion festhalten, damit ein Anwender der Funktion über das Verhalten der Funktion Bescheid weiß. Es ist auch empfehlenswert ein wenig in der R-Dokumentation zu blättern und nachzulesen, wie "unerwünschte" Datentypen behandelt werden oder einige Funktion daraufhin zu testen.

In einer streng typisierten Programmiersprache wird der Name einer Funktion und die Liste der Eingabewerte und ihrer Datentypen als die Signatur einer Funktion bezeichnet. In Programmiersprachen wie R, wo Datentypen dynamisch erkannt werden, ist diese Bezeichnung nicht so üblich.

Namen der Eingabewerte

Ein großer Vorzug der Programmiersprache R ist, dass die Eingabewerte einer Funktion Namen haben, die man beim Aufruf der Funktion angeben kann – man sollte hier besser sagen: angeben sollte. Denn die Namen der Eingabewerte machen in vielen Fällen die Quelltexte leichter lesbar.

Das folgende Beispiel mag hier noch nicht überzeugend sein:

union((1:3), (7:9))

union(x = (1:3), y = (7:9))

Die beiden Funktions-Aufrufe in Zeile 1 und 3 sind gleichwertig und führen zu einem identischen Ergebnis. Da man am Namen der Funktion ablesen kann, welche Aufgabe hier bearbeitet wird, sind die Namen x und y für die Eingabewerte kaum hilfreich.

Das folgende Beispiel zeigt, wie der Einsatz der Argument-Namen den Quelltext leichter verständlich macht. Verwendet wird jetzt die Funktion sort(), mit der sich ein Vektor sortieren lässt:

sort(x, decreasing = FALSE)

Eingabewerte sind der zu sortierende Vektor x und ein logischer Wert decreasing, der angibt, ob die Werte von x absteigend oder aufsteigend sortiert werden sollen; der default-Wert ist decreasing = FALSE , also aufsteigende Sortierung.

Im folgenden Skript wird zunächst ein Vektor v definiert, der dann aufsteigend sortiert wird. Dies geschieht dreimal und eigentlich sind alle drei Versionen identisch:

v <- c(1, 3, 2)

v.asc <- sort(v, FALSE)
v.asc
# [1] 1 2 3

v.asc <- sort(v)
v.asc
# [1] 1 2 3

v.asc <- sort(x = v, decreasing = FALSE)
v.asc
# [1] 1 2 3

In der ersten Version (Zeile 3) werden die beiden Argumente x und decreasing gesetzt, aber die Argument-Namen werden nicht angegeben. Damit der Leser die Anweisung sort(v, FALSE) richtig versteht, muss er parat haben, dass das zweite Argument decreasing heißt und nicht etwa increasing.

In der zweiten Version (Zeile 7) wird das zweite Argument nicht gesetzt; da decreasing mit einem default-Wert ausgestattet ist, muss dieser nicht explizit angegeben werden, wenn man ihn einsetzen möchte (mehr zu default-Werten weiter unten). Für einen Leser, der nicht parat hat, wie per default sortiert wird, ist diese Anweisung nicht verständlich.

Die dritte Version (Zeile 11) schließlich setzt die Argument-Namen ein und gibt sogar den eigentlich überflüssigen default-Wert an. Dadurch ist der Quelltext leicht verständlich und erspart im Zweifelsfall ein Nachschlagen in der Dokumentation.

Tip: Setzen Sie immer die Argument-Namen. Das Lesen der Quelltexte wird dadurch unglaublich erleichtert und man kann sich besser auf seine eigentlichen Aufgaben konzentrieren.

Umgekehrt mag es als zusätzlicher Aufwand erscheinen, wenn man beim Schreiben von Quelltexten immer die Argument-Namen angibt. Langfristig lohnt sich dies aber, da man dadurch nur besser mit den Funktionen vertraut wird.

Optionale Eingabewerte und default-Werte für Eingabewerte

Oben wurde beim Beispiel der Funktion sort() erklärt, dass sie ein Argument decreasing = FALSE besitzt. In der Dokumentation (und dann auch, wenn man Funktionen selber implementiert) wird dies folgendermaßen ausgedrückt:

sort(x, decreasing = FALSE)

Dies ist so zu lesen:

  1. Das Argument x ist verpflichtend; damit wird der Vektor eingegeben, der sortiert werden soll.
  2. Das Argument decreasing ist optional, das heißt es muss nicht gesetzt werden. Wird es nicht gesetzt, wird der default-Wert FALSE verwendet (und ansteigend sortiert).

Ein weiteres Beispiel, das in ähnlicher Form sehr oft auftritt, betrifft die Behandlung von NA-Werten. So besitzt zum Beispiel die Funktion mean() die Argumente x und na.rm, wobei Letzteres den default-Wert FALSE besitzt:

mean(x, na.rm = FALSE)

Die Bedeutung sollte klar sein:

  1. Das Argument x steht für den Vektor, von dessen Komponenten der Mittelwert berechnet werden soll.
  2. Das Argument na.rm regelt, wie NA-Werte behandelt werden. Im Fall von na.rm = FALSE verbleiben sie im Vektor x, im Fall von na.rm = TRUE werden sie aus dem Vektor entfernt. Wird das Argument na.rm nicht ausdrücklich gesetzt, wird der default-Wert verwendet.

Gerade wenn man selber Funktionen implementiert, kennt man die typischen Anwendungsfälle der Funktionen und kann dem Anwender der Funktion durch geschicktes Setzen von optionalen Argumenten und ihren default-Werten die Arbeit erleichtern.

Aufgabe: Stöbern Sie ein wenig in der R-Dokumentation und achten Sie darauf, für welche Argumente default-Werte gesetzt werden.

Das Argument "..." (dot-dot-dot)

Wer schon mit einer streng typisierten Programmiersprache gearbeitet hat, ist vielleicht verwirrt, wenn eine Funktion Eingabewerte wie sum() besitzt:

sum(..., na.rm = FALSE)

Das zweite Argument wurde soeben erklärt. Aber was soll sich hinter ... verbergen? Oder genauer gefragt: Welchen Datentyp hat ein Objekt, das hier eingegeben werden soll?

Die Notation ... in der Argument-Liste einer Funktion steht meist dafür, dass hier beliebig viele Objekte gesetzt werden können. Ob diese Objekte einen bestimmten Datentyp haben müssen, ist zunächst nicht festgelegt. Wie oben erklärt wurde, entscheidet darüber die Implementierung der Funktion – und entsprechende Hinweise sollten in der Dokumentation enthalten sein.

Um drei typische Beispiele zu nennen, die im Folgenden kurz erklärt werden:

  1. Die Funktion list(...) .
  2. Die Funktion sum(..., na.rm = FALSE) .
  3. Die Funktion rep(x, ...) .

1. Beispiel: Die Funktion list(...)

Die Funktion list() dient dazu, mehrere Objekte zu einer Liste zusammenzufassen. Anders als bei einem Vektor können diese Objekte beliebigen Datentyp haben – genau das macht eine rekursive Struktur aus. Das heißt aber, dass man sich hier nicht um die Datentypen der Argumente kümmern muss, es sind sogar wiederum Listen erlaubt.

Das folgende Beispiel wurde im Kapitel über Listen mehrfach verwendet:

book.R <- list(title = "Introduction to R", 
            author = "R-expert", year = 2018)

Hier werden also zwei Zeichenketten und eine Zahl zu einer Liste mit drei Komponenten zusammengefasst.

Man erkennt an dem Beispiel noch eine weitere Besonderheit: Das Argument ... besitzt eigentlich keinen Namen, anders als zum Beispiel die Funktion mean(x, na.rm = FALSE) , die man etwa mit mean(x = c(1, 3, 5) aufrufen könnte. Die Namen, die im Beispiel mit list() verwendet werden, sind keine Argument-Namen, sondern Namen, die dann für das Attribut names verwendet werden. Derartige Dinge regelt dann natürlich die Implementierung einer Funktion und sollte in der Dokumentation erläutert sein.

2. Beispiel: Die Funktion sum(..., na.rm = FALSE)

Das Argument ... in der Funktion sum() hat eine leicht andere Bedeutung als in der Funktion list(). Denn jetzt steht ... für die Objekte, deren Summe berechnet werden soll. Da man Summen nur von Zahlen (und nicht von Zeichenketten oder Listen) berechnen kann, müssen hier numerische Vektoren (Modus numeric) eingegeben werden. Versucht man andere Objekte zu setzen, erhält man eine Fehlermeldung. Aber wie bei der Funktion list() können hier beliebig viele Objekte eingegeben werden.

3. Beispiel: Die Funktion rep(x, ...)

Die Funktion rep() dient dazu, Wiederholungen von x zu erzeugen. Mit den Argumenten ... können weitere Parameter gesetzt werden – welche ist natürlich in der Dokumentation nachzulesen. Zum Beispiel das Argument times, das angibt, wie oft x wiederholt werden soll; oder das Argument each, das angibt, wie oft die einzelnen Komponenten von x wiederholt werden sollen. Hier sind einige Beispiele:

v <- rep(x = 17, times = 5)
v
# 17 17 17 17 17

v <- (1:3)
v3 <- rep(x = v, each = 3)
v3
[1] 1 1 1 2 2 2 3 3 3

In der Dokumentation ist genau festgelegt, welche Argumente anstelle von ... eingegeben werden können.

Die Reihenfolge der Argumente

Oben wurde schon beschrieben, dass die Eingabewerte einer Funktion Namen besitzen und dass die Quelltexte leichter lesbar sind, wenn man sie stets angibt. Die Argument-Namen ermöglichen eine weitere Erleichterung: Man muss nicht auf die Reihenfolge der Argumente achten, wenn man die Namen angibt.

Das folgende Beispiel zeigt drei identische Funktionsaufrufe:

rep(x = 17, times = 5)
# [1] 17 17 17 17 17

rep(times = 5, x = 17)
# [1] 17 17 17 17 17

rep(17, 5)
# [1] 17 17 17 17 17

Die ersten beiden Funktions-Aufrufe verwenden Namen, daher ist es unerheblich, in welcher Reihenfolge die Argumente gesetzt werden.

Der dritte Funktionsaufruf (Zeile 7) verwendet keine Argument-Namen; jetzt werden die Argumente in der Reihenfolge interpretiert, wie sie in der Dokumentation angegeben werden. Da das zweite Argument der Funktion ... lautet, ist bei Zeile 7 nicht eindeutig erkennbar, welches der hier erlaubten Argumente gleich 5 gesetzt wird. Daran siehrt man nochmal, wie hilfreich es ist, immer die Argument-Namen anzugeben.

Zusammenfassend kann man die Vorgehensweise des Interpreters beim Auswerten der Argumente einer Funktion so beschreiben:

  1. Sind keine Argument-Namen gesetzt, werden die Argumente in ihrer Reihenfolge gelesen und die Werte der Argument-Liste einer Funktion zugeordnet.
  2. Sind Argument-Namen gesetzt, ist die Reihenfolge irrelevant; die Zuordnung erfolgt allein durch die Namen.

Operatoren und Funktionen als Eingabewert

Bisher wurden stets Daten als Argumente einer Funktion verwendet, also Zahlen, Vektoren, Zeichenketten oder ähnliche Objekte. Es ist auch möglich als Argument eine Funktion oder einen Operator zu setzen. Ein Paradebeispiel dafür ist die Funktion outer(), die bereits in Das äußere Produkt outer() im Kapitel Matrizen in R: der Datentyp matrix ausführlich besprochen wurde. Andere wichtige Beispiele sind die Funktionen der apply()-Familie, die später besprochen werden (die Funktion tapply() wurde bereits bei Faktoren besprochen, siehe Die Funktion tapply() im Kapitel Faktoren in R: Anwendungen).

Die Funktion outer(X, Y, FUN = "*", ...) besitzt zwei Vektoren X und Y als Eingabewerte (man kann auch beliebige Felder verwenden); von diesen beiden Vektoren wird das äußere Produkt gebildet, wobei die entsprechenden Komponenten durch den mit FUN angegebenen Operator verknüpft werden. Der default-Wert für FUN ist die Multiplikation. Anstelle eines Operators kann man auch eine Funktion übergeben. Das folgende Skript zeigt einige Beispiele:

v <- (1:6)

# outer mit Operator +
m <- outer(X = v, Y = v, FUN = "+")
m
#     [,1] [,2] [,3] [,4] [,5] [,6]
# [1,]    2    3    4    5    6    7
# [2,]    3    4    5    6    7    8
# [3,]    4    5    6    7    8    9
# [4,]    5    6    7    8    9   10
# [5,]    6    7    8    9   10   11
# [6,]    7    8    9   10   11   12

# outer() mit Funktion pmax (paralleles Maximum)
m <- outer(X = v, Y = v, FUN = pmax)
m
#      [,1] [,2] [,3] [,4] [,5] [,6]
# [1,]    1    2    3    4    5    6
# [2,]    2    2    3    4    5    6
# [3,]    3    3    3    4    5    6
# [4,]    4    4    4    4    5    6
# [5,]    5    5    5    5    5    6
# [6,]    6    6    6    6    6    6

# outer() mit einer anonymen Funktion (die ein gewichtetes Mittel berechnet)
v <- (1:3)
m <- outer(X = v, Y = v, 
          FUN = function(x, y, ax, ay){
             a <- ax + ay;
             return( (ax * x + ay * y) / a )
          }, 2, 1)
m
#     [,1]     [,2]     [,3]
# [1,] 1.000000 1.333333 1.666667
# [2,] 1.666667 2.000000 2.333333
# [3,] 2.333333 2.666667 3.000000

Das erste Beispiel könnte man wie folgt interpretieren: Es wird zweimal nacheinander gewürfelt und es wird für die 36 möglichen Kombinationen die Augensumme gebildet.

Im zweiten Beispiel wird die maximale Augenzahl der beiden Würfe gebildet.

Im dritten Beispiel wird eine anonyme Funktion an FUN übergeben; anonym bedeutet, dass die Funktion sofort implementiert wird. Wenn man in der Implementierung zuerst ax = 1 und ay = 1 setzt, wird der Mittelwert von x und y berechnet und zurückgegeben. Die Faktoren ax und ay sorgen dafür, dass ein gewichteter Mittelwert von x und y berechnet wird. (Anonyme Funktionen werden dann im Kapitel über selbstdefinierte Funktionen ausführlich erläutert.)

Oben wurde schon gesagt, dass die Funktion outer() die Eingabewerte hat: outer(X, Y, FUN = "*", ...) . Im dritten Beispiel erkennt man jetzt auch die Verwendung des Argumentes ... in outer(): Die beiden Zahlen 2 und 1 (also die beiden letzten Argumente von outer() werden an die anonyme Funktion weitergegeben und setzen die Gewichtungen ax = 2 und ay = 1.

Aufgabe: Testen Sie den Funktionsaufruf

m <- outer(X = v, Y = v, 
          FUN = function(x, y, ax, ay){
             a <- ax + ay;
             return( (ax * x + ay * y) / a )
          }, 1, 1)

Jetzt sollte der Mittelwert berechnet werden, den man auch mit FUN = mean erhält.

Wie man für das Argument FUN selbstdefinierte Funktionen einsetzen kann, wird dann im nächsten Kapitel gezeigt.

Der Rückgabewert einer Funktion

Der Datentyp des Rückgabewertes

Betrachtet man jetzt eine Zuweisung wie

y <- f(x)

unter dem Aspekt, welche Datentypen der Rückgabewert von f() und damit auch y haben, so ist die Situation in R deutlich komplizierter als in streng typisierten Programmiersprachen – manche werden die Situation auch für deutlich einfacher halten.

In einer streng typisierten Programmiersprache muss der Rückgabewert einer Funktion einen eindeutigen Datentyp haben (lediglich wenn Vererbung ins Spiel kommt, sind gewisse Mehrdeutigkeiten zulässig). Und dieser Datentyp wird an die Variable y übergeben. Anders gesagt: wenn die Variable y deklariert wird, muss ihr schon der Datentyp verliehen werden, der von der Funktion f() zurückgegeben wird. Andernfalls wird die Zuweisung y <- f(x) einen Compiler-Fehler verursachen.

Dagegen werden in R Datentypen dynamisch erkannt. Dies bedeutet, dass für die Variable y kein Datentyp vereinbart werden muss (daher gibt es auch keine Deklarationen in R); die Variable y nimmt einfach denjenigen Datentyp an, der bei der Berechnung von f(x) entsteht.

Eine Folge davon, dass die Datentypen dynamisch erkannt werden, ist, dass eine Funktion in R unterschiedliche Rückgabewerte haben kann – in einer streng typisierten Sprache muss bei der Deklaration der Funktion der Datentyp des Rückgabewertes vereinbart werden und ist somit eindeutig. Ein einfaches Beispiel hierfür ist die Funktion print(): sie gibt das Objekt zurück, das als Eingabewert übergeben wurde. Das folgende Skript zeigt dies für unterschiedliche Datentypen:

x <- 17

str(print(x))
# [1] 17
# num 17

v <- c('A', 'B', 'C')

str(print(v))
# [1] "A" "B" "C"
# chr [1:3] "A" "B" "C"

m <- matrix(data = (1:6), nrow = 2, byrow = TRUE)

str(print(m))
#     [,1] [,2] [,3]
# [1,]    1    2    3
# [2,]    4    5    6
# int [1:2, 1:3] 1 4 2 5 3 6

Man sieht an diesen Beispielen, dass eine Funktion unterschiedliche Rückgabewerte besitzen kann – abhängig davon, welcher Eingabewert übergeben wird.

Und weiter kann man an diesem Verhalten von R sofort eine Folgerung für selbstdefinierte Funktionen ableiten: Man sollte nur in Ausnahmefällen zulassen, dass eine Funktion Rückgabewerte von verschiedenen Datentypen haben kann, da sonst Zuweisungen wie y <- f(x) schwer verständlich sind. Denn soll später mit y weitergerechnet werden, möchte man dessen Datentyp kennen (andernfalls können unter bestimmten Bedingungen Fehler in der Ausführung des Skriptes auftreten).

Funktionen ohne Rückgabewert

Es ist natürlich auch möglich, Funktionen zu definieren, die keinen Rückgabewert besitzen; oben wurden schon die Funktionen cat() und str() besprochen, die hier als Beispiele angeführt werden können.

Die Implementierung einer Funktion

Mit der Implementierung einer Funktion ist derjenige Quelltext gemeint, der ausgeführt wird, wenn eine Funktion aufgerufen wird. Dies wird im nächsten Kapitel – bei den selbstdefinierten Funktionen – ausführlich besprochen; jetzt sollen nur einige allgemeine Hinweise gegeben werden.

Generische Funktionen

In den bisherigen Kapiteln wurde oft gesagt, dass eine Funktion "generisch implementiert" ist; damit ist gemeint, dass für die Funktion mehrere Implementierungen existieren und je nach Datentyp des ersten Eingabewertes wird die entsprechende Implementierung aufgerufen.

1. Beispiel: head(x, n)

Als Beispiel sei die Funktion head(x, n) aus dem Paket utils genannt: Hier ist x ein R-Objekt und n eine ganze Zahl.

Ist x ein Vektor, so ist der Rückgabewert von head(x, n) der Vektor, der aus den ersten n Komponenten von x besteht. Ist dagegen x eine Matrix, so ist der Rückgabewert von head(x, n) die Matrix, die aus den ersten n Zeilen von x besteht.

2. Beispiel: print(x)

Ist der Eingabewert zum Beispiel ein Vektor, werden dessen Komponenten in einer Zeile ausgegeben. Falls Namen der Komponenten gesetzt sind (Attribut names) erfolgt eine tabellarische Ausgabe, die die Zuordnung der Namen zu den Komponenten erlaubt.

Bei einer Liste als Eingabewert, werden die Komponenten mit ihren Namen ausgegeben (zwischen den Komponenten befindet sich je eine Leerzeile). Für jede Komponenten wiederum wird die Funktion print() aufgerufen.

♦ ♦ ♦ ♦

Generische Implementierung kann sogar eine weitere Bedeutung haben: Für unterschiedliche Datentypen des ersten Eingabewertes kann es sogar Realisierungen mit unterschiedlichen Argument-Listen geben.

So ist zum Beispiel in der Dokumentation für die Funktion print() unter Usage zu lesen:

print(x, ...)

## S3 method for class 'factor'
print(x, quote = FALSE, max.levels = NULL,
        width = getOption("width"), ...)
        
## S3 method for class 'table'
print(x, digits = getOption("digits"), quote = FALSE,
        na.print = "", zero.print = "0", justify = "none", ...)
        
## S3 method for class 'function'
print(x, useSource = TRUE, ...)

Dies kann hier noch nicht genauer erklärt werden, da sich hinter dem Ausdruck S3 method for class ... verbirgt, wie man in R objekt-orientierte Programmierung realisiert. Es soll nur noch folgender Hinweis gegeben werden: Mit Hilfe der Funktion methods() aus dem Paket utils

methods(generic.function, class)

kann man sich zu einer gegebenen Funktion generic.function anzeigen lassen, welche Implementierungen existieren. Oder man kann sich zu einer gegebenen Klasse class anzeigen lassen, welche Funktionen für diese Klasse existieren (Funktionen, die einer Klasse zugeordnet sind, werden meist als Methoden bezeichnet).

So wird zum Beispiel für matrix angegeben:

methods(class="matrix")
# [1] anyDuplicated as.data.frame as.raster     boxplot       coerce        determinant   duplicated    edit          head          initialize    isSymmetric  
# [12] Math          Math2         Ops           relist        subset        summary       tail          unique       
# see '?methods' for accessing help and source code

Einige dieser Funktionen wurden in den Kapiteln über Matrizen ausführlich vorgestellt.

Aufgabe:

Testen Sie den Einsatz von methods() für einige Funktionen, die Sie schon öfters verwendet haben.

Der Quelltext einer Funktion

Oben wurden die Implementierungen der Funktion print() gezeigt; die letzte dieser Funktionen bezieht sich auf die Klasse function. Es ist naheliegend zu fragen, welche Ausgabe für eine Funktion zu erwarten ist; das Argument useSource deutet zudem darauf hin, dass man hier auf den Quelltext einer Funktion zugreifen kann.

Für das Beispiel der Funktion mean() soll dies nun demonstriert werden. Dazu wird zunächst die print()-Funktion für mean() aufgerufen:

print(mean)
# function (x, ...) 
#   UseMethod("mean")
# <bytecode: 0x000000001af19a50>
#   <environment: namespace:base>

Die Ausgabe ist wenig hilfreich und eher enttauschend, da der Quelltext nicht gezeigt wird: Zeile 2 besagt lediglich, dass die Funktion mean() neben x das dot-dot-dot-Argument besitzt. Zeile 5 besagt, dass sie sich im Paket base befindet.

Aufschlussreicher ist es, die Funktion methods() auf mean() anzuwenden – oben wurde gezeigt, dass sich damit die unterschiedlichen Implementierungen einer Funktion anzeigen lassen:

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

Lässt man sich jetzt die Funktion mean.default() ausgeben (Zeile 1), erhält man tatsächlich ihren Quelltext:

print(mean.default)
function (x, trim = 0, na.rm = FALSE, ...)
{
  if (!is.numeric(x) && !is.complex(x) && !is.logical(x)) {
    warning("argument is not numeric or logical: returning NA")
    return(NA_real_)
  }
  if (na.rm)
    x <- x[!is.na(x)]
  if (!is.numeric(trim) || length(trim) != 1L)
    stop("'trim' must be numeric of length one")
  n <- length(x)
  if (trim > 0 && n) {
    if (is.complex(x))
      stop("trimmed means are not defined for complex data")
    if (anyNA(x))
      return(NA_real_)
    if (trim >= 0.5)
      return(stats::median(x, na.rm = FALSE))
    lo <- floor(n * trim) + 1
    hi <- n + 1 - lo
    x <- sort.int(x, partial = unique(c(lo, hi)))[lo:hi]
  }
  .Internal(mean(x))
}
# <bytecode: 0x000000001a29e320>
#   <environment: namespace:base>

Es soll jetzt nicht versucht werden, diesen Quelltext nachzuvollziehen