Archiv für August 2009

30
Aug
09

Mit sieben Sieben sieben …

Nachdem ich schon im Beitrag “Der Weg ist das Ziel” Code von Java nach Scala “übersetzt” habe, probiere ich es diesmal mit einer funktionalen Sprache, nämlich Haskell – nicht dass ich Haskell wirklich beherrschen würde, aber dafür reicht es gerade so.

Ausgangspunkt ist der wirklich lesenswerte Artikel The Genuine Sieve of Eratosthenes” von Melissa E. O’Neill, in dem sie aufzeigt, dass das, was vielen Informatik-Studenten als Sieb des Eratosthenes verkauft wird, gar nicht der originale Algorithmus – der mit dem Abstreichen – ist, sondern ein zwar kurzes und elegant wirkendes, dafür aber hoffnungslos ineffizientes Plagiat. Nun ist es trivial, den originalen Algorithmus zu implementieren, wenn man vorher weiß, bis zu welcher Schranke die Primzahlen berechnet werden sollen. Deshalb stellt die Autorin eine clevere Implementierung vor, die das Durchstreichen sozusagen “just in time” durchführt.

Setzen wir also die elaborierteste Version (zu finden auf Seite 7 und 8 des Papiers) nach Scala 2.8 um:

object primes {

    type SI = Stream[Int]

    def sieve:SI = {
        def wheel2357:SI = Stream(4,2,4,6,2,6,4,2,4,6,6,
            2,6,4,2,6,4,6,8,4,2,4,2,4,8,6,4,6,2,4,6,2,6,
            6,4,2,4,6,2,6,4,2,4,2,10,2,10,2) append wheel2357
        def spin(s:SI, n:Int):SI = Stream.cons(n, spin(s.tail, n + s.head))

        case class It(value:Int, step:Int) {
            def next = new It(value + step, step)

            def atLeast(c:Int):It =
            if (value >= c) this
            else new It(value + step, step).atLeast(c)
        }

        implicit object ItOrdering extends Ordering[It] {
            def compare(thiz:It, that:It) = {
                val r = thiz.value - that.value
                if (r == 0) thiz.step - that.step else r
            }
            
        }

        import scala.collection.immutable.TreeSet

        def sieve(cand:SI, set:Set[It]):SI = {
            val c = cand.head
            val set1 = TreeSet[It]() ++ set.dropWhile(_.value < c) ++
               set.takeWhile(_.value < c).map(_.atLeast(c))
            if (set1.elements.next.value == c) {
                val set2 = TreeSet[It]() ++ set1.dropWhile(_.value == c) ++
                    set1.takeWhile(_.value == c).map(_.next)
                sieve(cand.tail, set2)
            } else {
                Stream.cons(c, sieve(cand.tail, set1 + It(c*c,2*c)))
            }
        }
        Stream(2,3,5,7,11) append sieve(spin(wheel2357,13), 
                  new TreeSet[It] + It(121,22))
    }

    def main(args:Array[String]) {
        sieve.takeWhile(_ < 1000).foreach(println)
    }
}

Dieser Code versucht, möglichst dicht am Original zu bleiben. Wie immer sieht der ganze Apparat viel furchterregender aus, als er eigentlich ist. Ich kann hier nur einen kurzen Überblick geben und empfehle, das sehr verständlich geschrieben Original-Papier zu lesen, bei dem die Ideen dieses Algorithmus Schritt für Schritt abgeleitet werden.

Die Zeile type SI = Stream[Int] führt einen Typ-Alias für Integer-Streams ein, denn wir benutzen sie hier wirklich oft, nämlich überall, wo in Haskell Listen verwendet wurden (die vom Verhalten her mehr mit Streams gemein haben als mit Scala-Listen). Wer genau hinschaut wird erkennen, dass das “Wheel” einen Schritt weiter gedreht ist als im Original – mehr dazu später. Das Wheel optimiert alle Kandidaten-Zahlen weg, die bereits durch 2, 3, 5 oder 7 teilbar sind. Dazu speichert es die Schrittweiten zum jeweis nächsten möglichen Kandidaten. So kann in der Methode spin() ganz einfach ein Stream erzeugt werden, der nur Zahlen mit größeren Primfaktoren als 2, 3, 5 und 7 enthält.

Nun kommt die Fallklasse It (für “Iterator”). Wenn wir eine neue Primzahl gefunden haben, erzeugen wir für diese ein neues It-Objekt, was dann später alle Vielfachen dieser Primzahl “just in time” eliminiert. Dazu werden bei jeder neuen Kandidatenzahl alle Its, die kleiner dieser Zahl sind, hochgesetzt. Gibt es danach mindestens ein It, das auf unserer Kandidatenzahl “landet”, ist diese zusammengesetzt. Wurde unser Kandidat dagegen von allen kleineren Its “übersprungen”, ist dieser eine neue Primzahl.

ItOrdering wird gebraucht, um die Its immer sortiert halten zu können. Als implizites Objekt muss man es nicht übergeben, sondern es fungiert als eine Art “Default” für alle “interessierten” Methoden oder Klassen (in unserem Fall TreeSet). Dazu müssen diese einen entsprechenden impliziten Parameter vereinbaren, der dann automatisch “gefüllt” wird – wobei es natürlich auch erlaubt ist, einen normalen Parameter mitzugeben, um ein anderes als das Standard-Verhalten zu erzwingen.

