Schachteln für Fortgeschrittene

Angeregt von dieser Frage auf Stackoverflow will ich mich heute mit geschachtelten Listen beschäftigen: Kann man eine Methode schreiben, die sowohl eine Liste von Ints wie auch eine Liste von Int-Listen wie auch eine Liste von Listen von Int-Listen u.s.w. aufsummieren kann? Sicher, das geht auch in Java – allerdings nicht typsicher: Da es dort keine Möglichkeit gibt, einen allgemeinen „Listen-Stapel-Typ“ zu definieren, muss gecastet werden, und das wiederum heißt, das Laufzeitfehler auftreten können, wenn ein ungeeignetes Objekt (sagen wir eine String-Liste) übergeben wird.

Wie sieht die Haskell-Lösung aus?

class Summable a where
  total :: a -> Int

instance Summable Int where
  total = id  

instance Summable x => Summable [x] where
  total = sum . map total  

total ([[[1,2,3],[4,5]],[[6],[7,8,9,10]]] :: [[[Int]]])
--55

Aha, Typklassen! Wir definieren eine Typklasse „Summable“, die eine Funktion „total“ zum Aufsummieren enthält. Die Instanz für Int ist trivial: Wir geben einfach die Zahl selbst zurück (man hätte dafür auch total x = x schreiben können). Interessanter ist die Instanz für Listen, die eine sogenannte Context-Bound enthält: Wenn a in der Klasse Summable ist, dann ist auch eine Liste von a in der Klasse – genau die Art von „Rekursivität“, die wir brauchen. Deshalb darf man in der Definition von total auch die Funktion total auf die Elemente anwenden, da man ja weiß, dass sie auch Summable sind. In der letzten Zeile wird die Funktion ausprobiert (dass man u.U. die Typangabe :: [[[Int]]] braucht, hat nichts mit unseren Typklassen zu tun, sondern ist der „Monomorphismus-Restriktion“ geschuldet, und wäre bei nicht-numerischen Typen nicht notwendig).

Wie setzt man das nun in Scala um? Ich habe mich zuerst mit einer typklassenartigen Implementierung versucht, bin aber nicht so recht weitergekommen. Am Ende hat dann eine etwas direktere Vorgehensweise zum Ziel geführt:

object nestedTest {
  
  case class Summable(total:Int)
   
  implicit def intSummable(i:Int) = Summable(i)
  
  implicit def listSummable[T <% Summable](list:List[T]) = Summable(list.map(_.total).sum)
  
  def total[T <% Summable](t:T) = t.total
  
  def main(args: Array[String]) {
    println(total(1))
    println(total(List(1,2,3)))
    println(total(List(List(1),List(2,3))))
    //println(total(List(List("a"),List("b","c")))) 
    //--> error: No implicit view available from List[List[java.lang.String]] => nestedTest.Summable.
  }
}

Summable ist jetzt keine Typklasse, sondern einfach ein Wrapper um Int. Dann definiere ich entsprechende Konvertierungen. Wieder ist die Umwandlung von Int nicht besonders spannend, dafür die Definition der List-Variante. Der entscheidende Trick ist die Verwendung einer View-Bound T <% Summable. Das heißt, dass T in ein Summable "umwandelbar" sein muss, und genau das ist der Trick, um Scala die gewünschte "Rekursivität" beizubringen. Durch die Garantie der View-Bound ist es erlaubt, map(_.total) zu schreiben, denn wir wissen ja, dass T in ein Summable konvertiert werden kann, und Summable hat ein Feld namens total. Für die Bequemlichkeit definieren wir noch eine Top-Level-Funktion für total, um dem Anwender das Hantieren mit Summable zu ersparen. In der main-Methode werden nun verschiedene Varianten ausprobiert. Ein Aufruf mit einem unpassenden Argument scheitert, und das sogar mit einer recht verständlichen Fehlermeldung.

Ich hoffe, dass das Beispiel gezeigt hat, dass Scalas Typsystem dem von Java auch bei recht praktischen Aufgabestellungen überlegen ist, auch wenn man manchmal etwas frickeln muss. Haskell glänzt hier mit einer sehr einfachen Variante, aber es gibt auch Konstrukte, die sich besser mit Scala implementieren lassen (z.B. alles mit variabler Anzahl Argumente).

