Die gängigste Art eine Funktion zu definieren ist diese als Bestandteil eines Objektes zu definieren. Derartige Funktionen werden auch als Methode bezeichnet1. Da Scala eine objektorientierte Sprache ist, sind Funktionen, welche nicht an ein Objekt gebunden sind (sie sind dementsprechend keine Methode) selber wiederum Objekte.
Bedingungen, die eine Funktion erfüllen muss um als Funktion zu gelten sind:
Unit
)
Die Deklaration einer Funktion beginnt mit dem Schlüsselwort def
gefolgt vom Namen der Funktion. Nach dem Namen folgt in Runden Klammern die Parameterliste
der Funktion.
Hinter den Runden Klammern wird nach einem Doppelpunkt der Rückgabetyp
der Funktion definiert. Abschließend folgt nach einem Gleichheitszeichen
der Inhalt der Funktion, welcher bei mehreren Anweisungen durch geschweifte Klammern
zu einer Blockanweisung zusammengefasst wird.
def test(v: Int):Int={ println(v) return v+1 }
In Scala braucht das return
Statement nur angegeben zu werden,
wenn ohne diese Anweisung weiter Code ausgeführt würde. Der
Rückgabewert einer Funktion ist immer das Ergebnis des zuletzt ausgeführten
Ausdrucks. Demnach ist die nachfolgend aufgeführte Funktion test2
equivalent zur vorherigen Funktion test
.
def test2(v: Int):Int={ println(v) v+1 }
Besteht eine Funktion nur aus einer Anweisung, können wir die geschweiften Klammern des Methodenrumpfs auch weglassen. Dadurch lassen sich einfache Funktionen lesbarer und in kürzerer Schreibweise darstellen.
def simpleFunction(v1: Int, v2: Int) : Int = (v1 + v2) * 3
In Scala hat jede Funktion einen Rückgabewert. Funktionen, deren Rückgabewert vom Typ () "Unit" ist, werden auch als Prozedur (engl. procedure) bezeichnet. Der einzige Sinn des Aufrufs einer Prozedur ist die Ausführung von Seiteneffekten. Prozeduren sind demnach nicht "funktional programmiert".
Funktionen, die andere Funktionen als Argument erwarten oder als Rückgabewert haben, werden als Funktionen höherer Ordnung (engl. higher-order functions) bezeichnet.
Ist eine Funktion an ein Objekt gebunden (also eine Methode) erfolgt deren Aufruf durch Angabe des Objektes gefolgt von einem Punkt, worauf die aufzurufende Funktion angegeben wird.
objectName.methodName(argument)
Möchten wir eine Methode eines Singleton- oder Companion Objektes (dt. Begleitobjekt) aufrufen, setzen wir anstatt der Instanzvariable den Objektnamen. Dieser Objektname repräsentiert das einzig bestehende Objekt diesen Typs.
SingletonName.methodName(argument)
In Scala gilt die Regel, dass der Punkt zwischen Objekt und Methode nicht angegeben werden muss. Dies kann zu einer "natürlicheren" Schreibweise beitragen, die auch das nachträgliche Lesen vereinfacht. Die nachfolgende Aufrufart ist demnach auch zulässig:
objectName methodName(argument)
Eine weitere Regel in Scala ist, das wenn eine Methode ein oder kein Argument hat, dann kann die dem Argument umgebene Klammer weggelassen werden. Enthält eine Methodensignatur mehr als ein Argument, kann zwar der Punkt weggelassen werden, aber die Klammern müssen angegeben werden.
objectName methodName argument
Die zuletzt beschriebene Aufrufweise wird als Operatorschreibweise bezeichnet. Tatsächlich gibt es in Scala keine Operatoren. Sprachkonstrukte, die wie Operatorenaufrufe aussehen, sind Methoden-/Funktionsaufrufe in Operatorschreibweise.
Zum Einstieg zu Setter und Getter in Scala sehen wir uns zunächst folgenden Quelltext an.
class A { var b = new B() println(b.value) b.value = "321" println(b.value) } class B (){ var value = "123" }
Es hat den Anschein als würde in Klasse A direkt auf das var value
zugegriffen, was aber nicht der Fall ist. Der Scala Compiler hat automatisch
einen Setter und einen Getter für value
generiert.
Die Getter-Methode hat den gleichen Namen wie die zugeordnete Variable. Die Setter-Methode hat im obigen Beispiel folgende Form:
def value_=(s: String)
Das Vorhandensein des Setters kann verdeutlicht werden, indem wir ihn explizit definieren:
class A { var b = new B() println(b.value) b.value = "321" println(b.value) } class B (){ var value = "123" def value_=(s: String) }
Versuchen wir diesen Quelltext zu kompilieren erhalten wir folgende Fehlermeldung:
C:\test>scalac Test.scala A.scala:4: error: ambiguous reference to overloaded definition, both method value_= in class B of type (x$1: java.lang.String)Unit and method value_= in class B of type (s: String)Unit match argument types (java.lang.String) b.value = "321" ^ A.scala:11: error: method value_= is defined twice def value_=(s: String) ^ two errors found
In vielen Fällen möchten wir jedoch beim Aufruf eines Setters oder
Getters eingreifen und weitere Aktionen veranlassen. Für diesen Fall
gehen wir einfach wie folgt vor. Wir definieren zunächst eine Variable,
welchen den Wert speichern soll. Im nächsten Schritt definieren wir den
Setter und den Getter in der Form, wie diese vorher automatisch generiert wurden.
Ein weiteres Eingreifen an anderen Stellen des Quelltextes eines Programms
ist nicht notwendig, da die Setter und Getter genauso vorliegen, wie es
bei den automatisch generierten der Fall war. Im nachfolgenden Beispiel
ist in Klasse B
die beschriebene Vorgehensweise angewendet worden.
class A { var b = new B() println(b.myValue) b.myValue = "321" println(b.myValue) } class B (){ private[this] var _myValue = "123" def myValue_= (s: String){ println("setter Method called") _myValue = s } def myValue : String = _myValue }
Der letzte (oder einzige) Parameter der Parameterliste einer Funktion kann als wiederholbar mit variabler Länge gekennzeichnet werden. Dies erlaubt, dass dieser Parameter einmal, keinmal oder beliebig oft im Funktionsaufruf angegeben werden kann. Die Kennzeichnung erfolgt, indem wir dem Parametertyp ein Asterisk (Sternchen) anhängen. Innerhalb der Funktion greifen wir auf den entsprechenden Parameter über ein Array des entsprechenden Typs zu. Das nachfolgende Beispiel zeigt zwei Funktionen mit variabler Parameterliste.
object MainObject { def main(args: Array[String]) { val a = new A() a.demo1(1,2,3,4,5) a.demo1(6) a.demo2() a.demo2(7,8) a.demo2(9,10,11) } } class A{ def demo1(value1: Int, value2: Int*) : Unit = { println(value1) value2.foreach(println) } def demo2(value1: Int*) : Unit = value1.foreach(println) }
Die Ausgabe des Programmes liefert:
1 2 3 4 5 6 7 8 9 10 11
Auch wenn wir innerhalb der Funktion auf die variablen Parameter über
ein Array zugreifen, können wir der Funktion kein Array (oder Seq
z.B. List
)
direkt übergeben. Um den Compiler mitzuteilen, jedes Element einer Seq
oder eines Arrays als einzelne Parameter zu verwenden, verwenden wir die
Syntax: variablenName: _*
in der Parameterliste.
Das nachfolgende Beispiel zeigt die Vorgehensweise:
object MainObject { def main(args: Array[String]) { val a = new A() val arr: Array[Int] = Array(1,2,3) val list: List[Int] = List(4,5,6) a.demo1(0,arr: _*) a.demo2(list: _*) } } class A{ def demo1(value1: Int, value2: Int*) : Unit = { println(value1) value2.foreach(println) } def demo2(value1: Int*) : Unit = value1.foreach(println) }
Die Ausführung des Programmes führt zu folgender Ausgabe auf der Systemausgabe:
0 1 2 3 4 5 6
Methoden sind in Scala nicht auf eine Parameterliste beschränkt. Vielmehr können wir beliebig viele Parameterlisten definieren. Das nachfolgende Beispiel zeigt eine Funktion mit 3 Parameterlisten und deren Aufruf.
def more(i: Int, j: Int)(k: Int)(m: Int): Int = i + j + k * m def main(args: Array[String]): Unit = { println(more(1,2)(3)(4)) }
Weisen wir einer Variablen das Ergebnis einer Funktion zu und definieren nicht alle Parameterlisten, so erhalten wir eine neue Funktion mit den fehlenden Parameterlisten. Derartige Funktionen bezeichnen wir als "partiell angewendete Funktion" (engl. partial applied function). Um klarzustellen, dass wir eine derartige Funktion haben möchten, geben wir nach der zuletzt definierten Parameterliste einen Unterstrich an. Anstelle der Variablendefinition können wir die partiell abgeleitete Funktion auch direkt als Funktion definieren.
Das nachfolgende Beispiel soll die Definition partiell angewendeter Funktionen, einmal als Variablendefinition und einmal als Funktionsdefinition, zeigen.
object Partially { def myFunction(a: Int)(b: Int)(c: Int): Int = { println("Hallo") a + b - c } val myPartial1 = myFunction(3)_ def myPartial2 = myFunction(4)_ def main(args: Array[String]): Unit = { println(myPartial1(3)(1)) println(myPartial2(5)(1)) } }
Eine Funktion liegt in "currysierter" Form vor, wenn ihre Parameter einzeln in mehreren Parameterlisten (sofern mehrere Parameter vorhanden sind) vorliegen.
def imCurried(a: Int)(b: Int)(c: Double): Double = a * b * c
Hat eine der Parameterlisten mehr als einen Parameter, liegt die entsprechende Funktion nicht in "currysierter" Form vor.
Das "Curry Prinzip" geht zurück auf ein Konzept des amerikanischen Mathematikers Haskell Brooks Curry und ermöglicht uns die Definition eigener Kontrollstrukturen. Nach Haskell Brooks Curry ist im Übrigen die funktionale Programmiersprache Haskell benannt.
Closures sind ein Element funktionaler Programmiersprachen. Ein Closure ist dabei ein Element, dass Zugriff auf seinen Erzeugungskontext hat.
Im nachfolgenden Quelltextbeispiel erzeugt die Funktion adderMaker
ein Closure.
def adderMaker(value: Int) = (x: Int) => value + x
Das hier erzeugte Closure ist die Funktion:
(x: Int) => value + x
Der Zugriff auf den Erzeugungskontext geschieht im obigen Beispiel die Variable value
,
welche bei der Erzeugung des Closures festgelegt wird. Um mit dem Closure arbeiten zu können,
ist es nicht notwendig, dass value
im Sichtbarkeitsbereich
der aktuellen Quelltextstelle liegt.
Im nächsten Beispiel sehen wir uns Closures in einer kleinen Anwendung an.
object MyClosureApp { def main(args: Array[String]): Unit = { new MyClosureApp } } class MyClosureApp { def adderMaker(value: Int) = (x: Int) => value + x val adder1 = adderMaker(3) val adder2 = adderMaker(40) val adderHandler = new AdderHandler() adderHandler.handleAdder(adder1) adderHandler.handleAdder(adder2) } class AdderHandler { def handleAdder(f: Int => Int) = println(f(2)) }
Im Beispiel haben wir ein Objekt object MyClosureApp
, welche die Anwendung startet
und eine Klasse des Typs class MyClosureApp
erzeugt. Innerhalb der Klasse definieren
wir die uns bekannte adderMaker
Funktion, um Closures erzeugen zu können.
Im Anschluss erzeugen wir zwei Closures adder1
und adder2
mit zwei
unterschiedlichen Werten für value
als Erzeugungskontext.
Die letzte Klasse AdderHandler
stellt eine Funktion handleAdder
zur Verfügung, welche eine
Funktion Int => Int
als Argument erwartet. Zum Abschluss der Klasse MyClosureApp
erzeugen wir eine Instanz von AdderHandler
und übergeben unsere Closures zur
Ausgabe von Werten an die Methode adderHandler
.
Die Ausführung des Programms führt zu folgender Ausgabe auf der Systemausgabe:
5 42
In Scala können die Argumente einer Funktion, wie bei Konstruktoren, über Ihren Namen übergeben werden. Werden die Argumente mit Ihrem Namen übergeben, ist die Reihenfolge, in der die Argumente angegeben werden, nicht mehr von Bedeutung. Das nachfolgende Beispiel zeigt die Anwendung benannter Argumente.
object NamedArguments { def main(args: Array[String]) { doOutput(1,2) doOutput(x = 1, y = 2) doOutput(y = 1, x = 2) } def doOutput(x: Int, y: Int) : Unit = { println("x = " + x) println("y = " + y) } }
Der Ablauf des Programmes führt zu folgender Ausgabe auf der Systemausgabe:
x = 1 y = 2 x = 1 y = 2 x = 2 y = 1
Es ist auch möglich, eine Mischung aus Parameterangabe nach Parameterreihenfolge und benannten Argumenten zu machen. Wurde jedoch ein Argument benannt übergeben, müssen alle weiteren Parameter benannt übergeben werden. Das nachfolgende Programm zeigt beispielhaft diese Möglichkeit.
object NamedArguments2 { def main(args : Array[String]) { doOutput(1,z = 2,y = 3) } def doOutput(x: Int, y: Int, z: Int) { println("x = "+x) println("y = "+y) println("z = "+z) } }
Die Ausführung des Programmes führt zu folgender Ausgabe auf der Systemausgabe:
x = 1 y = 3 z = 2
In Scala ist es möglich für Argumente einer Funktion Standardwerte, wie bei Konstruktoren, festzulegen. Wird ein Argument beim Aufruf einer Funktion nicht angegeben, so wird der Standardwert verwendet. Das nachfolgende Beispiel gibt einen ersten Eindruck zur Verwendung von Standardargumenten.
object StandardArgumente { def main(args: Array[String]) { standard1() standard1(3) standard1(4,5.0) } def standard1(v1: Int = 1, v2: Double = 2.0) { println(v1+" "+v2) } }
Auch wenn man Argumente mit Standardwerten verwendet, müssen nicht alle Argumente mit Standardwerten belegt werden. Es ist problemlos möglich, Argumente mit Standardwerten und Argumente ohne Standardwerte zu vermischen. Findet eine Mischung statt, so ist darauf zu achten, dass die Standardargumente möglichst am Ende der Argumentenliste definiert werden. Ist dies nicht der Fall, kommen die Standardwerte (außer beim Einsatz benannter Argumente) nicht zum Einsatz.
object StandardArgumente2 { def main(args: Array[String]) { standard2(1) standard2(2,3.0) standard2(3,4.0,false) } def standard2(arg1: Int, arg2: Double = 2.0, arg3: Boolean = true) { println(arg1+" "+arg2+" "+arg3) } }
Funktionen müssen nicht unbedingt an ein Objekt gebunden sein. Funktionen können auch direkt an der Stelle definiert werden, an denen Sie im Quelltext gebraucht werden. Derartige Funktionen sind Funktionsliterale in der Form2:
(x: Int, y: Int) => x + y
Funktionen sind in Scala vollwertige Elemente. Da Scala eine "vollständig" objektorientierte Sprache ist, sind Funktionen in Scala Objekte. Funktionen können unter anderem
Nachfolgend ein Beispiel einer höheren Funktion, welche eine Funktion als Argument in der Parameterliste erwartet.
def higherOrder(myFunction: (Int) => Int) = {}
Eingeleitet wird diese Funktion höherer Ordnung wie gewohnt mit dem Schlüsselwort def
gefolgt vom Namen der Funktion higherOrder
. In den Klammern hinter dem Namen
folgt als Parameter eine Funktionsdefinition myFunction: (Int) => Int
.
Hierbei ist myFunction
der Name der Funktion, wie Sie innerhalb von higherOrder
angesprochen werden kann. Wie bei Variablen muss der Name der Funktion nicht mit dem Namen
der übergebenden Funktion übereinstimmen. Nun folgt in Klammern die Parametersignatur-Definition,
welche die übergebene Funktion aufweisen muss. Nach dem recht gerichteten Pfeil =>
folgt die Ergebnisdefinition, welche mit dem Ergebnistyp der übergebenen Funktion übereinstimmen muss.
Das Gleichheitszeichen und die geschweiften Klammern runden das Beispiel zu einer vollständigen
Funktionsdefinition ab.
Im nachfolgenden Beispiel wollen wir unsere Funktion derart erweitern, dass mit der übergebenen Funktion innerhalb der Funktion höheren Ordnung etwas geschieht. Im Beispiel geben wir einfach das Ergebnis der übergebenen Funktion auf der Systemausgabe aus.
def higherOrder(myFunction: (Int) => Int) { println(myFunction(3)) }
Und nun noch ein vollständiges Programm, wo zwei unterschiedliche Funktionen unserer Funktion höherer Ordnung übergeben werden.
object GettingHigher { def main(args:Array[String]) { def function1 = (x: Int) => 3 * x def function2 = (y: Int) => 4 * y higherOrder(function1) higherOrder(function2) } def higherOrder(myFunction: (Int) => Int) { println(myFunction(3)) } }
Die Ausführung des Programms führt zu folgender Ausgabe auf der Systemausgabe:
9 12
Besteht die Parameterliste der zu übergebenen Funktion aus nur einem Parameter, so kann die Klammer um den Parameter auch weggelassen werden.
def higherOrder(myFunction: Int => Int) = {}
Enthält die Parametersignatur der zu übergebenen Function mehrere Parameter, so sind diese einfach durch ein Komma getrennt aufzulisten.
def higherOrder(myFunction: (Int,Double,String) => Int) = {}
Das Objekt scala.Predef
(API ) enthät
6 Methoden (3 * 2 Signaturen) die zur Prüfung der Datenkonsistenz eingesetzt werden können:
final def assert(assertion: Boolean, message: ? Any): Unit
def assert(assertion: Boolean): Unit
final def assume(assumption: Boolean, message: ? Any): Unit
def assume(assumption: Boolean): Unit
final def require(requirement: Boolean, message: ? Any): Unit
def require(requirement: Boolean): Unit
Die Hauptaufgabe aller Methoden ist grundsätzlich gleich. Sie prüfen ob eine Bedingung
gegeben ist und werfen im negativem Falle ein Throwable
Objekt. assert
und assume
werfen im negativen Fall ein AssertionError
wohingegen
require
eine IllegalArgumentException
wirft.
Stellt sich nun die Frage, warum wir drei verschieden Methoden benötigen um eine Bedingung zur Laufzeit zu prüfen. Der Unterschied der Methoden liegt im Grund (warum der Programmierer prüft) des Aufrufs.
assert
: Mit assert soll das eigentliche Programm (die Bibliothek) auf Korrektheit geprüft werden.assume
: Prüft das Programm wie assert
auf Korrektheit, wobei hier Axiome geprüft werden sollen.
require
: Mit require
sollen Argumente eines Nutzers des Programmes (Bibliothek) auf
Zulässigkeit geprüft werden.
Zusammenfassend kann gesagt werden, dass wenn assert
oder assume
negativ
ausgewertet werden ein Fehler im Programm (Bibliothek) vorliegt. Wird require
negativ
ausgewertet so ist das Programm (Bibliothek) in Ordnung, nur die übergebenen Argumente sind
nicht zulässig.
Die Methoden assert
, assume
und require
kommen jeweils in 2 Ausprägungen.
Einmal, wo nur die zu prüfende Bedingung übergeben wird und einmal, wo zusätzlich zur Bedingung
noch eine "Message" übergeben wird, die im negativen Fall der Bedingung mit dem Throwable
Objekt ausgegeben wird.
Nachfolgend ein Beispielprogramm zur Verwendung von assert
.
object MyFirstAssertion{ def main(args: Array[String]): Unit = { val value1 = 42 val value2 = 43 assert(value1 == value2) } }
Die Ausführung des Programms führt zu folgender Ausgabe auf der Systemausgabe:
Exception in thread "main" java.lang.AssertionError: assertion failed at scala.Predef$.assert(Predef.scala:165) at test.MyFirstAssertion$.main(Test.scala:8) at test.MyFirstAssertion.main(Test.scala)
Im nachfolgenden Beispiel nehmen wir die gleiche Prüfung nochmals vor mit
der Erweiterung, dass wir assert
eine Message übergeben, die
im negativen Fall ausgegeben werden soll.
object MyFirstAssertion{ def main(args: Array[String]): Unit = { val value1 = 42 val value2 = 43 assert(value1 == value2, "value1 = "+value1+"; value2 = "+value2) } }
Die Ausführung des Programms führt zu folgender Ausgabe auf der Systemausgabe:
Exception in thread "main" java.lang.AssertionError: assertion failed: value1 = 42; value2 = 43 at scala.Predef$.assert(Predef.scala:179) at test.MyFirstAssertion$.main(Test.scala:8) at test.MyFirstAssertion.main(Test.scala)
Die nächste Variante verwendet anstatt assert
require
um die Datenkonsistenz zu prüfen.
Exception in thread "main" java.lang.IllegalArgumentException: requirement failed: value1 = 42; value2 = 43 at scala.Predef$.require(Predef.scala:233) at test.MyFirstRequire$.main(Test.scala:8) at test.MyFirstRequire.main(Test.scala)
Die Ausführung des Programms führt zu folgender Ausgabe auf der Systemausgabe:
Exception in thread "main" java.lang.IllegalArgumentException: requirement failed: value1 = 42; value2 = 43 at scala.Predef$.require(Predef.scala:233) at test.MyFirstRequire$.main(Test.scala:8) at test.MyFirstRequire.main(Test.scala)
Nicht immer wollen wir, dass unsere Funktionen/Methoden (ab jetzt kurz nur Methoden) überall
sichtbar (erreichbar) sind. Um die Sichtbarkeit der Methoden einzuschränken helfen uns, wie bei
Konstruktoren, die Sichtbarkeitsmodifizierer
private
und protected
.
Dem Thema Sichtbarkeit ist aug ScalaTutorial.de ein eigenes Kapitel gewidmet.
Grundsätzlich kennen wir in Scala drei Ebenen der Sichtbarkeit:
protected
private
public:
Der Sichtbarkeitsbereich "public" (dt. öffentlich) ist in Scala Standard. Sofern keine
Einschränkung mit private
oder protected
vorgenommen wird, ist die
Sichtbarkeit der Methode öffentlich. Ein separate Sichtbarkeitsmodifizierer für
öffentlich, ist in Scala nicht vorgesehen.
protected:
Der Sichtbarkeitsbereich protected
(dt. geschützt) beschränkt den Sichtbarkeitsbereich
der Methode auf die eigene Klasse und auf alle Klassen die von dieser Klasse abgeleitet sind.
private:
Der Sichtbarkeitsbereich private
(dt. Privat) beschränkt die Sichtbarkeit
auf die eigene Klasse.
Die Sichtbarkeitsbereiche für protected
und private
können noch feiner
definiert werden.