Methoden und Funktionen


Bei verschiedenen Gelegenheiten habe ich bemerkt, dass man es in Java-Land mit der Unterscheidung von Methoden und Funktionen nicht so genau nimmt. Dabei kennt Java eigentlich nur Methoden, die zwar auch Funktionen im mathematischen Sinne, aber nicht im Sinne einer „programmtechnischen Einheit“ (wie in den funktionalen Sprachen) sind. Um beispielsweise eine Methode als ein Argument zu übergeben, habe ich Java im Wesentlichen nur zwei Tricks auf Lager: Entweder ich bastle ein geeignetes Wrapper-Objekt, oder ich greife zu Reflection und übergebe eine java.lang.reflect.Method und das zugehörige Objekt.

Auch in Scala kann man recht gut damit leben, die feinen Unterschiede zu ignorieren, und dass, obwohl Scala auch Funktionen (im Sinne funktionaler Sprachen) besitzt. Der Grund dafür ist die ziemlich reibungslose Zusammenarbeit der beiden Sprachbestandteile. Es ist eher selten, dass man deswegen in Scala irgendwo in Schwierigkeiten gerät, aber trotzdem kann es dem Verständnis nicht schaden, die Unterschiede einmal klar zu machen.

Gerade der dicke syntaktische Zucker, der die mit Abstand wichtigste Klebstelle Scalas zwischen funktionaler und objektorientierter Welt bedeckt, macht es einem am Anfang etwas schwer zu verstehen, was hinter den Kulissen abläuft. Prinzipiell gilt, dass sich Scala-Methoden weitgehend wie Java-Methoden verhalten (abgesehen davon, dass es kein echtes Äquivalent für statische Methoden in Scala gibt). Für eine Scala-Methode wird also in etwa der gleiche Byte-Code wie für eine Java-Methode produziert. Dementsprechend kann eine Scala-Methode auch nicht als ein Argument auftauchen, oder? Oder doch?

class Foo {
  def square(d:Double) = d*d
}

val foo = new Foo

//Eine Methode als Argument?
List(1,2,3).map(foo.square(_))
res0: List[Double] = List(1.0, 4.0, 9.0)

//Eine Methode wird einer Variablen zugewiesen?
val s:Double=>Double = foo.square
//--> s: (Double) => Double = <function>
s(3)
//--> res1: Double = 9.0

Nun, wer auf die Ausgabe nach der Definition der Variablen s schaut, kann des Rätsels Lösung schon erraten: Die Methode wird in eine Funktion umgewandelt, genauer gesagt, hübsch in ein Funktionsobjekt eingepackt. Wie sieht nun das Ding aus, das aus foo.square geworden ist? Für Funktionen mit einem Argument finden wir im Package scala das für Funktionen mit einem Argument zuständige Trait Function1 (es gibt davon welche für 0 bis 22 Argumente, bei mehr Argumenten kapituliert der Compiler), das so aussieht:

trait Function1[-T1, +R] extends AnyRef { self =>
  def apply(v1:T1): R
  override def toString() = "<function1>"
  ...
}

Die oft anzutreffende Typ-Schreibweise T1 => R ist übrigens nur eine bequeme Abkürzung für den Typ Function1[T1, R]. Unser Funktionswrapper-Objekt im obigen Beispiel könnte man nun (unter der Voraussetzung, dass die Variable foo im Sichtbarkeitsbereich liegt) in etwa so „per Hand“ schreiben:

val s = new Function1[Double, Double] {
   def apply(v: Double): Double = foo.square(v)
}

Bekanntlich ist sind alle Methoden in Scala, die apply heißen, ein bisschen „magisch“, denn man darf das apply beim Aufruf weglassen, also statt gnarf.apply(42) einfach gnarf(42) schreiben. Und damit lassen sich auch Funktionen, die eine apply-Methode besitzen, und Methoden, die das nicht tun (Wie soll auch eine Methode eine Methode besitzen?) unterscheiden:

s.apply(23)
//-> res0: Double = 529.0
foo.square.apply(23)
//--> error: missing arguments for method square in class Foo;
//--> follow this method with `_' if you want to treat it as a partially applied function
//-->        foo.square.apply(23)
//-->            ^

Bevor ich nun auf die verschiedenen syntaktischen Finessen bei der Definition von Funktionen eingehe, möchte ich die Rolle rückwärts wagen, nämlich eine Funktion einer Methode „zuweisen“:

class Bar(f:Double=>Double) {
  def m = f
}

val bar = new Bar(d => d*d)
bar.m(13)
//--> res0: Double = 169.0

Dass das funktionert, ist eigentlich ganz einleuchtend. Schon weiter oben hatten wir eine Funktion einer Variablen zugewiesen, und hier ist es ganz ähnlich. Es mag verwundern, dass m in der Definition keine Argumentliste hat, aber scheinbar mit einem Argument aufgerufen werden kann. Aber m liefert ja (ohne das dafür ein Argument nötig wäre) eine Funktion zurück, und natürlich darf ich diese Funktion sofort und auf der Stelle mit einem Argument aufrufen.

Und nun wie angekündigt noch ein paar syntaktische Varianten für unsere square-Funktion:

//Unter Verwendung des Function1-Traits
val s = new Function1[Double, Double] { def apply(d: Double) = d*d }

//Diese Variante ähnelt einer gewöhnlichen Methoden-Definition
val s = (d: Double) => d*d

//Hier wird zuerst der Typ der Variablen festgelegt, den Rest erledigt die Typinferenz
val s: Double => Double = d => d*d

//Wenn jedes Argument im Funktionskörper genau einmal (und bei mehreren Argumenten 
//auch in der gleichen Reihenfolge wie in der Argumentliste) auftritt wie in dieser Version...
val s: Double => Double = d => Math.pow(d, 2)
//... kann die Argumentliste weggelassen und jedes Argument durch _ ersetzt werden
val s: Double => Double = Math.pow(_, 2)

Die letzte Variante erklärt auch das seltsame _+_ in Beispielen wie List(1,2,3).reduceLeft(_+_): Eigentlich haben wir eine Funktion der Art (x:Int, y:Int) => x.add(y) vor uns, nur heißt die add-Funktion +, Punkt und Klammern wurden weggelassen (Ein wenig mehr syntaktischer Zucker: Statt a.b(c) darf man immer a b c schreiben) und am Ende wurden x und y wie oben gezeigt durch _ ersetzt.

Als letztes sei noch ein kleiner Stolperstein im Methoden-Funktionen-Dunstkreis erwähnt, der für Anfänger immer wieder überraschend ist:

val s = foo.square
//--> error: missing arguments for method square in class Foo;
//--> follow this method with `_' if you want to treat it as a partially applied function
//-->        val s = foo.square
//-->                    ^

//Folgen wir einmal dem Tipp aus der Fehlermeldung und hängen einen Unterstrich an:
val s = foo.square _
//--> s: (Double) => Double = <function>
s(12)
//--> res0: Double = 144.0

Weiter oben hatte ich das Problem umgangen, indem ich für s den Typ Double=>Double vorgab. Der Grund für den Fehler ist hoffentlich nach Lektüre dieses Artikels etwas klarer geworden: Auf der rechten Seite steht eine Methode, und die will primär nur eines, nämlich aufgerufen werden. Das funktioniert hier nicht, weil keine Argumente da sind. Auf die zweite Möglichkeit, sich in eine Funktion zu verwandeln, kommt die Methode hier ohne zartes Schubsen unsererseits (durch den Unterstrich, der hier soviel bedeutet wie „nimm mich so wie ich bin, führe mich nicht aus“) oder der Typinferenz (durch Angabe des gewünschten Funktionstyps Double=>Double) nicht. Hat man eine Funktion statt einer Methode, gibt es dieses Problem bei der Zuweisung übrigens nicht, man könnte hier z.B. einfach val t = s schreiben.

Zu Funktionen ließe sich noch eine Menge sagen (etwa zur Funktions-Komposition, dem Currying oder praktischen Tuple-Tricks), aber ich will es an dieser Stelle erst einmal bewenden lassen, damit die eigentliche Botschaft nicht untergeht: Methoden und Funktionen sind zwei unterschiedliche Sprachbestandteile in Scala, und Funktionen sind „unter der Motorhaube“ ganz normale Objekte.

3 Gedanken zu “Methoden und Funktionen

  1. Dennoch ist es den Scala-Entwicklern für meine Begriffe ganz gut gelungen, die Unterschiede zwischen Methoden und Funktionen zu minimieren und für den normalen Scala-Programmierer zu verbergen (behaupte ich mal). Denn sieht man einmal von der Hilfsmethode apply ab, die man ja in Scala zum Aufruf von Funktionen nicht unbedingt braucht, können Methoden und Funktionen im Grunde auch gleich behandelt werden, auch wenn beide intern (im Byte-Code) anderes realisiert werden. Für meine Begriffe muss man sich erst mit dem Unterschied auseinandersetzen, wenn man Funktionsobjekte mit weiterer Funktionalität ausstatten möchte (z.B. Map) oder ein Scala-Programm im Speicherverbrauch und in der Laufzeit verbessern möchte.

Hinterlasse einen Kommentar

Diese Seite verwendet Akismet, um Spam zu reduzieren. Erfahre, wie deine Kommentardaten verarbeitet werden..