Ich fasse schon einmal den guten Vorsatz, das Blog nächstes Jahr nicht ganz so stiefmütterlich zu behandeln. An spannenden Themen soll es nicht mangeln: Da wäre das gerade frisch releaste Ceylon (allerdings mit noch enttäuschenden Funktionsumfang), Bauarbeiten an Scala und Java, die tolle Sprache Frege, die langsam erwachsen wird und ihr Vorbild Haskell, bei dem auch interessante Neuerungen anstehen, wenn man Simon Peyton Jones glaubt.

Dann wünsche ich allen ein Frohes Fest, einen Guten Rutsch und ein erfolgreiches 2012!

Werbeanzeigen

breakOut – ein kleiner Durchbruch

Heute ist mir ein nützliches Feature der Scala-Collections untergekommen: breakOut.

Angenommen, ich habe eine Liste von Paaren, und will sie eine Map umwandeln. „Früher“ (in Scala 2.7) gab es verschiedene Tricks dafür, z.B.

val list = List(1,2,3,4,5) zip List("one","two","three","four","five")
println(list)
//--> List((1,one), (2,two), (3,three), (4,four), (5,five))

val map0 = Map[Int, String]() ++ list
println(map0)
//--> Map(5 -> five, 1 -> one, 2 -> two, 3 -> three, 4 -> four)

Abgesehen davon, dass Listen jetzt eine toMap-Methode haben, gibt es auch die Möglichkeit, breakOut zu benutzen:

...
import collection._

val map1:Map[Int,String] = list.map(identity)(breakOut)
println(map1)
//--> Map(5 -> five, 1 -> one, 2 -> two, 3 -> three, 4 -> four)

Wie funktionert das? Schauen wir uns die Typ-Signatur von List.map an. Davon gibt es zwei Versionen, und natürlich ist „unsere“ die mit den zwei Argumentlisten:

def map [B, That] (f: (A) ⇒ B)(implicit bf: CanBuildFrom[List[A], B, That]) : That 

Aha! In der zweiten Argumentliste wird normalerweise stillschweigend und implizit ein CanBuildFrom übergeben. Diese Objekte bilden sozusagen das Rückgrat für alle Konvertierungen der Collection-Bibliothek. Anstelle dieses „normalen“ CanBuildFrom-Objekts liefern wir nun explizit eine spezielle Version, die von der Methode breakOut (die im package object collection in package.scala definiert ist).

Der breakOut-Trick funktionert nicht nur mit map, sonder bei allen Methoden, die eine zweite „CanBuildFrom-Argumentliste“ haben, z.B. bei zip:

...
val map2:Map[Int,String] = (List(1,2,3,4,5) zip List("one","two","three","four","five"))(breakOut)
println(map2)
//--> Map(5 -> five, 1 -> one, 2 -> two, 3 -> three, 4 -> four)

Eine andere nützliche Anwendung ist die Konvertierung zwischen veränderlichen und unveränderlichen Collections:

...
val map3: collection.mutable.Map[Int,String] = map2.map(identity)(breakOut)
println(map3)
//--> Map(2 -> two, 4 -> four, 1 -> one, 3 -> three, 5 -> five)

Also falls einmal irgendwo mittendrin in einer Berechnung der Typ nicht passen sollte, hat man gute Chancen, dass ein angehängtes (breakOut) das Problem kuriert.

Wer noch mehr Details wissen will: Auf Stackoverflow gibt es eine sehr ausführlichere Antwort zu diesem Thema.

Intuitive APIs mit impliziten Parametern

Will man eine Liste von Paaren aufspalten, gibt es unzip:

println(List((1,"one"),(2,"two")).unzip)
//--> (List(1, 2),List(one, two))

Was ist, wenn man eine Liste von Either hat? Nun, man kann „etwas ähnliches“ schreiben:

def splitEither[A,B](list: List[Either[A,B]]):(List[A],List[B]) =
       (list.collect{case Left(a) => a}, list.collect{case Right(b) => b})

