Archive for the ‘map’ Tag

Teil 6.2: Set, Tuple und Map

Sequenzen von Daten reichen für manche Anwendungsfälle nicht aus. Wenn wir Beispielsweise Daten sortiert halten wollen ist eine Seq nicht die beste Wahl, da das sortierte Einfügen ein Flaschenhals darstellen kann. Das gleiche gilt wenn wir Daten suchen wollen. Die Seq zu durchlaufen und jedes Element zu überprüfen ob es auf unsere Anforderungen passt kann bei vielen Elementen viel zu lange dauern. Aber dafür haben wir ja Sets und Maps, die dafür genau das Richtige sind.

Da beide Collection-Typen vom gleichen Obertyp erben wie Seq können wir auf die gleichen Methoden zurückgreifen um die Datenstrukturen anzusprechen.

Set

Testen wir die Methoden von Set doch gleich einmal:

scala> val set = Set(1, 2, 3)
set: scala.collection.immutable.Set[Int] = Set(1, 2, 3)

scala> set contains 3
res116: Boolean = true

scala> set contains 5
res117: Boolean = false

scala> set ++ List(3, 4, 5)
res118: scala.collection.immutable.Set[Int] = Set(5, 1, 2, 3, 4)

Wie zu erkennen speichert das Set nur Unikate. Wenn wir also eine Liste aus Daten haben und nur die Anzahl der verschiedenen Elemente haben wollen genügt es diese einfach in ein Set einzufügen und dann die Größe des Sets zu ermitteln:

scala> val xs = List(2, 5, 1, 7, 3, 35, 72, 23, 6, 1, 73, 35, 76, 86, 17)
xs: List[Int] = List(2, 5, 1, 7, 3, 35, 72, 23, 6, 1, 73, 35, 76, 86, 17)

scala> val set = Set() ++ xs
set: scala.collection.immutable.Set[Int] = Set(5, 1, 6, 73, 2, 17, 86, 76, 7, 3, 35, 72, 23)

scala> set.size
res119: Int = 13

Ein weiteres Merkmal ist, dass die Elemente im Set nicht sortiert sind, wie wir an der String-Repräsentation erkennen können. Um dies zu ändern müssen wir anstatt eines Sets einfach ein SortedSet referenzieren:

scala> import scala.collection.immutable.SortedSet
import scala.collection.immutable.SortedSet

scala> val set = SortedSet[Int]() ++ xs
set: scala.collection.immutable.SortedSet[Int] = TreeSet(1, 2, 3, 5, 6, 7, 17, 23, 35, 72, 73, 76, 86)

Es gibt in scala.Predef keine Typreimplementierung von SortedSet weshalb wir es von Hand importieren müssen. Dies erreichen wir ganz einfach mit dem import-Statement. Bitte beachtet, dass wir das SortedSet von Hand typisieren müssen, die die Typinferenz des Compilers hier nicht mehr ausreicht.

Tuple

Der Konstruktor von Map erwartet sogenannte Tuple von Daten. Um also eine Map zu erstellen müssen wir zuerst einen Tuple erstellen. Aber was ist ein Tupel? Ein Tupel ist eine Datenstruktur, die beliebige Daten aufnehmen und mit einander „verbinden“ kann. Die einfachste Möglichkeit einen Tuple zu kreieren ist einfach irgendwelche Daten mit einem Komma voneinander zu trennen und diese dann einzuklammern:

scala> ("de", "Germany")
res125: (java.lang.String, java.lang.String) = (de,Germany)

scala> (1, "hello", 5.34)
res126: (Int, java.lang.String, Double) = (1,hello,5.34)

In Scala können bis zu 22 solcher Daten „vertupelt“ werden. Tuple stellt aber keine Collection dar. Die Methoden, die wir bisher kennen gelernt haben sind größtenteils deshalb auch nicht darauf anzuwenden. Ein Tuple soll auch nicht als Collection dienen. Statt dessen soll er die Möglichkeit bieten mehrere zusammenhänge Daten innerhalb des Codes hin und her zu reichen, ohne dass man sich dafür eine eigene Datenstruktur bauen muss. Brauchen können wir dies bspw. bei einer Methode, die mehrere Daten zurückgeben soll:

def heavyCalculation(): (Int, Int) = {
  val memoryUsage = 30
  val cpuUsage = 91
  (cpuUsage, memoryUsage)
}