Im Original-Papier wird eine Priority-Queue verwendet. Auch Scala hat eine Implementierung, aber im Unterschied zur Haskell-Version ist diese veränderlich und unterstützt auch nicht das wahlfreie Löschen von Elementen. Als “Ersatz” muss deshalb hier ein TreeSet herhalten. In einer ernsthaften Implementierung würde man wahrscheinlich besser mit einer eigenen Implementierung fahren. Man sieht auch, dass das Hantieren mit dem TreeSet nicht besonders elegant aussieht und demnach eine “maßgeschneiderte” Lösung sehr zur Übersichtlichkeit beitragen könnte.

Am Ende wird ein wenig “Setup” betrieben und die innere sieve-Methode – das eigentliche Arbeitstier – gestartet. Dabei ist im Gegensatz zur Original-Version schon die Primzahl 11 (und ihr entsprechendes It-Objekt) vorhanden, weil der Umgang mit einem eventuell leeren Set umständliche Fallunterscheidungen erfordert hätte. Das ist auch der Grund, warum das Wheel bei 13 und nicht bei 11 beginnt.

Alles in allem war es eine sehr interessante Sache, Haskell-Code in Scala abzubilden, und auch wenn er jetzt nicht mehr ganz so elegant ist, kann man doch noch die Ausgangsversion recht gut wiedererkennen. Auf eine derartige Implementierung mit verschachtelten Streams wäre ich bestimmt nicht von allein gekommen.

Ich hoffe, dass dieser Beitrag nicht zu mathematisch geraten ist, und würde mich natürlich über Verbesserungsvorschläge freuen.

29
Aug
09

Swing-Listeners ade!

Heute mal etwas Java. Während Scala mit dem in der Distribution enthaltenen ScalaSwing (als Wrapper um Swing) recht elegante GUI-Programmierung erlaubt, ist das originale Swing nicht so bequem, wie eine extrem wichtigen Bibliothek sein sollte. GUI-Programmierung ist ein dankbarer Anwengungsfall für Domänenspezifische Sprachen, aber Java ist eben nicht die richtige Sprache dafür. Und die guten Ansätze, die es gibt, werden nicht realisiert.

Eine Sache, die mir ganz besonders die Tränen in die Augen treibt, sind Listener. Funktionen erster Klasse (bzw. Closures) würden das Problem elegant lösen, aber da Java das leider nicht hat, habe ich den Vorschlaghammer ausgepackt: Annotations und jede Menge Reflection. Ja ich weiß, Reflection ist böse. Und ich fühle mich nicht wohl dabei. Und ich weiß, dass man sich damit mächtig in den Fuß schießen kann. Aber auf der anderen Seite wird der Drang, beim Tippen von “new ActionListener…” so zu reagieren, immer stärker.

Der Vorschlaghammer hat wenigstens einen niedlichen Namen: Swirrel, die Kreuzung von Swing mit einem Eichhörnchen (Squirrel).

Wie es funktioniert? Nun, die Komponente muss als Member-Variable vorliegen, dann kann sie einfach mit dem Namen der aufzurufenden Methode annotiert werden:

@ActionPerformed("doSomething")
JButton button = new JButton("Do it!");

Wenn doSomething() z.B. in der gleichen Klasse definiert wäre, sähe der dazu äquivalente Listener so aus:

button.addActionListener(new ActionListener() {
   public void actionPerformed(ActionEvent ae) {
       doSomething(); //oder doSomething(ae);
   }
});

Wenn die GUI fertig zusammengebaut ist, muss man eine statische Methode für jedes Top-Level-Fenster aufrufen, die dann überall nach solchen Annotations sucht und entsprechende Listener anhängt. Die spezifizierte Methode kann entweder kein Argument oder ein Argument der entsprechenden Event-Klasse (hier: ActionEvent) besitzen. Sie kann in der gleichen Klasse, in Superklassen oder den übergeordneten Containern (also all das, was über rekursive getParent()-Aufrufe zu erreichen ist) stehen. Das Aufrufen der statischen Methode ist lästig, also gibt es auch eine Unterklasse von JFrame, die das automatisch erledigt.

Natürlich muss man sich an die Regeln halten, soll einem das Ganze nicht zur Laufzeit um die Ohren fliegen. Insbesondere sieht man den Zielmethoden nicht an, dass sie reflektiv aufgerufen werden, also ist es eine gute Idee, ihren Zweck zu kommentieren, bevor man sie versehentlich umbenennt oder löscht. Es ist also etwas Disziplin gefordert – sagt nicht, ich hätte euch nicht gewarnt!

Swirrel ist noch in der Pre-Alpha-Phase und deckt noch längst nicht alle Listener ab. Bei meinen bisherigen Tests funktionierte die Bibliothek einwandfrei, aber ich freue mich natürlich über jedes Feedback. Hauptsache ihr setzt das Dingens noch nicht produktiv ein …

23
Aug
09

Endrekursions-Nachlese

Wie ich bei meinem ersten Beitrag über Endrekursion gelernt habe, kann man zwar im Byte-Code Endrekursion nachbauen (wie es Scala tut), aber automatisch kann das die JVM von Sun nicht, und auch Sun’s Java-Compiler liefert keine nachgebaute Version.