println(splitEither(List[Either[Int,String]](Left(42),Right("two"))))
//--> (List(42),List(two))

Und wenn man einen anderen Datentyp hat, der sich zum Splitten anbietet, etwa Alternativen, dann schreibt man sich eben eine weitere Methode…

Aber zehn Methoden, die alle fast dasselbe tun, sehen nicht besonders chic aus, oder? Und wenn ich beim Refactoring zu einem anderen Datentyp wechsle, darf ich auch überall eine andere split-Methode verwenden. Aber es geht besser, und zwar mit impliziten Parametern. Genauer gesagt implementieren wir das „Type Class Pattern“. Zuerst einmal überlegen wir uns, wie ein Hilfs-Objekt für unsere generische split-Methode aussehen müsste:

trait Splitter[A,B,C] {
   def splitList(list: List[C]):(List[A],List[B])
}

Die split-Methode ist dann trivial:

def split[A,B,C](list:List[C])(implicit splitter:Splitter[A,B,C]):(List[A],List[B]) = splitter.splitList(list)

Hier hatte ich eine kleine Denkblockade, weil sich der implizite Parameter nicht geeignet mit impliziten Objekten oder vals belegen lässt, weil dort natürlich alle Parameter an konkrete Typen gebunden sein müssen, also keine Typ-Parameter zulassen. Oder vornehm ausgedrückt: „Objekte und vals sind in Scala monomorph“.

Zum Glück hat mir Miles Sabin mit seiner Antwort auf Stackoverflow auf die Sprünge geholfen: Es geht ganz einfach mit impliziten Methoden, bei denen die Typ-Parameter kein Problem sind. Irgendwie hatte ich verdrängt, dass implizite Methode nicht nur Typkonvertierungen erlauben, sondern auch implizite Parameter liefern können. Hier der gesamte Code:

 
object splitTest {

  trait Splitter[A,B,C] {
     def splitList(list: List[C]):(List[A],List[B])
  }

  implicit def pairSplitter[A, B] = new Splitter[A, B, Pair[A, B]] {
    override def splitList(list: List[Pair[A, B]]) : (List[A], List[B]) =
    (list.collect{case (a,_) => a}, list.collect{case (_,b) => b})
  }

  implicit def eitherSplitter[A, B] = new Splitter[A, B, Either[A,B]]() {
     override def splitList(list: List[Either[A,B]]):(List[A],List[B]) =
       (list.collect{case Left(a) => a}, list.collect{case Right(b) => b})
  }

  def split[A,B,C](list:List[C])(implicit splitter:Splitter[A,B,C]):(List[A],List[B]) = 
     splitter.splitList(list)

  def main(args: Array[String]) {
    println(split(List((1,"one"),(2,"two"))))
    println(split(List[Either[Int,String]](Left(42),Right("two"))))
    //println(splitList(List(1,2,3,4))) //compiliert nicht
  }

}

Natürlich ist das einiger Aufwand, die ganze Maschinerie in Stellung zu bringen. Gelohnt wird es einen mit Erweiterbarkeit und Flexibilität: Man kann jederzeit Splitter für andere Datentypen schreiben, und diese entweder wie vorgestellt implizit machen, oder split explizit mitgeben. Für den wichtigeren Aspekt halte ich allerdings die Verringerung der Namen, die man sich merken muss. Die Methode split „tut einfach das Richtige“, jedenfalls solange sie weiß, wie sie es anstellen muss. Statt in der API nachzusehen, wie denn die für meinen Fall zutreffende von zehn split-Versionen heißt (so es sie denn überhaupt gibt), kann ich einfach ausprobieren, ob split mit meinem Typ klarkommt – und das schon typsicher beim Compilieren.

Natürlich war das nur ein ganz elementares Beispiel, aber es zeigt trotzdem, welche kreativen Möglichkeiten man bei der Gestaltung einer API hat, und wie man dem Nutzer damit das Leben leichter machen kann.

Context und View Bounds (von Daniel C. Sobral)

Auf Stackoverflow habe ich eine wirklich druckreife Erklärung von Context und View Bounds von Daniel C. Sobral gefunden, der so freundlich war, mich diese übersetzen und verwenden zu lassen:

Was ist eine View Bound?

View Bounds sind ein Mechanismus, der in Java eingeführt wurde, damit man einen Typ A so verwenden kann, als ob es ein Typ B wäre. Die typische Syntax ist:


def f[A <% B](a: A) = a.bMethod

Mit anderen Worten, A sollte eine implizite Umwandlung nach B besitzen, so dass man Methoden von B auf einer Instanz vom Type A aufrufen kann. Die gebräuchlichste Verwendung von View Bounds in der Standardbibliothek (jedenfalls vor Scala 2.8.0), war mit dem Ordered-Trait, in etwa so:


def f[A <% Ordered[A]](a: A, b: A) = if (a < b) a else b

Da man ein A in ein Ordered[A] umwandeln kann, und weil Ordered[A] die Methode <(other: A): Boolean definiert, kann man den Ausdruck a < b verwenden.

Was ist eine Context Bound?

Context Bounds wurden in Scala 2.8.0 eingeführt, und werden typischerweise mit dem sogenannten Typklassen-Pattern verwendet, einem Code-Pattern, dass die Funktionalität von Haskells Typklassen emuliert, wenn auch in einer etwas umständlicheren Art und Weise.

Während eine View Bound mit einfachen Typen (etwa A <% String) verwendet werden kann, benötigt eine Context Bound einen parameterisierten Typ, wie etwa Ordered[A] weiter oben, aber nicht wie String.

Eine Context Bound beschreibt einen impliziten Wert anstelle der impliziten Umwandlung bei den View Bounds. Sie wird verwendet, um anzuzeigen, dass für einen Typ A ein impliziter Wert des Typs B[A] vorhanden ist. Die Syntax sieht so aus:


def f[A : B](a: A) = g(a) // wobei g einen impliziten Wert des Typs B[A] benötigt

Das ganze ist etwas verwirrender als eine View Bound, weil nicht unmittelbar klar ist, wie man so etwas benutzt. Ein gebräuchliches Anwendungsbeispiel in Scala ist:


def f[A : ClassManifest](n: Int) = new Array[A](n)

Für eine Array-Initialisierung eines parameterisierten Typs muss ein ClassManifest vorhanden sein – aus ziemlich obskuren Gründen im Zusammenhand mit Type Erasure und dem damit nicht kompatiblen Verhalten von Arrays.

Ein anderes verbreitetes Beispiel in der Standardbibliothek ist etwas komplizierter:


def f[A : Ordering](a: A, b: A) = implicitly[Ordering[A]].compare(a, b)

Hier wird die Methode implicitly verwendet, um den impliziten Wert zurückzugewinnen, den wir benötigen, in unserem Fall vom Typ Ordering[A], der die Methode compare(a: A, b: A): Int definiert.

Weiter unten werden wir eine weitere Realisierungsmöglichkeit kennen lernen.

Wie sind View Bounds und Context Bounds implementiert?

Es sollte angesichts ihrer Definition nicht überraschen, dass sowohl View Bounds wie auch Context Bounds mittels impliziter Parameter realisiert werden. In Wahrheit ist die von mir vorgestellte Schreibweise syntaktischer Zucker für die tatsächlichen Vorgänge. So sieht diese „entzuckert“ aus:


def f[A <% B](a: A) = a.bMethod
def f[A](a: A)(implicit ev: A => B) = a.bMethod

def g[A : B](a: A) = h(a)
def g[A](a: A)(implicit ev: B[A]) = h(a)

Dementsprechend kann man stets die volle Syntax verwenden, was insbesondere bei Context Bounds nützlich sein kann:


def f[A](a: A, b: A)(implicit ord: Ordering[A]) = ord.compare(a, b)

Wofür werden View Bounds verwendet?

View Bounds verwendet man hauptsächlich, um die Vorteile des „Pimp My Library“ Patterns zu nutzen, vobei man Methoden zu einer existierenden Klasse „hinzufügen“ will, aber irgendwie den ursprünglichen Typ zurückgeben will. Wenn man den Original-Typ nicht zurückgeben will, benötigt man keine View Bounds.

