Wie würdet ihr die Fakultätsfunktion, das Hallo-Welt-Programm der funktionalen Programmierung, naiv in Scala implementieren? Ungefähr so?
def fac(n) = if (n <= 1) 1 else n*fac(n-1)
Leider reicht das nicht. Die Typinferenz von Scala ist ziemlich schwach. Man muß deshalb den Typen des Arguments sowie den Rückgabetypen der Funktion angeben (Letzterer wird normalerweise automatisch festgestellt, bei rekursiven Definitionen geht das aber nicht):
def fac(n: Int):Int = if (n <= 1) 1 else n*fac(n-1)
Damit ist fac
zwar definiert, aber nicht so allgemein wie möglich. Für BigInts beispielsweise wird die Funktion nicht funktionieren.
Man könnte nun auf die Idee kommen, etwas ähnliches wie das Folgende zu schreiben:
def fac(n: Number):Number = if (n <= 1) 1 else n*fac(n-1)
Das schlägt aber leider gleich doppelt fehl, weil einerseits nicht alle Zahlen von Number
erben, und Number
andererseits nicht die üblichen arithmetischen Operatoren und Größenvergleiche bereitstellt. Es wäre auch unflexibler als in Haskell, da man dort a posteriori eine sogenannte Typklasse implementieren kann, womit der Typ der Funktion in Haskell wie folgt aussieht:
fac :: Number a => a -> a
Das heißt: Wir erhalten ein Objekt vom Typen a und geben eines vom Typen a zurück, und zwar für jeden Typen, der die Number
-Operationen zur Verfügung stellt. Man beachte, daß der Typ genauer ist als der in der obigen Scala-Funktion, welche uns nur garantiert, daß sie uns irgendeine Zahl zurückgibt, aber nicht, von welchem Typ. Das liegt daran, daß die Haskell-Version von fac
über den Typen a parametrisiert ist.
Nun gibt es solche Typparameter auch in Scala. Wir würden gerne etwas hinbekommen wie:
def (Numeric[T]) fac[T](n: T):T = if (n <= 1) 1 else n*fac(n-1)
Leider können wir das so nicht schreiben, weil Scala keine Typklassen kennt, aber etwas anders bekommen wir es hin:
def fac[T](n: T)(implicit num: Numeric[T]):T = {
import num.{mkNumericOps, mkOrderingOps}
if (n <= num.one)
num.one
else
n * fac(n - num.one)
}
Was hierbei geschieht, grenzt an dunkle Magie. Es wird ein impliziter Parameter num
eingeführt, der vom Compiler auf der Seite des Aufrufenden automatisch eingesetzt wird, falls eine zu den Typen passende Implementierung im jeweiligen Kontext sichtbar ist. Zusätzlich bedienen wir uns innerhalb von fac
der Tatsache, daß mkNumericOps
und mkOrderingOps
in der Klasse Numeric[T]
als implizite Umwandlungen definiert sind. Sie erlauben es uns, unseren Wert n
, über dessen Typen wir gar nichts wissen, in etwas umzuwandeln, das Rechenoperationen bzw. Größenvergleiche mit der gängigen mathematischen Schreibweise unterstützt — und das, ohne die Umwandlungen explizit zu machen. Nebenbei bietet Numeric[T]
uns Konstanten für die Werte 0 und 1, die wir leider ausschreiben müssen.
(Bemerkung: Man kann die import
-Zeile auch durch eine import num._
-Zeile ersetzen, die alle Methoden aus num
zugleich importiert.)
Daß unsere Definition tut, was sie soll, können wir nachprüfen:
scala> fac(5.0)
res17: Double = 120.0
scala> fac(5)
res18: Int = 120
scala> fac(BigInt(5))
res19: BigInt = 120
Das ist nun sowohl schlechter als auch besser als Haskells Typklassen. Der Hauptnachteil ist, daß der Mechanismus wesentlich weniger transparent ist und mehr auf dem Verstecken von Fakten beruht, was man grundsätzlich kritisch sehen sollte. Als Korollar dazu läßt auch die Typinferenz arg zu wünschen übrig (das ist allerdings im Falle von Scala ganz allgemein so).
Andererseits ist der Mechanismus flexibler, denn er gibt dem aufrufenden Kontext die Möglichkeit, jede beliebige Implementierung für die Rechenoperationen bereitzustellen. Ich habe bereits in einem früheren Eintrag festgestellt, daß Typklassen in dieser Hinsicht eine Barriere zur Wiederverwendung von Code darstellen. Scalas Funktionen werden mithilfe von Umwandlungsfunktionen polymorph, die je nach Kontext unterschiedlich aussehen können und einfach als Parameter übergeben werden. Haskells Funktionen werden hingegen direkt über statische Eigenschaften von Typen parametrisiert, was die Polymorphie einschränkt.
Letztlich bietet Scala in etwa das, was ich am Ende meines früheren, oben referenzierten Eintrags vorgeschlagen habe. Offenkundig lassen allerdings sowohl die Transparenz als auch die syntaktische Unterstützung noch zu wünschen übrig, womit die Frage nach einer optimalen Lösung des Problems weiter unbeantwortet bleibt.
Kompromisse sind wohl nicht zu vermeiden, wenn man eng mit Java zusammenarbeiten will.
Comments
Submit a comment
Note: This website uses a JavaScript-based spam prevention system. Please enable JavaScript in your browser to post comments. Comment format is plain text. Use blank lines to separate paragraphs.