Stellen wir uns vor, dass diese Methode große Berechnungen machen müsste. Nachdem die Berechnungen abgeschlossen sind würden wir gerne Wissen wie viel Systemressourcen sie benötigt haben. Wir geben also die Speicher- und CPU-Auslastung in Prozent zurück. Der Rückgabewert wird ebenfalls in Klammern geschrieben um dem Compiler zu verdeutlichen, dass ein Tuple zurückgegeben werden soll.

Tuple haben nur einen kleinen Schönheitsfehler wenn man versucht auf ihre Elemente zuzugreifen:

scala> heavyCalculation()
res130: (Int, Int) = (91,30)

scala> res130._1
res131: Int = 91

scala> res130._2
res132: Int = 30

Da ein Tuple beliebige Daten aufnehmen kann gab es bei ihrer Implementierung keine Möglichkeit einen geeigneten Namen zu definieren  der zu jedem Rückgabewert passt. Man hat sich deshalb auf die Schreibweise tuple._N entschieden wobei N ein Wert zwischen 1 und 22 ist. Je nach Größe des Tuples kann man dann damit an dessen Elemente gelangen. Wird ein Feld adressiert, das bei der momentanen Größe des Tuples nicht existieren kann bekommen wir eine Fehlermeldung:

scala> res130._3
<console>:11: error: value _3 is not a member of (Int, Int)
              res130._3
                     ^

Wenn ein Tuple die Größe 2 hat wird auch von einem Tuple2 gesprochen, bei der Größe 3 von einem Tuple3 usw. Um einen Tuple2 zu erstellen gibt es noch eine spezielle Methode:

scala> 1 -> "hello"
res141: (Int, java.lang.String) = (1,hello)

scala> (1).->("hello")
res142: (Int, java.lang.String) = (1,hello)

Dies ist syntaktischer Zucker für die wohl am meisten erstellte Tuple-Art. Vor allem in Verbindung mit Maps werden wir regen Gebrauch davon machen. Um an alle Elemente eines Tuples zu kommen bieten sich die Method `productIterator` an:

scala> val t = (6, "hello", 3.245, true, 'h')
t: (Int, java.lang.String, Double, Boolean, Char) = (6,hello,3.245,true,h)

scala> val i = t.productIterator
i: Iterator[Any] = non-empty iterator

scala> while (i.hasNext) println(i.next)
6
hello
3.245
true
h

Wir erhalten einen Iterator, über den wir nach Lust und Laune iterieren können.

Map

Maps besitzen die tolle Eigenschaft, dass sie zu einem bestimmten Schlüssel einen Wert abspeichern. Das könnte z.B. so aussehen:

scala> val m = Map("de" -> "Germany", "en" -> "England", "fr" -> "France")
m: scala.collection.immutable.Map[java.lang.String,java.lang.String] = Map(de -&gt; Germany, en -&gt; England, fr -&gt; France)

Um einen Wert aus einer Map zu erhalten gibt es zwei Möglichkeiten:

scala&gt; m("de")
res144: java.lang.String = Germany

scala&gt; m.get("de")
res145: Option[java.lang.String] = Some(Germany)

Die erste Schreibweise kennen wir bereits. Die Zweite ist auch nicht viel komplizierter gibt aber einen komischen Typ namens Some zurück. Some ist ein Untertyp vom Typ Option. Wofür dieser gut ist sehen wir wenn wir einen Wert anfordern der nicht existiert.

scala&gt; m("ch")
java.util.NoSuchElementException: key not found: ch
<stacktrace>

scala> m.get("ch")
res147: Option[java.lang.String] = None

Im ersten Fall erhalten wir eine Exception, die wir natürlich nicht erhalten wollen. Die Vorgehensweise in Java wäre jetzt zuerst zu prüfen ob der Schlüssel existiert und nur dann seinen Wert anzufordern. Es würde auch die Möglichkeit bestehen einfach die Exception abzufangen und zu verarbeiten. Die Vorgehensweisen haben aber ein Problem: Sie erfordern viel unnützen Code, der uns nicht direkt bei der Bewältigung unserer Aufgabe weiterbringt. Hier eine Beispielimplementierung der ersten Vorgehensweise:

def mapValue[A, B](m: Map[A, B], s: A) =
  if (m contains s) m(s) else null

scala> mapValue(m, "de")
res151: String = Germany