Eigentlich ziemlich unverständlich, denn das einzige halbwegs stichhaltige Gegenargument ist, dass die rekursiven Aufrufe bei Endrekursion natürlich nicht (ohne weiteres) im Stacktrace zu finden sind. Aber ganz ehrlich: Ist ein Programm, das viele Fälle von Stack-Überlaufen vermeidet und zudem schneller läuft nicht wichtiger als ein “ordentlicher” Stacktrace?

Bei Sun scheint sich nun – nach jahrelangem Dornröschenschlaf – langsam vorsichtiges Umdenken einzustellen, wie der Blog-Eintrag. Tail Calls in the JVM (John Rose @ Sun) zeigt.

Warum ich das “JVM von Sun” so betone? Ganz einfach: Einige andere JVMs, wie z.B. das in Eclipse verwendete J9 von IBM, besitzen Endrekursion, und das teilweise recht lange (IBM’s Implementierung beispielsweise seit Java 1.3). Vor diesem Hintergrund erscheint Sun’s Zurückhaltung einigermaßen peinlich.

Endrekursion lässt sich übrigens auch mit anderen Mitteln simulieren, z.B. mit sogenannten “Trampolines”:

Used in some LISP implementations, a trampoline is a loop that iteratively invokes thunk-returning functions (continuation-passing style). A single trampoline is sufficient to express all control transfers of a program; a program so expressed is trampolined, or in trampolined style; converting a program to trampolined style is trampolining. Trampolined functions can be used to implement tail recursive function calls in stack-oriented languages.

(Wikipedia)
Hier beißt sich die Katze in den Schwanz, denn Java verfügt bekanntlich nicht über Funktionen höherer Ordnung, was die Implementierung bestimmt nicht einfacher macht (es gibt eine Klasse sun.reflect.misc.Trampoline, aber nichts genaues weiß ich nicht …)

Wer ganz genau wissen will, wie Scala die JVM zur Endrekursion “überredet”, sei auf das Whitepaper Tail call elimination on the Java Virtual Machine (M. Schinz, M. Odersky) verwiesen.

21
Aug
09

Fallklassen

So, endlich einmal wieder ein regulärer Beitrag.

Fallklassen (case classes) und Fallobjekte (case objects) sind eine nützliche Sache. Einerseits haben sie gewisse Ähnlichkeiten mit enums, auf der anderen Seite kommt ihnen eine wichtige Rolle beim Pattern Matching zu. Was sind nun Fallklassen? Im Prinzip handelt es sich um ganz normale Klassen, für die der Compiler automatisch ein paar nette Extras hinzufügt.

Am auffälligsten ist, dass man Fallklassen instantiieren kann, ohne new aufzurufen. Das wird dadurch möglich, dass der Compiler im Begleitobjekt eine apply() Methode generiert, die einfach den Konstruktor-Aufruf übernimmt. Da man statt x.apply() einfach x() schreiben kann, sieht es so aus, als ob das new einfach weggefallen wäre. Hier ein kleiner Test, in dem wir das “per Hand” nachvollziehen – denn das ist auch für normale Klassen, die oft gebraucht werden, eine nette Abkürzung.

 
//unsere Fallklasse
case class Foo1(s:String)
Foo1("hi!")
//--> res0: Foo1 = Foo1(hi!)

//das macht der Compiler für uns:
class Foo2(val s:String)

object Foo2{
  def apply(s:String) = new Foo2(s)
}
Foo2("hi!")
//--> res1: Foo2 = Foo2@127bd0e

Ein weiterer Unterschied gibt es in den Konstruktorargumenten: Es braucht kein val angegeben zu werden, damit man auf die Argumente von außen zugreifen kann (allerdings kann man var verwenden, um sie veränderbar zu machen). Eine Fallklasse sollte sich nur über ihren Namen und die Konstruktorargumente “definieren”, und diese deshalb normalerweise unveränderlich sein. Der Compiler übernimmt nämlich auch noch eine “vernünftige” Implementierung von equals(), toString() und hashCode(). Probieren wir es aus:

Foo1("x").toString
//--> res2: String = Foo1(x)
Foo2("x").toString
//--> res3: java.lang.String = Foo2@1a7e2b2
Foo1("x") == Foo1("x")
//--> res4: Boolean = true
Foo2("x") == Foo2("x")
//--> res5: Boolean = false

Kommen wir zum letzten “Extra”, der unapply() Methode. Sie erlaubt, ein Objekt nach gewissen Regeln in seine “Einzelteile” zu zerlegen, und sie macht auch das Pattern Matching erst richtig flexibel. Man nennt Klassen mit dieser Eigenschaft “Extraktoren” Auch hier probieren wir einen “Nachbau”:

case class Person(name:String, age:Int)

def isDaniel(p:Person) = p match {
  case Person("Daniel", age) => 
      println("Daniel ist " + age + " Jahre alt")
      true
  case Person(name, age) => 
      println("Unbekannte Person "+name+" ist "+age+" Jahre alt")
      false
}

isDaniel(Person("Daniel", 35))
//--> Daniel ist 35 Jahre alt
//--> res0: Boolean = true
isDaniel(Person("Daniela", 17))
//--> Unbekannte Person Daniela ist 17 Jahre alt
//--> res1: Boolean = false

//der "Nachbau" mit unapply()
class Mensch(val name:String, val alter:Int) 

object Mensch {
   def unapply(m:Mensch):Option[(String, Int)] = Some((m.name, m.alter))
}