Das klassische Beispiel für die Verwendung von View Bounds ist der Umgang mit Ordered. Beispielsweise ist Int nicht Ordered, aber es gibt eine implizite Umwandlung. Das weiter oben gegebene Beispiel benötigt eine View Bound genau deshalb, weil dort der nicht konvertierte Typ zurückgegeben wird:


def f[A <% Ordered[A]](a: A, b: A): A = if (a < b) a else b

Dieses Beispiel würde ohne View Bounds nicht funktionieren. Wenn man allerdings einen anderen Typ zurückgibt, benötigt man keine View Bounds mehr:


def f[A](a: Ordered[A], b: A): Boolean = a < b

Die Konvertierung (sofern notwendig) geschieht vor der Übergabe des Parameters an f, so dass f darüber nichts wissen muss.

Neben Ordered ist einer der häufigsten Anwendungsfälle die Verwendung der Java-Klassen String und Array als ob sie Scala Collections wären. Zum Beispiel:


def f[CC <% Traversable[_]](a: CC, b: CC): CC = if (a.size < b.size) a else b

Wenn man versuchen würde, das ohne View Bounds zu schreiben, wäre der Rückgabetyp für einen String WrappedString (in Scala 2.8), und analog bei einem Array.

Das gleiche passiert selbst dann, wenn der Typ nur als Typ-Parameter des Rückgabetyps verwendet wird:


def f[A <% Ordered[A]](xs: A*): Seq[A] = xs.toSeq.sorted

Wofür werden Context Bounds verwendet?

Context Bounds werden hauptsächlich für eine Technik verwendet, die man in Anlehnung an Haskells Typklassen „Typklassen-Pattern“ getauft hat. Grundsätzlich bietet dieses Pattern eine Alternative zur Vererbung, in dem es die Funktionalität eine Art „impliziten Adapter“ bereitstellt.

Das klassische Beispiel ist Ordering in Scala 2.8, das Ordered in der gesamten Scala-Bibliothek ersetzt hat. Die Verwendung ist:


def f[A : Ordering](a: A, b: A) = if (implicitly[Ordering[A]].lt(a, b)) a else b

Allerdings schreibt man es normalerweise in dieser Form:


def f[A](a: A, b: A)(implicit ord: Ordering[A]) = {
    import ord._
    if (a < b) a else b
}

Dabei wird ausgenutzt, dass innerhalb von Ordering einige implizite Konvertierungen bereitgestellt werden, die die traditionellen Verwendung von Operatoren unterstützen. Ein weiteres Beispiel in Scala 2.8 ist Numeric:


def f[A : Numeric](a: A, b: A) = implicitly[Numeric[A]].plus(a, b)

Ein komplizierteres Beispiel ist die Verwundung von CanBuildFrom für die neuen Collections, aber es gibt [auf Stackoverflow] bereits eine ziemlich ausführliche Antwort darauf, so dass ich hier nicht darauf eingehe. Und wie bereits erwähnt gibt es die Verwendung von ClassManifest, die beispielsweise zur Initialisierung von Arrays ohne konkrete Typen benötigt wird.

Es ist viel wahrscheinlicher, dass Context Bounds mit dem Typklassen-Pattern für eigene Klassen verwendet werden, weil man damit die Trennung von Verantwortlichkeiten [Seperation of Concerns] erreichen kann, während man die Verwendung von View Bounds im eigenen Code durch gutes Design vermeiden kann (sie werden meist verwendet, um mit einem fremden Design zurechtzukommen).

Obwohl es sie schon eine ganze Weile gibt, hat sich die Verwendung von Context Bounds erst 2010 richtig durchgesetzt und ist nun zu einem gewissen Grad in vielen von Scalas wichtigsten Bibliotheken und Frameworks zu finden. Die weitestgehendste Verwendung findet sich in der Scalaz-Bibliothek, die eine Menge Haskell-Power in Scala verfügbar macht. Ich kann nur eine Recherche zum Typklassen-Pattern empfehlen, um sich mit dessen verschiedenen Einsatzmöglichkeiten vertraut zu machen.

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.