Montag, 31. Januar 2011

Scala: Spass mit Streams

Ich wurde ja neulich mal gefragt ob ich mal was über Streams in Scala bloggen könnte (auch wenn es hier eher um IO ging). Da mir da allerdings ein gutes Beispiel gefehlt hat, habe ich das erst einmal auf die lange Bank geschoben (irgendwie blogge ich derzeit glaub ich etwas zu wenig :/).

Aber ok, dank eines hervorragenden Posts von Mario Gleichmann bin ich dann doch noch auf eine ganz brauchbare Idee gekommen. (Schon allein Vollständigkeit halber empfehle ich den Post von Mario zu lesen!)

Im folgenden Code geht es einfach nur darum die nachfolgende Jahreszeit zu bestimmen.

sealed abstract class Season
case object Spring extends Season
case object Summer extends Season
case object Fall extends Season
case object Winter extends Season

object Main {
lazy val seasons: Stream[Season] =
Spring #:: Summer #:: Fall #:: Winter #:: seasons

def nextSeason(now: Season): Season = {
@scala.annotation.tailrec
def getNextFromStream(s: Stream[Season]): Season =
s match {
case a @ x #:: y #:: _ =>
if (now eq x)
y
else
getNextFromStream(a tail)
}
getNextFromStream(seasons)
}

def main(args: Array[String]): Unit = {
println(nextSeason(Spring))
println(nextSeason(Summer))
println(nextSeason(Fall))
println(nextSeason(Winter))
}
}

Um die Jahreszeiten abzubilden nutze ich hier einen Stream (also eine unendliche Liste) die ich rekursiv durchgehe bis ich die aktuelle Jahreszeit gefunden habe und einfach die Nächste ausgebe.

Dieses Beispiel wirkt zugegebenermaßen etwas wie mit Kanonen auf Spatzen zu schiessen, zeigt aber die Ausdrucksstärke von Streams und Lazy-Evaluation. Ein sehr realer Anwendungsfall ist hier z.B. ein Roundrobin-Verfahren. Anstelle also stumpf eine Liste durchzugehen und am Ende wieder an den Anfang springen in Zukunft einfach mal über einen Stream nachdenken ;)

Kommentare:

Anonym hat gesagt…

Hai xRaich[o]^2x,

sehr lesenswerter post!

Vielen Dank nochmal für Dein klasse Feedback - die Idee Streams zu verwenden finde ich wie gesagt sehr clever!

Ich habe mir Deine endgültige Implementierung nochmals angeschaut, wobei mir noch zwei Ideen / Varianten in den Sinn gekommen sind:

1. @ - Binding eines Teils des Pattern-Ausdrucks:

Hier könnte man auch nur den 'Rest' (also Tail) des Streams binden, also so:

case x #:: ( tail@ y #:: rest ) => if (now eq x) y else getNextFromStream( tail )

2. Bedingung als Guard

Die Bedingung könnte man auch als Guard einsetzen und so den else-Fall als weiteren case beschreiben:

case x #:: y #:: _ if (now eq x) => y
case _ => getNextFromStream( s tail )

Nochmals, vielen Dank für Dein Feedback und Deine Ideen!

Viele Grüsse

Mario

xRaich[o]²x hat gesagt…

Arg, die Idee mit den Guards ist eigentlich so offensichtlich das ich da hätte selber drauf kommen müssen ^^. Macht das ganze noch einiges schöner. Danke!

afripowered hat gesagt…

Sehr schöner Eintrag. Ich liebe diese Sprache immer mehr :-)

Der Guard-Vorschlag treibt das ganze natürlich zu Perfektion :-)

Anonym hat gesagt…

Hey xRaich[o]^2x,

hier noch eine weitere Möglichkeit, ganz ohne Guard auszukommen: im Pattern kann natürlich auch direkt gegen den aktuellen Parameter / Value 'now' gematcht werden. Da Scala allerdings klein geschriebene Namen als zu bindenede Variablen interpretiert, müsste dieser 'gross' geschrieben werden (now -> Now), was ggf. etwas unschön ist:

def nextSeason2( Now: Season ): ...
...
case Now #:: y #:: _ => y
case _ => getNextFromStream( s tail )
...

Grüsse

Mario