def isDaniel(m:Mensch) = m match {
  case Mensch("Daniel", age) => 
      println("Daniel ist " + age + " Jahre alt")
      true
  case Mensch(name, age) => 
      println("Unbekannte Person "+name+" ist "+age+" Jahre alt")
      false
}

isDaniel(new Mensch("Daniel", 35))
//--> Daniel ist 35 Jahre alt
//--> res2: Boolean = true
isDaniel(new Mensch("Daniela", 17))
//--> Unbekannte Person Daniela ist 17 Jahre alt
//--> res3: Boolean = false

Ich will an dieser Stelle nicht auf die genauen Regeln für unapply() eingehen, mehr dazu findet sich z.B. in dem Papier Matching Objects with Patterns (B. Emir, M. Odersky, J. Williams) oder im bereits erwähnten Buch Programming in Scala.

Ein netter Trick, der mir von joni im Scala-Forum verraten wurde, hilft bei der Validierung der Konstruktorargumente. Wie wir bereits gesehen haben, haben diese für Fallklassen eine viel wichtigere Bedeutung als bei normalen Klassen, die sie auch einfach “auslutschen und wegwerfen” können. Nun möchte man eventuell gerne diese Argumente prüfen oder sogar ändern, bevor man sie wirklich an die Fallklasse übergibt. Dafür würde sich die apply()-Methode des Begleitobjekts eignen, wenn der Compiler mitspielen würde:

case class RestKlasse(wert:Int, modul:Int)

object RestKlasse {
  def apply(wert:Int, modul:Int) = {
    require(modul > 1)
    new RestKlasse(wert % modul, modul)
  }
}

//--> error: method apply is defined twice
//-->        case class RestKlasse(wert:Int, modul:Int)
//-->                   ^

Eigentlich logisch, denn das Begleitobjekt unserer Fallklasse muss ja genau so eine apply()-Methode schon besitzen. Die Lösung des Problems ist so simpel wie überraschend: Man definiere die case class zusätzlich als abstrakt, was den Compiler offenbar davon abhält, eine apply()-Methode zu generieren, aber den sonstigen Fallklassen-”Service” intakt läßt:

abstract case class RestKlasse(wert:Int, modul:Int)

object RestKlasse {
  def apply(wert:Int, modul:Int) = {
    require(modul > 1)
    new RestKlasse(wert % modul, modul){}  //<-- die {} Klammern sind notwendig
  }
}

RestKlasse(14,5)
//--> res0: RestKlasse = RestKlasse(4,5)

Zum Schluss noch ein kleines Beispiel für die Verwendung von Fallobjekten, und zwar ternäre Logik:

sealed trait Ternary {
  def &&(that:Ternary):Ternary
  def ||(that:Ternary):Ternary
  def unary_!() :Ternary
}

case object True extends Ternary {
  def &&(that:Ternary) = that match {
    case True => True
    case False => False
    case Maybe => Maybe 
  }
  def ||(that:Ternary) = that match {
    case True => True
    case False => True
    case Maybe => True 
  }
  def unary_!() = False
}

case object False extends Ternary {
  def &&(that:Ternary) = that match {
    case True => False
    case False => False
    case Maybe => False
  }
  def ||(that:Ternary) = that match {
    case True => True
    case False => False
    case Maybe => Maybe
  }
  def unary_! = True
}

case object Maybe extends Ternary {
  def &&(that:Ternary) = that match {
    case True => Maybe
    case False => False
    case Maybe => Maybe
  }
  def ||(that:Ternary) = that match {
    case True => True
    case False => Maybe
    case Maybe => Maybe
  }
  def unary_!() = Maybe
}

defined trait Ternary
defined module True
defined module False
defined module Maybe

(True && Maybe) == (False || Maybe)
//--> res0: Boolean = true

Diese kleine Anwendung zeigt, dass sich Fallklassen oder -objekte auch gut für die Implementierung Algebraischer Datentypen eignen. Algebraische Datentypen sind sozusagen ein “Sammlung” mehrerer (teilweise sehr verschiedener) Untertypen, ohne dass eine echte Vererbungsbeziehung (die es in vielen Sprachen wie etwa Haskell gar nicht gibt) bestehen würde.

Ich hoffe, dass dieser kleine Überblick zum besseren Verständnis der Fallklassen beigetragen hat. Darüber hinaus wollte ich zeigen, wie man die verwendete Compilermagie auch für die eigenen Zwecke nutzen kann.

19
Aug
09

Nur ein kurzes Lebenszeichen, und der Hinweis auf ein wirklich gutes Interview mit Martin Odersky (leider englisch). Aber keine Sorge, ich werde hier nicht zum Twitterer mutieren …

14
Aug
09

Auf Entzug

Seit Donnerstag ist mein DSL gestört, am Dienstag(!) wollen sich die Telekomiker darum kümmern. Bis dahin gibt es leider keine neuen Posts.

Hoffentlich überlebe ich den Internet-Entzug…

09
Aug
09

Endrekursion

Ich musste erst mal Tante Wikipedia fragen, wie man “tail recursion” in vernünftiges Deutsch übersetzt – “Schwanzrekursion” schien mir irgendwie nicht ganz passend zu sein. Was Rekursion ist, sollte eigentlich jeder Programmierer wissen (falls nicht, wird es gut im 2. Kapitel dieses Skripts erklärt), aber “Endrekursion” dürfte für die meisten Java-Programmierer ein Fremdwort sein. In Scala, wo Rekursion der Normalfall und Schleifen eher die Ausnahme sind, gehört dieses Konzept zum grundlegenden Rüstzeug – aber keine Angst, es ist wirklich nicht kompliziert.

