Teil 16: Funktionen

Viel haben wir uns in letzter Zeit mit Scala beschäftigt. Das bisher gesehene war aber nur die Spitze des Eisbergs und gab uns lediglich einen Einblick in die imperative Programmierung mit Scala. Lassen wir dies aber nun hinter uns und folgen endlich dem Weg der funktionalen Programmierung. Den Anfang machen die Funktionen, da sie der Grundbestandteil einer jeden funktionalen Sprache sind.

Funktionen aus der funktionalen Programmierung sind nicht zu verwechseln mit den Funktionen aus der prozeduralen Programmierung. In letzterem dienen sie dazu den Kontrollfluss übersichtlicher zu gestalten und Redundanzen zu vermindern. Das Konzept einer Funktion aus der funktionalen Programmierung ähnelt dagegen mehr dem einer Methode, die nicht an eine Klasse gebunden ist: Sie kann einen Wert als Eingabe erhalten und liefert dazu ein entsprechendes Resultat. Im Gegensatz zu einer Methode ist eine Funktion in Scala aber ein Objekt und kann deshalb auch so behandelt werden. Sie können in einem Programm herumgereicht und in Ausdrücken ausgewertet werden. Das macht sie viel flexibler als stinknormale Methoden, die immer nur an eine einzelne Klasse gebunden sind.

Durch diese große Ähnlichkeit ist eine Funktion in der Benutzung auf den ersten Blick nicht von einer Methode zu unterscheiden:

val double: (Int) => (Int) = {
  (i: Int) => i*2
}

scala> double(5)
res26: Int = 10

scala> double(-12)
res27: Int = -24

Eine Funktion wird durch das =>-Symbol erstellt, dem sogenannten Funktions-Literal. Wie allem in Scala können wir der Funktion mit dem Gleichheitszeichen einen Körper zuweisen, der jedes Mal ausgeführt wird wenn die Funktion aufgerufen wird. Dass eine Funktion aber eben nicht wie eine Methode funktioniert ist ganz klar an dem „val“ vor der Definition zu erkennen. Die Funktion wird an eine Variable gebunden und kann dennoch mit Parametern aufgerufen werden – das ist bei einer Methode nicht möglich. Was nach dem Namen der Variable folgt ist der Typ der Variable. In unserem Fall wäre das (Int) => (Int). Das bedeutet so viele wie: Nimm einen Int als Eingabe und gebe wieder einen Int aus. Die Syntaxregel für diese Schreibweise lautet:

(<input_param1>, <input_param2>, <input_paramN>) => (<output_param1>, <output_param2>, <output_paramN>)

Die Schreibweise der Ein- und Ausgabeparameter gleicht der von Tupeln. In runden Klammern werden die einzelnen Parameter durch Kommas voneinander getrennt. Tatsächlich handelt es sich aber nur bei den Ausgabeparametern um Tupel, erwarten tut eine Funktion keine Tupel, sondern mehrere Parameter, wie bei einer Methode. Besonders praktisch ist noch, dass wir bei einem einzigen Parameter die runden Klammern weglassen können:

val mul: (Int, Int) => Int = {
  (i: Int, j: Int) => i*j
}
val twice: Int => (Int, Int) = {
  (i: Int) => (i, i)
}

scala> mul(3, 4)
res30: Int = 12

scala> mul.apply(3, 4)
res31: Int = 12

scala> val t = (3, 4)
t: (Int, Int) = (3,4)

scala> mul(t)
:10: error: not enough arguments for method apply: (v1: Int, v2: Int)Int in trait Function2.
Unspecified value parameter v2.
              mul(t)
                 ^

scala> twice(3)
res1: (Int, Int) = (3,3)

scala> val (x, y) = twice(3)
x: Int = 3
y: Int = 3

Versuchen wir einen Tupel an unsere Funktion zu übergeben, so erhalten wir eine Fehlermeldung. Der Aufruf der apply-Methode ist ein weiteres Indiz darauf, dass Funktionen gewöhnliche Objekte sind und bei ihrem Aufruf von Scalas Syntaxzucker profitieren. Wollen wir nun einen Block zur Ausführung an eine Funktion binden, so müssen wir diesen durch notieren eines =>-Symbols auch entsprechend als Funktionskörper kennzeichnen. Wir benötigen ihn um die Eingabeparameter einer Funktion an Variablen binden zu können. Das Literal (i: Int, j: Int) => bedeutet also: Binde den ersten Eingabeparameter an die Variable i, den zweiten an die Variable j und mache diese Variablen auf der rechten Seite verfügbar. Dank Scalas Typinferenz müssen wir den Typ dieser Variablen aber nur seltenst angeben, in unserem Fall können wir sie weglassen.