scala> mapValue(m, "ch")
res152: String = null

Die erste Implementierung ist an sich sehr kurz, wir müssen aber null zurückgeben wenn der Wert nicht existiert. Der große Nachteil des Codes ist weiterhin, dass er nicht generisch ist. Würden wir das auch noch einbauen wollen müssten wir nochmal ein bisschen mehr schreiben. Die Implementierung mit Exceptions würde ich gerne auslassen, da sie wohl sowieso niemand praktisch anwendet. Sie würde sowieso mehr Codezeilen beanspruchen.

Scala macht es sich da einfacher und gibt ein Option zurück, das entweder Some oder None sein kann, je nach dem ob ein Wert existiert oder nicht. Was man mit Option so alles machen und wie es uns vor NullPointerExceptions beschützen kann möchte ich nochmal gesondert besprechen, das soll nicht Inhalt dieses Artikels sein.

Um an die Schlüssel oder Werte einer Map zu gelangen werden uns die folgenden Methoden bereitgestellt:

scala> m.keys
res158: Iterable[java.lang.String] = Set(de, en, fr)

scala> m.values
res159: Iterable[java.lang.String] = MapLike(Germany, England, France)

Mehr brauchen wir momentan eigentlich nicht über Map zu wissen.

Teil 6: Collections

Das Herzstück einer jeden Programmiersprache sind ihre Collections. Sie werden immer benötigt sobald Datensätze zusammengefasst werden und stellen viele Möglichkeiten bereit die Daten zu transformieren. So gibt es:

  • Seq: Können Datensätze, die vom gleichen Typ sind, aufnehmen und eignen sich besonders gut um durchlaufen zu werden oder um auf die Daten per Index zuzugreifen.
  • Set: Speichert nur Unikate, d.h. es kann kein Datensatz mehrfach vorkommen. Eignen sich besonders gut um zu schauen ob bestimmte Datensätze schon existieren.
  • Map: Speichert Key-Value-Paare. Zu jedem gespeicherten Key wird ein Value gespeichert, den man anhand des Keys leicht wiederfinden kann.

Alle Collections erben vom Typ `Iterable`, der wiederum vom Type `Traversable` erbt. Das ganze nochmal in einer grafischen Darstellung:

Image

Alle Collections gibt es sowohl in einer veränderlichen und in einer unveränderlichen Variante. Automatisch importiert wird die unveränderliche Variante. Wenn Collections mit veränderlichem Inhalt benötigt werden, dann müssen diese explizit referenziert werden, aber dazu später mehr.

Unveränderlich heißt, dass jede Operation auf eine Collection immer eine komplett neue Collection gleichen Typs erstellt, die alten Daten bleiben bis zu ihrer Destruktion unangetastet. Das Package in dem sich die unveränderlichen Objekte befinden heißt `scala.collection.immutable`, equivalent dazu nimmt das Package `scala.collection.mutable` die veränderlichen Collections auf. Nun noch zwei Graphen mit den Namen der konkreten Implementierungen.

Für die Unveränderlichen:

Image

Die Veränderlichen:

Image

Und noch die Legende:

Image

Da man keine Lust hat die ganze Zeit die Collections über ihren vollen Package-Namen zu referenzieren bzw. diesen zu importieren geschieht dies automatisch. Genau genommen werden alle Objekte im Package `scala` und alle Member von Objekt `scala.Predef` automatisch importiert. Innerhalb von Predef werden verschieden Typdefinitionen festgelegt, u.A. für die unveränderlichen Collections. Die Methode `Console.println` erhält z.B. auch eine Neudefinition, damit sie komfortabel nur mit `println` aufgerufen werden kann. Ein Blick in die Sourcen von Predef lohnt sich also – es gibt viel zu entdecken.

Bei der Entwicklung der Collections wurde viel Wert auf eine einheitliche API genommen. Dies bedeutet, dass die wichtigsten Operationen bei jedem Collection-Typ die gleichen sind. Dabei spielt es keine Rolle ob mit veränderlichen oder unveränderlichen oder gar mit den Collections gearbeitet wird, die parallele Datenverarbeitung erlauben. Einzig allein der Typ List definiert einige Methoden, die auch nur dieser Typ kennt – man sollte also wenn möglich nur in einem möglichst kleinen Sichtbarkeitsbereich mit List arbeiten und nach außen hin den Typ mit seiner Schnittstelle Seq ansprechen.