Schauen wir uns zwei verschiedene Implementierungsvarianten für die Fakultät an:

//Variante 1 - nicht endrekursiv
def fac1(n:Int):BigInt = 
  if (n <= 1) 1
  else fac1(n-1) * n

fac1(20)
//--> res0: BigInt = 2432902008176640000

//Variante 2 - endrekursiv
def fac2(n:Int):BigInt = {
  def loop(k:Int, prod:BigInt):BigInt = 
     if (k <= 1) prod 
     else loop(k-1, prod * k)
 
  if (n <= 1) 1 else loop(n,1) 
}

fac2(20)
//--> res1: BigInt = 2432902008176640000

Schön, zumindest kommt bei beiden das gleiche Ergebnis heraus. Variante 1 sieht kürzer, hübscher und verständlicher aus, Variante 2 ist der Stil, den man gewöhnlich in Scala findet. Warum? Weil die erste Version nicht nur langsamer ist, sondern sogar zu Stack-Überläufen führen kann, während das bei Variante 2 nicht vorkommen kann.

Variante 1 hat nämlich das Problem, dass die Rechnung noch nicht “fertig” ist, wenn der rekursive Aufruf erfolgt. So wird der Aufruf von fac1(5) als ((((1*2)*3)*4)*5) ausgeführt. Die Rekursion steigt tiefer und tiefer ab, und am Ende liefert sie die Ergebnisse jeweils dem darüberliegenden Aufruf zurück. Dieser führt noch eine Multiplikation aus und gibt seinerseits das Ergebnis nach “oben” weiter.

Variante 2 dagegen lagert die Rekursion in eine innere Methode loop aus, die ein zusätzliches Argument – nämlich das Produkt der bisherigen Werte – besitzt. Durch diesen Trick ist der rekursive Aufruf das allerletzte, was in loop passiert. Aus diesem Grund braucht man die gerade abgegearbeitete Version des Aufrufs nicht mehr, sie ist für die weitere Berechnung nicht mehr nötig, und kann sozusagen (nach einer Neubelegung der Argumente) “recycelt” werden. Und genau das tut die JVM (allerdings nicht so raffiniert, wie das funktionale Compiler tun): sie steigt nicht eine Ebene “tiefer”, sondern verwendet die aktuelle Umgebung von loop erneut, ohne dass ein echter Methodenaufruf stattfinden würde. Der generierte Byte-Code ähnelt dann mehr einer while-Schleife als dem von Variante 1, und ist entsprechend effizient. Diese Optimierung nennt sich Endrekursion.

Es gibt also einen guten Grund dafür, Variante 2 zu bevorzugen, auch wenn sie ein wenig komplizierter ist. Es lohnt sich, etwas Gehirnschmalz zu investieren, um seine Methoden endrekursiv zu formulieren – mit etwas Übung ist das gar kein Problem. Manchmal reicht schon eine andere Berechnungsreihenfolge, aber oft muss man auch auf den Trick mit dem “Sammelparameter” (der offizielle Name ist “Akkumulator” oder kurz “accu”) zurückgreifen.

In Scala 2.8 gibt es übrigens eine Annotation namens @tailrec, bei der der Compiler eine Warnung ausgibt, wenn die entsprechende Methode nicht so übersetzt werden kann, dass sie endrekursiv ausgeführt wird – eine großartige Hilfe, um Performanceprobleme und Stacküberläufe zu vermeiden.

Da Endrekursion eine Optimierungstechnik der JVM und nicht des verwendeten Compilers ist, gelten alle Ausführungen auch für Java. Wenn man also in Java einen Stacküberlauf durch Rekursion bekommt, kann man die betroffene Methode genauso in der hier präsentierten Weise umformulieren, um Endrekursion zu ermöglichen.
… dachte ich zumindest, bin aber eines besseren belehrt worden. Es ist der Scala-Compiler und nicht die JVM, der die Endrekursions-Optimierung vornimmt. Später mehr dazu.

Dann wünsche ich euch noch fröhliches Rekursieren!

06
Aug
09

Closures und “erstklassige” Funktionen

Heute soll es um ein grundlegendes Feature von Scala und schwerwiegendes Manko von Java gehen: Funktionen erster Klasse (engl.: “First Class Functions”). Schon bei diesem Satz ist die Verwirrung programmiert, deshalb wollen wir an dieser Stelle erst einmal sehr sorgfältig definieren, worüber wir eigentlich sprechen, den an wenigen Stellen der Mainstream-Programmierung ist die Sicht so trübe wie im Dunstkreis der Begriffe “Closure”, “Funktion” und “Methode”. Natürlich sind Object.hashCode() oder Math.sin(double x) im mathematischen Sinne “Funktionen”, aber aus Java-Sicht handelt es sich um Methoden, denn sie sind an ein Objekt bzw. eine Klasse gebunden. Auch Scala hat derartige Methoden. Unter “Funktionen erster Klasse” versteht man dagegen Funktionen, die selbstständige und vollwertige Bestandteile der Sprache sind: Sie besitzen einen eigenen Typ, und man kann sie Variablen zuweisen oder als Argumente übergeben. So etwas geht in Java definitiv nicht, denn dort ist die einzige grundlegende Einheit das Objekt, und so muss man mit Interfaces und anonymen Klassen tricksen, um z. B. Funktionen als Argumente übergeben zu können (man denke etwa an das Runnable-Interface). Scala hat Funktionen erster Klasse, man kann sie zuweisen und übergeben, man kann sie sehr bequem (und mit jeder Menge syntaktischem Zucker) erzeugen und auch ohne großen Aufwand aus einer Methode eine Funktion machen. In diesem Zusammenhang spricht man auch von “Funktionen höherer Ordnung” (engl.: “Higher Order Functions”), d.h. Funktionen, die andere Funktionen als Argumente nehmen und/oder zurückliefern. Ein kleines Beispiel:

def twice[A](func: A => A)(a:A) = func(func(a))

def square(n:Int) = n*n

val biSquare = twice(square) _

biSquare(3)
//--> res0: Int = 81
twice(biSquare)(2)
//--> res1: Int = 65536

Die Funktion twice nimmt eine Funktion als Argument, bei der Argumenttyp und Rückgabetyp gleich ist, und führt sie “auf sich selbst” aus. Aus sin(x) wird also sin(sin(x)) oder in unserem Fall aus square(x) die Funktion square(square(x)), die also “insgesamt” die vierte Potenz berechnet. Die zurückgegebene Funktion lässt sich in einer Variablen speichern (man beachte den Unterstrich, der dafür sorgt, dass die Funktion nicht ausgeführt wird, sonder “als Ding an sich” übergeben wird), ausführen oder auch nochmal an twice übergeben, was eine Funktion liefert, die die sechzehnte Potenz berechnet.

Man stelle sich vor, man solle in Java etwas Analoges zu twice schreiben – das wäre eine ziemliche Herausforderung. Nun taugt diese Form der Abstraktion nicht nur für solche kleinen Spielchen, sondern ist auch extrem nützlich, um flexible, abstrakte Algorithmen zu schreiben. Zum Beispiel ist es Sortieralgorithmen völlig egal, nach welchen Kriterien die Elemente miteinander verglichen werden, solange es nur “den Regeln” entspricht (hier: solange eine totale Ordnung hergestellt wird), und Funktionen erster Klasse erlauben, dieses Detail elegant zu abstrahieren.

Aber schauen wir und erst einmal die Alternative in Java an: Ein Beispiel von www.java2s.com, leicht aktualisiert:

public class EmpComparator implements Comparator<Person> {
  public int compare(Person emp1 , Person emp2) {
    int nameComp = emp1.getFirstName().compareTo(emp2.getFirstName());
    return ((nameComp == 0) ? emp1.getLastName().compareTo(
        emp2.getLastName()) : nameComp);
  }
}

Das ist eine ganz gewöhnliche Comparator-Klasse, die man Collections mitgeben kann, um die Sortierreihenfolge festzulegen – und damit das täglich Brot eines Java-Programmierers. Nun ja, das mag ja eventuell angehen, wenn man einen Comparator wiederverwendet, aber wie oft will man einfach nur eine Liste sortieren und verwendet die tolle Comparator-Klasse nie wieder?

Hier eine mögliche Scala-Lösung. Da die Bezeichner firstName und lastName unangenehm lang sind, habe ich ein wenig in Scalas Trickkiste gegriffen: Wenn man Person als Fall-Klasse schreibt (was für derartige Wert-Klassen sowieso die beste Lösung ist), kann man selbige elegant mit einem case “auseinandernehmen” (kleines Detail: case muss in geschweiften Klammern stehen, und dann darf man die umschließenden runden Klammern weglassen).

case class Person(firstName:String, lastName:String)

List(Person("Werner","Heisenberg"),Person("Albert","Einstein"),Person("Max","Planck")).sort{
    case (Person(f1,l1),Person(f2,l2)) => if(f1 == f2) l1 < l2 else f1 < f2 }

Ja, die Java-Lösung funktioniert auch. Aber ich denke, die Syntax macht hier einen gewaltigen Unterschied. Und sobald man Java mit den Möglichkeiten von Funktionen erster Klasse im Hinterkopf betrachtet, fallen einem sofort die ganzen hässlichen Krücken auf: Comperatoren, Iteratoren, Runnable, ActionListener und wie sie alle heißen. Ein kurzer Blick in die Bibliothek Functional Java zeigt, was mit “echten” Funktionen alles möglich wäre, denn sie besitzt sowohl eine API für normales Java wie auch für Java mit BGGA.

Kommen wir zum Begriff “Closure”, der oft synonym zu “Funktion erster Klasse” gebraucht wird. Das stimmt nicht ganz, denn nicht jede Implementierung einer “Funktion erster Klasse” muss sich auch als Closure verwenden lassen (so sind Funktionspointer in C/C++ keine Closures). Allerdings sind beide Konzepte so eng miteinander verwandt, dass sie in vielen Sprachen – so auch Scala – zu einem einzigen Konstrukt zusammengefasst werden, was zwar zu einer einfacheren Sprache, aber nicht unbedingt zu einer klareren Definition beiträgt. Was ist nun eine Closure? Der Name “Closure” kommt daher, dass Closures ihre Ursprungs-Umgebung wie eine Kapsel “umschließen” und mit sich herumtragen (eine detailliertere Beschreibung findet sich bei Wikipedia). Klingt das etwas seltsam? Dann spielen wir doch einmal Pingpong mit einer lokalen Variablen, deren Definitionsbereich schon lange verlassen wurde:

def funny = {
  var x = 0 
  ( () => {x+=1; x}, () => {x+=10; x})
}
//--> funny: (() => Int, () => Int)

val (c1,c2) = funny
//--> c1: () => Int = <function>
//--> c2: () => Int = <function>

println(c1())
//--> 1
println(c2())
//--> 11
println(c2())
//--> 21
println(c1())
//--> 22

Ein Erklärungsversuch: funny ist eine Methode, die eine lokale Variable x enthält und zwei Closures zurückliefert, die x um eins oder zehn erhöhen. Der Witz ist, dass beide Closures Zugriff auf dasselbe x besitzen und die Änderungen der “anderen Seite” sehen können.

Was hier mächtig nach Spielerei aussieht, ist in Wahrheit ein mächtiges Werkzeug. Die englische Version des oben genannten Wikipedia-Artikels listet folgende Anwendungsgebiete auf: Steuerung des Verhaltens von Bibliotheks-Funktionen durch Übergabe von Closures (unser sort-Beispiel), Definition von Kontroll-Strukturen, Kommunikation über Veränderung der Umgebung (unser funny-Beispiel) und – nur für funktionale Sprachen interessant – die Implementierung eines Objekt-Systems (etwa in Bla). Closures können also tatsächlich mehr als irgendwelche Klassen-Ersatzversuche.