val mul: (Int, Int) => Int = {
  (i, j) => i*j
}

Existiert nur ein Kommando im Block, so können wir auch die geschweiften Klammern auslassen:

val mul: (Int, Int) => Int = (i, j) => i*j

Auf den ersten Blick sieht das alles sehr abenteuerlich aus und dürfte die Frage aufkommen lassen wofür man das zum Teufel nochmal benötigt. Was für einen Vorteil sollte man schon haben wenn man statt einer Methode eine Funktion zur Abarbeitung des Codes nutzt?

Die Antwort darauf ist kurz aber unverständlich: Es dient zur Abstraktion. Toll, zu welcher Abstraktion? Dies ist die weitaus wichtigere Frage wenn man verstehen will wofür Paradigmen gebraucht werden. Wichtig ist nicht was sie abstrahieren und wie sie es tun, sondern welchen konkreten Vorteil man von deren Einsatz hat.

Um zu erklären welche Vorteile es hat Funktionen einzusetzen, möchte ich zu einem Beispiel aus der imperativen Programmierwelt greifen.

def extensiveOp() = { Thread.sleep(2000); 1 }
def anotherExtensiveOp() = { Thread.sleep(2000); 2 }

def executeWhenNecessary(cond: Boolean, f: Int) =
  if (cond) f else 0

Der Code sollte nicht schwer zu verstehen sein. Wir wollen eine teure Operation nur dann ausführen wenn eine bestimmte Bedingung wahr ist. Übergeben wir die auszuführende Methode, dann erzielen wir aber nicht den gewünschten Effekt. Nicht die Methode wird übergeben, sondern deren Rückgabewert.

scala> executeWhenNecessary(false, extensiveOp)

res10: Int = 1

Nun stellt sich die Frage wie wir dieses Problem am geschicktesten lösen. Eine Möglichkeit wäre anstatt der Methode ein Enum zu übergeben, anhand dessen wir dann innerhalb der Methode die richtige Methode zur Abarbeitung auswählen können:

object Op extends Enumeration {
  val Extensive, AnotherExtensive = Value
}
import Op._

// other methods as before

def executeWhenNecessary(cond: Boolean, op: Op.Value) =
  if (cond) op match {
    case Extensive => extensiveOp()
    case AnotherExtensive => anotherExtensiveOp()
  }
  else 0

Nun funktioniert der Code auch korrekt:

scala> executeWhenNecessary(false, Extensive)

res11: Int = 0

Obige Vorgehensweise birgt aber einige Nachteile. Wir benötigen zum einen einen Enum und müssen diesen umständlich importieren und adressieren. Zum anderen ist unser Code nicht mehr skalierbar. Ändern wir etwas am Enum oder an den auszuführenden Methoden, so müssen wir auch die Methode executeWhenNecessary ändern.
Eine andere Lösung geht über die Ausführung einer gewrappten Methode:

trait Operation {
  def apply(): Int
}

// other methods as before

def executeWhenNecessary(cond: Boolean, op: Operation) =
  if (cond) op() else 0

Bei dieser Lösung benötigen wir einen Wrapper, der den Code beinhaltet, der ausgeführt werden soll.

executeWhenNecessary(false, new Operation {
  def apply() = extensiveOp()
})
// this code will return immediately

Im Gegensatz zur vorherigen Lösung über ein Enum haben wir aber einen entscheidenden Vorteil: Skalierbarkeit. Wollen wir später etwas ändern ist dies kein Problem, da wir nur den Inhalt des Wrappers ändern müssen und sonst nichts. Ein weiterer Vorteil gegenüber Enums ist, dass wir den Code parametrisieren können:

trait Operation[A] {
  def apply(): A
}

// other methods as before

def executeWhenNecessary[A](cond: Boolean, op: Operation[A]): Option[A] =
  if (cond) Some(op()) else None

Nun können wir beliebige Objekte zurückgeben:

executeWhenNecessary(false, new Operation[String] {
  def apply() = "hello world"
})
executeWhenNecessary(false, new Operation[Int] {
  def apply() = 10
})

Wir haben sogar die Möglichkeit eine unterschiedliche Anzahl an Übergabeparametern festzulegen:

trait Operation0[Ret] {
  def apply(): Ret
}
trait Operation1[A, Ret] {
  def apply(a: A): Ret
}
trait Operation2[A, B, Ret] {
  def apply(a: A, b: B): Ret
}

def execute0[Ret](op: Operation0[Ret]) = op()
def execute1[A, Ret](a: A)(op: Operation1[A, Ret]) = op(a)
def execute2[A, B, Ret](a: A, b: B)(op: Operation2[A, B, Ret]) = op(a, b)

// and so on...

Der Einsatz dieser Wrapper erklärt sich von selbst:

//for Operation0

scala> execute0(new Operation0[String] { def apply() = "hello world" })
res27: String = hello world

//for Operation1

val reverse = new Operation1[String, String] { def apply(str: String) = str.reverse }

scala> execute1("hello")(reverse)
res26: String = olleh

// for Operation2

val add = new Operation2[Int, Int, Int] { def apply(i: Int, j: Int) = i+j }
val multiply = new Operation2[Int, Int, Int] { def apply(i: Int, j: Int) = i*j }

scala> execute2(8, 3)(add)
res22: Int = 11

scala> execute2(8, 3)(multiply)
res23: Int = 24

def add(i: Int, j: Int) = execute2(i, j)(new Operation2[Int, Int, Int] { def apply(i: Int, j: Int) = i+j })
def multiply(i: Int, j: Int) = execute2(i, j)(new Operation2[Int, Int, Int] { def apply(i: Int, j: Int) = i*j })

scala> add(8, 3)
res28: Int = 11

scala> multiply(8, 3)
res29: Int = 24

Je nach eingesetzter OperationX und mit Hilfe von sogenanntem Currying können wir unterschiedliche Operationen ausführen.

Currying bezeichnet ein Verfahren bei dem Funktionen mit mehreren Parameterlisten miteinander so verkettet werden, sodass am Ende eine Funktion mit nur einer Parameterliste übrig bleibt (wie gezeigt an den add und multiply Methoden). Oben haben wir zwar Methoden anstatt Funktionen zur Verkettung eingesetzt, aber das kann man durchaus auch als Currying durchgehen lassen.

Tolle Abstraktion, da versteht man ja gar nichts mehr. Das oder so etwas ähnliches werdet ihr jetzt vermutlich denken. Und dem stimme ich euch auch voll zu. Das Problem ist nämlich, dass wir viel zu viel syntaktischen Overhead haben. Wir müssen einen OperationX erstellen, deren Typparameter angeben und die apply-Methode implementieren. Was für ein Aufwand. Geht das nicht einfacher?

Natürlich geht es einfacher, sonst würde ich euch das hier nicht erklären. In der Scala-Lib gibt es bereits vordefinierte Objekte namens FunctionX, die gleich wie unsere OperationX-Objekte funktionieren.

def execute2[A, B, Ret](a: A, b: B)(f: Function2[A, B, Ret]) = f(a, b)

def add(i: Int, j: Int) = execute2(i, j)(new Function2[Int, Int, Int] { def apply(i: Int, j: Int) = i+j })
def multiply(i: Int, j: Int) = execute2(i, j)(new Function2[Int, Int, Int] { def apply(i: Int, j: Int) = i*j })

scala> add(8, 3)
res33: Int = 11

scala> multiply(8, 3)
res34: Int = 24

Somit entfällt schon einmal die Definition der OperationX-Objekte. Das ist aber nicht alles. In Scala ist FunctionX ein Synonym für ein Funktionsliteral. Genau genommen ist ein Funktionsliteral syntaktischer Zucker zur Erstellung von Funktionen. Wir können das obige Beispiel also auch so schreiben:

def execute2[A, B, Ret](a: A, b: B)(f: (A, B) => Ret) = f(a, b)

def add(i: Int, j: Int) = execute2(i,j)((i, j) => i+j)
def multiply(i: Int, j: Int) = execute2(i,j)((i, j) => i*j)

scala> add(8, 3)
res35: Int = 11

scala> multiply(8, 3)
res36: Int = 24