Tja, was ist denn nun mit Java? Java ist die einzige Mainstream-Programmiersprache, die keine Closures (wie Boo, C#, Delphi, Fan, Groovy, JavaScript, Lua, PHP, Perl, Python, Ruby, Scala oder Smalltalk) oder zumindest Funktionen erster Klasse (wie C/C++ mit seinen Funktionspointern) besitzt.
Und wie sich mittlerweile sicher herumgesprochen hat, wird Java 7 leider keinen der Vorschläge (wie die bereits oben genannte BGGA-Implementierung) für die Unterstützung von Closures umsetzen. Meiner Meinung nach ist damit der Zug für Java endgültig abgefahren. Natürlich wird die Sprache allein wegen der ungeheuren Menge vorhandenen Codes weiterexistieren (das tut COBOL ja auch), aber ich kann mir nicht vorstellen, wie es nach dieser Entscheidung in absehbarer Zeit in Javaland Innovation geben könnte, die diesen Namen verdient. Und Java braucht wirklich Innovation, die über syntaktischen Zucker und Trostpflästerchen hinausgeht.

Schade.

04
Aug
09

Marskratervermeidung

Wie wir wissen hat die NASA einen kleinen, aber teuren Marskrater erzeugt, nur damit die Schulkinder auf der Erde ein schönes Beispiel dafür haben, wie wichtig es ist, bei einer Berechnungen penibel auf die richtigen physikalischen Einheiten zu achten.

Wenn man sich die Antwort auf dieses Problem in Java-Land anschaut (nämlich JSR-275), kommt sicher ein bisschen Bewunderung angesichts des freigiebigen und virtuosen Gebrauchs von Generics auf, lässt aber die Frage nach der Praktikabilität einer solchen Lösung offen. Nun ja, vielleicht schafft ja Project Coin Linderung

Hier nun eine kleine Scala-Spielerei, die zeigt, wie leichtgewichtig man ein kleines Einheiten-Framework implementieren und wie schmerzlos dabei die Syntax gestaltet werden kann.

case class Length(mm:Int) {
  def +(that:Length) = Length(this.mm + that.mm)
  def -(that:Length) = Length(this.mm - that.mm)
  def *(scalar:Int) = Length(scalar * mm)
}

So weit, so gut. Unsere Längen basieren intern auf mm, aber natürlich kann man ohne weiteres auch Methoden zum Abfragen der cm, m oder km schreiben. Addieren und Subtrahieren kann die Klasse auch. Im richtigen Leben würde man statt Int wahrscheinlich Double oder sogar BigDecimal nehmen, je nach Anwendung.

implicit def intToMM(value:Int) = new { 
    def mm = Length(value) 
}
implicit def intToCM(value:Int) = new {  
    def cm = Length(value*10) 
}
implicit def intToM(value:Int) = new {  
    def m = Length(value*1000) 
}
implicit def intToKM(value:Int) = new {  
    def km = Length(value*1000000) 
}

Die impliziten Konvertierungen verwenden anonyme Klassen – wozu sollen die temporären Objekte auch einen Namen bekommen, wenn sie nur dazu da sind, uns die passenden Length-Objekte zu liefern?

Ein kleiner Test:

(3 m) + (5 cm) == (3050 mm)
//--> res0: Boolean = true
(3 m) + (5 cm) == (3005 mm)
//--> res1: Boolean = false

Tja, die Operatorpräzedenz ist festgelegt, da hilft auch kein Flehen und Brummen, wir brauchen hier leider die Klammern. Aber trotzdem finde ich diese Version viiieeel lesbarer als Java. Auf jeden Fall ist die hier gezeigte Technik es ein einfaches, aber nützliches Werkzeug bei der Gestaltung eigener DSLs.

01
Aug
09

Typberatung à la Scala

Heute will ich die Frage behandeln: Was tun, wenn der Typ nicht passt? Scala hat darauf viele Antworten, unter anderem strukturelle Typen und implizite Konvertierungen.

Strukturelle Typen – typsicheres Ducktyping

Ein lästiges Beispiel aus meiner beruflichen Praxis sind StringBuilder und StringBuffer. Beide Klassen haben fast identische Methoden, und beide Klassen implementieren Appendable und CharSequence. Das Problem ist, dass diese beiden Interfaces ziemlich spartanisch daherkommen und sich untereinander nicht viel zu sagen haben:

 
public interface CharSequence {
    int length();
    char charAt(int index);
    CharSequence subSequence(int start, int end);
    public String toString();
} 

public interface Appendable {
    Appendable append(CharSequence csq) throws IOException;
    Appendable append(CharSequence csq, int start, int end) throws IOException;
    Appendable append(char c) throws IOException;
} 

Toll, und wie setze ich jetzt mit einer einzigen Methode z.B. einen StringBuilder/Buffer durch setLength(0) zurück? Es gibt immer noch Tonnen von Code mit dem “alten” StringBuffer, und da StringBuilder nicht threadsicher ist, ist “Suchen und Ersetzen” eine gefährliche Lösung. Wäre es nicht schön, einfach sagen zu können: “Mir ist egal, welcher Typ da ankommt, Hauptsache er hat die und die Methoden”? Genau das ist ein struktureller Typ:

def clear(sb:{def setLength(length:Int)}) {
  sb.setLength(0)
}

val builder = new StringBuilder("hallo")
clear(builder)
println(builder.toString == "")
//--> true

Viel mehr gibt es dazu nicht zu sagen: Der strukturelle Typ {def setLength(length:Int)} funktionert so ziemlich überall, wo es ein normaler Typ tun würde. Soweit ich weiß, kann man von einem struktuellen Typ keine anderen Typen ableiten, aber das ist eigentlich auch logisch.

Skriptsprachen lösen das gleiche Problem mit Duck-Typing (“if it walks like a duck…”), allerdings mit dem kleinen Unterschied, dass einem der Code zur Laufzeit um die Ohren fliegt, wenn das übergebene Objekt die entsprechende Methode nicht besitzt.

Implizite Konvertierungen

… sind eine mächtige Waffe, mit der mach sich auch mächtig in den Fuß schießen kann. Sagt nicht, ich hätte euch nicht gewarnt! Aber diese Waffe macht erst Dinge wie DSLs und das “Pimp my Library”-Pattern möglich. Ich möchte hier eine etwas ungewöhnliche Anwendung zeigen, nämlich “disjunkte Typen” (eine Variable kann Werte verschiedener Typen besitzen, die nichts miteinander zutun haben). Zuerst das Grundgerüst ohne implizite Umwandlungen:

sealed abstract class |[P,Q] //die Klasse heißt wirklich | 
case class LeftOr[P,Q](value:P) extends |[P,Q]
case class RightOr[P,Q](value:Q) extends |[P,Q]

def doubleMe(t: Int | String) {
    t match {
        case LeftOr(i) => println(2*i)
        case RightOr(s) => println(s + " " + s)
    }
}

doubleMe(LeftOr(42))
//--> 84
doubleMe(RightOr("Bora"))
//--> Bora Bora

Ähnlich wie bei Methoden ist auch bei Typen die Infix-Schreibwiese erlaubt, was uns das hübsche Int | String anstatt |[Int,String] erlaubt. Weniger schön ist der Aufruf, bei dem wir selbst einen der Untertypen instantiieren müssen – und auch noch aufpassen, dass wir den richtigen erwischen, denn hier geht nur LeftOr(Int) oder RightOr(String). Nun fügen wir implizite Konvertierungen hinzu, und zwar nicht nur für unseren speziellen Fall, sondern gleich generisch für alle möglichen “Belegungen” unserer Typen.

implicit def leftToDis[P,Q](left:P) = LeftOr[P,Q](left)
implicit def rightToDis[P,Q](right:Q) = RightOr[P,Q](right)

doubleMe(23)
//--> 46
doubleMe("Cha")
//--> Cha Cha

Viel besser, nicht wahr? So funktionert es: Wenn der Compiler feststellt, dass der falsche Typ vorliegt (oder z.B. eine bestimmte Methode nicht vorhanden ist), prüft er als allerletzen Ausweg bevor er aufgibt, ob es eine implizite Umwandlung gibt. Diese Umwandlung muss eindeutig sein, und muss die Typen direkt ineinander konvertieren (Ketten von impliziten Umwandungen sind also verboten). In unserem Fall erkennt der Compiler, dass der Aufruf von doubleMe mit einem der beiden Untertypen von | erfüllt werden könnte, und dann benutzt er jeweils eine der beiden impliziten Methoden zur Konvertierung.

Viele Beispiele für implizite Konvertierungen finden sich in Scala selbst, etwa in scala.Predef (diese Klasse wird beim Starten von Scala automatisch importiert). Dort findet sich u. a. folgende implizite Methode:

implicit def stringWrapper(x: String) = new runtime.RichString(x)

Die Klasse scala.runtime.RichString implementiert nun jede Menge nützliche Methoden, die Java-Strings fehlen, so dass man z.B. schreiben kann:

println("Cha " * 3)
//--> Cha Cha Cha 
println("!dlroW olleH".reverse)
//--> Hello World!

Ich hoffe, meine kleine Scala-Typberatung hat euch gefallen, auch wenn wir damit nur an der Oberfläche gekratzt haben.




Follow

Bekomme jeden neuen Artikel in deinen Posteingang.