Das sieht doch gleich viel besser aus. Aber ist das jetzt die versprochene Abstraktion? Unser Code ist ein wenig skalierbarer geworden. Wir können die Funktionsweise der executeX-Methoden ändern, ohne dass wir die darauf aufbauenden Methoden ändern müssten. Ebenso haben wir eine komfortable Möglichkeit kennen gelernt wie wir Code verzögert ausführen können (Erinnerung an die executeWhenNecessary-Methode). Aber das dürfte euch noch zu wenig sein. Setzen wir also noch eins drauf und führen noch mehr Syntaxzucker ein.

Unterstrich-Platzhalter

Gegeben sei nach wie vor eine execute-Methode:

def execute[A, B, Ret](a: A, b: B)(f: (A, B) => Ret) = f(a, b)

Sie soll weiterhin als Platzhalter für beliebigen Code dienen, den wir statt dessen ausführen könnten. Die Erstellung einer darauf aufbauenden Methode wie add oder multiply ist nicht schwer, kann aber noch weiter vereinfacht werden:

def add(i: Int, j: Int) = execute(i,j)(_+_)
def multiply(i: Int, j: Int) = execute(i,j)(_*_)

scala> add(8, 3)
res37: Int = 11

scala> multiply(8, 3)
res38: Int = 24

Der Unterstich dient uns – wie schon bei vielem in Scala – als Platzhalter für ein Funktionsargument. Wir können also immer so viele Funktions-Platzhalter verwenden wie wir Argumente haben. Eine Übersicht:

_ = a
_ _ = (a, b)
_ _ _ = (a, b, c)
usw.

Hat man sich erst einmal an die Platzhalter-Schreibweise gewöhnt, so ist diese sehr gut lesbar. (_+_) kann man als „Erzeuge eine Funktion und addiere zu deren ersten Parameter den zweiten Parameter“ lesen und ist die schon fast kürzeste Schreibweise, die es gibt. Noch kürzer ist nur noch (+), also eine Notation bei der die Unterstriche komplett fehlen, aber diese Schreibweise würde einen Fehler verursachen. Sie bedeutet nämlich nicht (_+_), sondern +(_, _). Wir können das mit folgender Funktion ausprobieren:

def doubleAdd(i: Int, j: Int) = 2*execute(i, j)((i, j) => add(i, j))
def doubleAdd(i: Int, j: Int) = 2*execute(i, j)(add(_, _))
def doubleAdd(i: Int, j: Int) = 2*execute(i, j)(add)

scala> doubleAdd(8, 3)
res40: Int = 22

Wie zu sehen funktioniert das Platzhalter-Symbol nicht nur für Funktionen sonder auch für die Parameter einer normalen Methode. Wichtig ist dabei, dass mit dem Platzhalter-Symbol nicht die Reihenfolge der Parameter geändert werden kann. Das bedeutet, dass ein (_+_) niemals (a, b) => b+a heißen kann. Wollen wir die Parameter umdrehen müssen wir also auf die Platzhalter-Schreibweise verzichten.

Ich möchte euch die Regeln, wann und wo genau ein Unterstrich verwendet werden darf, jetzt aber nicht näher erläutern, das werde ich in einem der der weiteren Artikel nachholen. Jetzt ist wichtig, dass ihr die Schreibweise mal gesehen habt, damit ihr damit etwas anfangen könnt.

Diese Schreibweise ist zwar schön kurz, aber selbst wenn ihr sie jetzt schon komplett verstanden habt dürfte euch noch immer nicht klar sein wie man damit abstrahiert programmieren kann. Gehen wir also zum nächsten Kapitel weiter, in dem ich euch ein praktisches Beispiel zeigen werde an dem ihr erkennen könnt wozu sich Funktionen so alles einsetzen lassen.

Advertisements

3 comments so far

  1. Aiman on

    Der like Button wuerde sich gut im Blog machen, oder habe ich ihn uebersehen?

    • antoras on

      Am Ende eines jeden Artikels gibt es ein paar Buttons um die Inhalten zu teilen/liken. Genügen die deinen Ansprüchen?

  2. toronto small business seo on

    Generally I do not read article on blogs, but I wish to say that this write-up very pressured me to take a look at and do so! Your writing taste has been surprised me. Thank you, quite nice article.


Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s

%d Bloggern gefällt das: