Shownotes & Links
- The Neophyte’s Guide to Scala
- Scala Meta
- Enforcing invariants in Scala datatypes
- Strategic Scala Style: Designing Datatypes
- Extractors
- Type classes
Transkript
Stefan Tilkov: In unserer heutigen Episode des innoQ Podcasts wollen wir uns wieder einmal mit Scala beschäftigen. Als Gast habe ich heute den Daniel Westheide dabei. Hallo Daniel.
Daniel Westheide: Hallo.
Stefan Tilkov: Erzähl doch kurz ein bisschen was über dich, bitte.
Daniel Westheide: Ich arbeite als Senior Consultant bei der innoQ und beschäftige mich hauptsächlich mit Backend-Anwendungen, mit verteilten Systemen und in letzter Zeit auch ein bisschen mit Big Data. Ich habe eine Vorliebe für funktionale Programmierung, insbesondere für Scala.
Stefan Tilkov: Du bist außerdem - daher kennt dich vielleicht sogar der ein oder andere Leser - Autor eines länglichen Blogs mit dem Titel “The Neophyte’s Guide to Scala”. Das, finde ich, muss man auch mal erwähnen.
Daniel Westheide: Genau, das habe ich vor einigen Jahren geschrieben und aufgrund der Nachfrage dann auch in ein e-Book umgewandelt. Beides erfreut sich immer noch einiger Beliebtheit.
Stefan Tilkov: Das verlinken wir am Ende auch auf jeden Fall in den Shownotes. Deswegen sprechen wir über Scala. Vielleicht mal ein Geständnis vorab: Ich bin alles andere als ein Scala Experte, was etwas schräg ist, weil wir uns vorgenommen haben über fortgeschrittenere Dinge zu sprechen. Einen Intro Podcast zu Scala hatten wir schon mit dem Tobias Neef, den verlinken wir natürlich auch nochmal. Heute wollen wir ein bisschen über spannendere, neuere Dinge sprechen, die man als Einsteiger vielleicht noch nicht weiß. Sagen wir mal, dass wir uns im intermediate bis fortgeschrittenen Themenbereich tummeln. Dazu habe ich mir ein paar Fragen von dir soufflieren lassen, also ich bin heute einfach nur “Fragen-Monkey”, und wenn mir zwischendurch irgendetwas einfällt oder wenn mir auffällt, was ich interessant finde oder gar nicht verstehe, dann frage ich einfach wieder dazwischen. Ich lege mal mit dem ersten Thema los, es ist sogar ein Feature, das ich trotz meines limitierten Scala-Wissens kenne, nämlich Case Classes. Das ist für mich immer so eines der Features, mit denen man es sehr gut verkaufen kann, die man immer als erstes nennt. Vielleicht kannst du darauf noch eingehen, warum das so cool ist und vielleicht auch, was daran nicht so toll ist.
Daniel Westheide: Ja, gerne. Wie du schon sagtest, Case Classes sind ein gutes Verkaufsargument für Scala und auch etwas, wo manche Java-Leute gerne sagen, dass sei etwas, worauf sie wirklich neidisch sind und was ihnen in Java fehlt. Warum ist das toll: Case Classes erlauben mir mit minimalem Aufwand und minimalem Boilerplate einen Datentyp zu definieren und dann wird jede Menge Code für mich, den ich in Java selbst schreiben müsste, generiert. Dazu gehört eine equals-Methode, eine Hashcode-Methode, eine toString-Methode. Die Felder einer Case Class sind alle per Default public und immutable und es wird auch eine Copy-Methode generiert, sodass ich aus einer Instanz einer Case Class leicht eine neue erzeugen kann, in der ich vielleicht ein Feld oder zwei geändert habe. Dadurch fallen schon viele Design Patterns weg, die man nicht mehr braucht, zum Beispiel das Builder Pattern ist in Scala dadurch auch gar nicht so verbreitet. Eine weitere Sache, die ich mit Cases Classes machen kann, ist, dass ich sie im Pattern Matching einsetzen und destrukturieren kann.
Stefan Tilkov: Stopp, stopp, stopp. Warum brauche ich kein Builder Pattern mehr, wenn ich Case Classes verwende?
Daniel Westheide: Ich kann sehr leicht aus einer Instanz eine veränderte erzeugen, durch diese Copy-Methode. Natürlich hat man einen gewissen Overhead, weil man ja eine Kopie erzeugt. Deswegen ist das Builder Pattern auch nicht völlig unbekannt in Scala, aber eben nicht so verbreitet wie man das in Java zum Beispiel hat.
Stefan Tilkov: Ich assoziiere mit dem Builder Pattern, dass ich durchaus auch unterschiedliche Typen habe, also eine Kaskade von Methoden, die im Prinzip was erzeugen und dann etwas zurückgeben, auf dem ich wieder andere Dinge aufrufen kann und das muss nicht unbedingt jedes Mal dieselbe Klasse sein. Es könnte durchaus eine andere Klasse sein, die da zurück kommt - das hat jetzt mit Case Classes nichts zu tun, oder?
Daniel Westheide: Nee, das nicht, genau.
Stefan Tilkov: Alles klar. Pattern Matching…
Daniel Westheide: Pattern Matching erlaubt mir Case Classes zu destrukturieren. Das ist ziemlich bequem, ich denke das kennst du auch aus Clojure, das Destrukturieren von Datenstrukturen wie Maps oder Listen. Natürlich hat das aber auch so seine Schattenseiten. Zunächst einmal, dass alle Felder public sind, ist schon potenziell problematisch. In der Objektorientierung haben wir gerne eine Kapselung des internen Zustandes, während es in der funktionalen Programmierung beliebt ist, Daten und Verhalten komplett voneinander zu trennen, und dann würde man Pattern Matching benutzen, um auf den Daten zu arbeiten. Ein Problem ist zum Beispiel, wenn ich zu einer Case Class ein Feld hinzufüge oder wegnehme als Refactoring, dann geht überall dort, wo meine Case Class destrukturiert wird, direkt etwas kaputt und dann habe ich viele Compile-Fehler. Das ist eine Sache, die ziemlich mühsam ist. Wenn ich also viel mit Pattern Matching von Case Classes arbeite und etwas an der Struktur verändere, habe ich danach viele Stellen, wo ich etwas anpassen muss. Ansonsten sind Case Classes zwar schön bequem, aber was auch nicht ganz trivial ist, ist damit Invarianten der Geschäftslogik durchzusetzen, da kann man leicht etwas falsch machen.
Stefan Tilkov: Kannst du mir da ein Beispiel geben? Was wären Invarianten, die man typischerweise in so einer Klasse immer wahr haben möchte?
Daniel Westheide: Sagen wir, wir hätten eine Case Klasse “Point” mit einer X- und Y- Koordinate und unsere Invariante wäre, dass die Koordinaten größer oder gleich Null sein müssen. Wenn ich das jetzt durchsetzen will, ist die einfachste Variante, dass ich eine Exception im Konstruktor werfe, wenn diese Invarianten nicht eingehalten werden.
Stefan Tilkov: Setter oder sowas gibt’s ja nicht, das ist ja alles immutable. Das heißt, ändern kann ich das sowieso nicht zur Laufzeit.
Daniel Westheide: Genau, Setter gibt es ohnehin nicht, aber im Konstruktor muss ich dann dafür sorgen, dass diese Invarianten eingehalten werden. Es funktioniert im Grunde so, wie man es in Java auch machen würde. Man wirft zum Beispiel eine IllegalArgumentException oder etwas in der Art. Es ist aber kein idiomatisches Scala, das zu tun. In Scala ist es so, dass man Errors nicht gerne als Exceptions behandelt, sondern im Rückgabetyp encodiert. Das heißt, ich könnte alternativ meinen Case-Class-Konstruktor als private deklarieren und dann im Companion-Object der Case Class eine Konstruktor-Funktion angeben, die mir nicht einen Point in diesem Fall zurückgibt, sondern eine Option von Point. Diese Option wäre nur definiert, wenn die Eingabewerte valide sind, wenn sie die Invarianten nicht verletzen. Ansonsten wäre die Option ein None, also nicht definiert. Dann würde man mit dieser Option weiter arbeiten.
Stefan Tilkov: Die Option, wenn ich mich richtig erinnere, haben wir im ersten Podcast als Muster erklärt. Vielleicht sagst du noch ganz kurz etwas zum Companion-Object.
Daniel Westheide: In Scala ist es möglich, da es dort so etwas wie statische Methoden nicht gibt, für jede Klasse ein Companion-Singleton-Object mit dem gleichen Namen zu definieren und dieses kann dann auch beliebige Methoden, Funktionen, Felder und so weiter haben. Zwischen der Klasse und dem Singleton Object besteht die besondere Beziehung, dass sie gegenseitig auf private Felder zugreifen können - deswegen heißt es Companion-Object oder Companion-Class, je nachdem, in welche Richtung man schaut.
Stefan Tilkov: Gut, weitere Schatten von Case-Classes?
Daniel Westheide: Also wenn ich denke, ich habe meine Invarianten durchgesetzt, sei es durch Assertions oder durch einen privaten Konstruktor und eine Factory Methode im Companion-Object, dann habe ich wahrscheinlich vergessen, dass es diese Copy-Methode gibt. Die Copy-Methode erzeugt eine neue Instanz der Case-Class, wo ich Felder verändern kann. Hier kann ich dann meine Invarianten komplett aushebeln, wenn ich “copy” aufrufe - das wird oft vergessen.
Stefan Tilkov: Was kann ich tun, um das zu verhindern?
Daniel Westheide: Es gibt ein paar Tricks, die ziemlich kompliziert sind. Man schreibt dann schon wieder viel Code, deswegen möchte ich auf diese jetzt gar nicht eingehen, denn elegant heißt: gar keine Case-Class zu verwenden. Es gibt seit Kurzem ein neues Scala Projekt für Macros, die alten Macros sollen nicht mehr fortgeführt werden. Das Projekt heißt “Scalameta” und stellt unter anderem eine Annotation bereit: Die Data-Annotation. Da kann ich dann zum Beispiel genau sagen: Ich möchte keine Copy-Methode; Ich möchte keine Apply-Methode in meinem Companion-Objekt erzeugt bekommen; Ich habe also eine sehr gute Kontrolle darüber, welche von diesen tollen Sachen, die ich für Cases-Classes immer generiert bekomme, ich auch wirklich haben will.
Stefan Tilkov: Ist das dann immer noch eine Case-Class, die ein bisschen anders funktioniert oder ist es eine normale Klasse, in der ich dann doch wieder die Dinge selbst machen muss, die mir die Case-Class weggenommen hat?
Daniel Westheide: Es ist eine Klasse, bei der dann auch viele Dinge generiert werden, die für Case-Classes automatisch generiert werden. Manche davon kann ich ausschalten, zum Beispiel die Copy-Methode. Es verhält sich ansonsten, wenn ich es nicht ausschalte, wie eine Case-Class. Zum Beispiel kann ich es im Pattern Matching verwenden und ich habe eine Equals- und Hashcode-Methode und was man sonst noch so alles bekommt.
Stefan Tilkov: Also das coole “Killer-Feature” Case-Classes am besten umgehen. Zumindest dann, wenn man kompliziertere Inhalte hat?
Daniel Westheide: Es gibt natürlich einfache Datentypen, wo es völlig valide ist, die Case-Class zu verwenden. Wenn ich komplexere Geschäftslogik habe, um zu validieren, was ein valider Zustand ist, dann gibt es Alternativen dazu, die man eher wählen sollte.
Stefan Tilkov: Damit haben wir das erste Thema, glaube ich, abgehandelt.
Daniel Westheide: Genau.
Stefan Tilkov: Kommen wir zu dem zweiten. Das ist eine Sache, die mich manchmal an unselige C++ Vergangenheiten erinnert, aber vielleicht widerlegst du das auch gleich: Was hat es mit diesen “Implicits” auf sich? Das musst du mir nochmal erklären.
Daniel Westheide: Das ist so ein Thema, vor dem viele Leute, die sich mit Scala noch nicht so auskennen, Angst haben oder denken, dass sei etwas ganz Schlimmes. Ich denke wir nähern uns dem jetzt erst einmal langsam an, ich hoffe, dass ich es erklären kann. Zunächst einmal ist es so, dass man in der funktionalen Programmierung allgemein, aber auch in der Scala Community, sehr viel Wert auf Abstraktion legt. Ein Beispiel dafür ist parametrischer Polymorphismus: Ich habe Methoden oder Funktionen mit einem Typparameter und der Rückgabewert dieser Methode - oder Argumente - ist vom Typ erst nicht konkret bekannt, sondern nutzt diesen Typparameter -
Stefan Tilkov: Generics sozusagen, oder Templates.
Daniel Westheide: Genau, Generics oder Templates. Es limitiert mich erst einmal sehr in den möglichen Implementierungen meiner Methode, der Typparameter “A” zum Beispiel, über den ich nichts weiter weiß. Deshalb gibt es die Möglichkeit, Einschränkungen vorzunehmen. Zum Beispiel: Mein “A” muss ein Untertyp eines bestimmten anderen Typs sein oder ein Obertyp. Das sind in Scala “Upper”- oder “Lower”-Typebounds. Ein Beispiel wäre: “A” muss ein Untertyp sein von “Ordered”. In Java ist es vergleichbar mit “Comparable” und Ordered erbt, glaube ich, sogar von Comparable, weil es auch auf der JVM läuft - so ist das alles relativ gut miteinander verzahnt. Dann basiert diese Einschränkung für meine Typparameter auf einer Vererbungshierarchie. Auch in Java hat man eine Alternative: es gibt ja auch den “Comparator”, von dem man nicht erbt, sondern den stellt man für einen bestimmten Typen bereit und den muss man dann einer Methode, die etwas vergleichen können möchte, explizit als weiteres Argument mitgeben. In Scala gibt es ein “Trait” - so etwas wie ein Interface in Java - namens Ordering und das erbt auch vom Comparator, soviel ich weiß. Hier wird aber das sogenannte “Typeclass-Pattern” umgesetzt - ich erkläre gleich noch, wie das genau funktioniert. Die Idee ist, dass ich sage: Für meinen Typparameter “A” habe ich die Einschränkung, dass eine Instanz der Ordering-Typeclass vorhanden sein muss, dass der Compiler mir nachweist, dass eine solche Instanz vorhanden ist.
Stefan Tilkov: Du musst aufpassen, dass du mich nicht abhängst beziehungsweise muss ich aufpassen, dass du mich nicht abhängst. Eine Instanz einer Typeclass ist offensichtlich ein Type, richtig? Ist das so, dass eine Instanz eine Typklasse -
Daniel Westheide: Ein Instanz einer Typeclass ist eigentlich ein Wert. Am besten erkläre ich, wie Typeclasses überhaupt umgesetzt sind, weil sie in Scala tatsächlich nur ein Pattern sind, während sie in einer Sprache wie Haskell wirklich Syntax-Support haben und nicht nur ein Pattern sind. Ordering ist ein Trait mit einem Typparameter und der hätte jetzt Methoden, die ich implementieren muss für meinen bestimmten, konkreten Typen. Ich könnte zum Beispiel ein Ordering für “Int” bereitstellen, das heißt, dass ich den Ordering Trait erbe oder ein Mixin davon mache.
Das weise ich einem Value zu, der aber ein implicit value ist. Eine Funktion, die nun sagt, sie braucht den Nachweis, dass eine Ordering-Instanz vorhanden ist, hat eine implicit parameter list. In Scala kann ich mehrere Parameterlisten in einer Methode haben und die letzte davon kann das “implicit” Keyword haben. Der Scala-Compiler sucht anhand von definierten Implicits die Parameter.
Ich kann zum Beispiel in meiner Klasse diesen Implicit lokal definiert haben; Einen implicit value vom Typ “Ordering of Int”. Wenn ich irgendwo eine Methode habe, die generisch ist, aber ein Ordering of “A” - “A” ist der Typparameter - erwartet, dann rufe ich diese Methode, die vielleicht eine Liste von “A” erwartet, mit einer Liste von Ints auf. Der Scala-Compiler weiß, dass er dann ein implicit Ordering of Int finden muss. Soweit klar?
Stefan Tilkov: Ich versuche es noch einmal zu rekapitulieren, soweit ich es kapiert habe: Implicit ist sozusagen dieses Coercion-Feature, das mir den passenden Converter sucht, wenn ich etwas brauche, das ich nicht habe oder wenn ich jemanden habe, der mir das liefert, was ich brauche auf Basis dessen, was ich habe, dann kann ich das benutzen, um es zu erzeugen.
Daniel Westheide: Genau.
Stefan Tilkov: In keiner Weise besser, als deine Erklärung, aber das ist, was mir gerade durch den Kopf gegangen ist. In diesem Fall bräuchte ich etwas, das für meine Klasse eine bestimmte Spezialisierung hat, aber eben nicht in meiner Klasse.
Daniel Westheide: Ganz genau. Das Tolle an diesem Pattern ist, dass ich Verhalten, wie Ordering oder auch andere Sachen für Typen definieren kann, die ich nicht selbst kontrolliere. Zum Beispiel könnte ich auch ein Ordering für “DateTime” erzeugen und ich muss dann DateTime natürlich nicht von irgendetwas ableiten - das kann ich ja gar nicht - sondern ich erzeuge es außerhalb. Es ist eigentlich auch gar nicht so unterschiedlich zu Protocols in Clojure. Ich weiß nicht, ob diese jetzt unseren Zuhörern bekannt sind.
Stefan Tilkov: Die gleiche Idee, dass es in Clojure auch ohne Typen geht. Ich erweitere praktisch etwas oder liefere eine Implementierung einer Funktion für einen Typen, dessen Designer nichts davon gewusst hat, dass ich jemals diese Funktion haben möchte, den das nicht interessiert.
Daniel Westheide: Sowohl Clojure’s Protocols, als auch Typeclasses lösen damit das sogenannte “Expression Problem”: Verhalten zu Typen hinzufügen, die man nicht neu kompilieren kann oder möchte. Da sind Typeclasses als Alternative zur Vererbung relativ beliebt in Scala - aber diese implicit Parameterlisten schrecken Neulinge oft erst einmal ab.
Stefan Tilkov: Gibt es noch einen anderen Nutzen für diese implicit Parameterlisten oder sind sie genau für dieses Typeclass-Pattern.
Daniel Westheide: Das Typeclass-Pattern ist nur ein Anwendungsfall dafür. Ein anderer, der recht häufig ist, ist so eine Art Dependency-Injection zu machen oder einen Kontext zu liefern. In Scala gibt es zum Beispiel schöne Future Implementierungen. Ich kann ein Future mappen oder filtern und so weiter, aber das heißt: Jedes Mal brauche ich auch einen Execution-Context. Es ist vergleichbar mit einem Thread-Pool, aber auf einer höheren Abstraktionsebene.
Diesen Execution-Context möchte ich aber auch nicht immer explizit überall mitschleppen, deswegen wird er auch als implicit Parameter definiert, das heißt ich muss einen implicit Execution-Context irgendwo bereit stellen. Das ist dann kein Typeclass Pattern. Der Execution-Context hat keinen Typparameter, so wie “Ordering of A”, sondern es ist einfach eine Abhängigkeit, die wir haben und die man nicht immer überall explizit durchreichen möchte.
Stefan Tilkov: Stark, finde ich ein cooles Feature.
Daniel Westheide: Das freut mich von dir zu hören.
Stefan Tilkov: Gut, dann sind wir mit diesem Teil, Implicits und Typeclasses durch und gehen zum nächsten. Das nächste in der Liste ist ein Feature, das ich auch ziemlich cool finde: Pattern Matching!
Daniel Westheide: Genau.
Stefan Tilkov: Vielleicht erklärst du das auch noch einmal kurz für die, die nicht täglich Erlang programmieren und sagst was da noch im Detail drin steckt.
Daniel Westheide: Pattern Matching gibt es in Scala an vielen verschiedenen Stellen, aber was man am meisten kennt, sind Pattern Matching Expressions. Ich habe irgendeinen Wert, gefolgt von einem Match-Keyword und einer Folge von Cases. Es ist im Grunde eine deutlich bessere Version vom Java Switch Statement, deutlich flexibler. Da gibt es verschiedene Arten von Patterns.
Wie schon erwähnt, kann ich Case Classes in meinem Pattern destrukturieren. Es gibt aber auch viele verschiedene andere Patterns: Dieses Konstruktor-Pattern für Case-Classes; das ist auch schon das beliebteste, was viele Leute gerne benutzen, weil man so etwas in seinem Java-Switch-Statement überhaupt nicht hat - das ist wahrscheinlich auch das, was du in Clojure gerne magst, dass du eine Map hast, wo du anhand bestimmter Keys, die du im Voraus weißt, bestimmte Werte heraus ziehst.
Stefan Tilkov: Ja, immer dieses Destrukturieren ist das Attraktive daran.
Daniel Westheide: Ich hatte schon erwähnt, dass dieses Destrukturieren von Case-Classes nicht besonders refactoring-freundlich ist. Wenn ich ein Feld hinzufüge, habe ich sehr viele Stellen, an denen ich mein Destructuring anpassen muss. Oft ist es so, dass Leute dieses Destructuring von Case-Classes verwenden, wo sie vielleicht nur an einem einzigen Feld interessiert sind und die anderen Felder werden alle mit Underscores gefüllt. Das heißt: Da ist der Wert egal, man benutzt ihn nicht.
Wenn man aber wirklich nur an einem Feld interessiert ist, kann man das eben verhindern, dass man beim Refactoring so viel anpassen muss. Zum Beispiel gibt es typisierte Pattern in Scala. Anstatt des Destrukturierens sage ich in meinem Case, ich erwarte etwas von einem bestimmten Typen. Das sieht dann so ähnlich aus wie einen Typen in einer Parameterliste zu deklarieren. Zum Beispiel: foo: Bar. Das würde genau dann matchen, wenn der Wert, auf den ich matche, genau diesem Typ entspricht. So habe ich nicht diese Underscores für jedes Feld, an dem ich gar nicht interessiert bin.
Stefan Tilkov: Lege ich dann explizit Typen für das an, was mich interessiert? Muss ich da an der Typhierarchie herumfummlen?
Daniel Westheide: Typischerweise ist der Typ, an dem man interessiert ist, oder anders herum - Ich habe vielleicht eine Funktion, deren Input irgendein Obertyp ist und in meinen Cases möchte ich dann die verschiedenen Fälle abfrühstücken.
Stefan Tilkov: Verstehe. Anstatt eines “instanceOf” mache ich das richtige Matching.
Daniel Westheide: Genau, das ist eigentlich nur ein besseres “instanceOf”, aber intern findet hier auch ein “instanceOf”-Check statt, aber mit freundlicherer Syntax. Andere Fallstricke, die es noch beim Pattern Matching gibt, sind das sogenannte “Variable Pattern”: Mit dem matched man im Grunde auf alles. Man bindet den Wert, gegen den man matched, nur an einen bestimmten Namen. Das ist oft auch gewollt, aber was ich oft gesehen habe ist, dass man den Namen, den man hier in dem Variable-Pattern verwendet, auch schon außerhalb seiner Pattern-Matching-Expression definiert hat. Einen Value zum Beispiel. Die Annahme ist dann, dass man genau gegen den Wert dieses Values matched, das ist aber nicht der Fall, denn innerhalb der Pattern-Matching-Expression findet hier ein Shadowing dieser schon definierten Variable statt. Ich weiß eigentlich dann gar nichts mehr von dieser außen definierten Variable und kann nicht mehr auf diese Weise auf sie zugreifen. Lösen kann ich das durch ein sogenanntes “Stable Identifier Pattern”: Wenn ich also gegen einen ganz bestimmten Wert matchen möchte, der schon einem Value mit einem Identifier zugewiesen wurde, dann kann ich in meiner Pattern-Matching-Expression Back-Ticks um diesen Namen herum verwenden. Das ist dann ein Stable Identifier Pattern. So kann ich sicherstellen, dass ich kein Shadowing von einem Namen habe, der außerhalb der Expression verwendet wird.
Weitere Fallstricke finden sich mit generischen Datentypen, zum Beispiel Listen. Das ist ein Fall, den ich häufig gesehen habe. List hat auch einen Typparameter “A” - “List of A” - da schreiben Scala Neulinge gerne Funktionen, die eine List of A tatsächlich bekommen, wo sie dann aber in ihren Cases matchen möchten: Im Fall, dass es eine List of String ist, mache etwas Bestimmtes; Wenn es eine List of Int ist, mache etwas anderes.
Stefan Tilkov: Und wir sind uns einig, dass sie damit eigentlich fundamental Recht haben und das gefälligst funktionieren müsste.
Daniel Westheide: Genau. Leider funktioniert das natürlich nicht. So wie Java auch, hat Scala das Feature der Type Erasure.
Stefan Tilkov: “Feature” ist gut..
Daniel Westheide: Ich sage jetzt bewusst Feature, weil es auch etwas Gutes an sich hat, aber in diesem Fall fällt man damit schneller auf die Füße. Die Liste ist zur Laufzeit immer eine Liste vom Typ Object oder in Scala “AnyRef”. Die Information ist also gar nicht mehr da, ob es ein String oder ein Int ist. Es gibt dann ein Compiler-Warning, aber es schlägt nicht zur Compile-Zeit fehl, so ein Pattern-Matching zu machen. Man bekommt dann vermutlich sehr unerwartete Ergebnisse.
Stefan Tilkov: Was kann man da tun?
Daniel Westheide: Die Lösung besteht in sogenannten “Class Tags”. Im Grunde haben diese auch die Form von Type-Classes. Ein Class-Tag hat auch einen Typparameter “T” und ich muss erst einmal nur dafür sorgen, dass der Class-Tag verfügbar ist, indem ich an meiner Methode für den Typparameter “A” oder “T” einen implicit Class-Tag anfordere. Der Scala Compiler weiß, dass ein Class-Tag verfügbar ist. Dann kann ich mir den Class-Tag zur Laufzeit holen und kann den mit dem Typen String vergleichen, um sicherzustellen, dass ich hier eine Liste von Strings habe. Das ist natürlich Reflection. Es ist eine Lösung, die funktioniert, aber besser ist es zu vermeiden, dass man überhaupt so ein Pattern-Matching benötigt.
Stefan Tilkov: Im Prinzip hat man sich ein kleines eigenes Typsystem daneben gebastelt, ein zur Laufzeit verfügbares. Ein bisschen schräg fühlt sich das sicher schon an. Haben wir noch Dinge, die wir zu Pattern-Matching sagen können?
Daniel Westheide: Den “beliebten” Match-Error bekommt man, wann immer man ein Pattern-Matching macht, wo man nicht alle möglichen Fälle abgedeckt hat. Das passiert recht häufig, wenn man komplexe Patterns hat und einen Default-Fall vergisst. Man kann natürlich immer als letzen Case ein Variable-Pattern oder ein Underscore nehmen, um sicher zu stellen, dass man alle Fälle abgedeckt hat. Man muss sich überlegen, was das Verhalten in dem Fall sein soll, aber es gibt bestimmte Datentypen, mit denen Pattern Matching wirklich gut harmoniert; Wo man dann auch vom Compiler mehr Hilfe bekommt: Eine Warnung, dass man gar nicht alle Fälle abgedeckt hat.
Das sind sogenannte “algebraische Datentypen”, wie “Option”, der aus genau zwei verschiedenen Fällen besteht; Some oder None. Ich kann solche Datentypen auch selbst definieren. Der Clou an dem Ganzen ist eben, dass so ein Datentyp, wie zum Beispiel Option, als “sealed” markiert ist. Das Keyword sealed sagt dem Scala-Compiler, dass es nur genau die Untertypen geben kann, die in diesem Source-File definiert sind. Dadurch hat der Compiler mehr Informationen und kann einem sagen, dass man einen oder mehrere Fälle nicht abgedeckt hat.
Stefan Tilkov: Ergibt Sinn. Sonst noch etwas?
Daniel Westheide: Fallstricke, glaube ich, nicht mehr. Eine Sache, die vielen Leuten unbekannt ist, ist zum Beispiel die Tatsache, dass es Pattern-Alternativen gibt, die ich mit dem Pipe-Symbol ausdrücken kann: Ich kann in einem Case mehrere Patterns als OR-Verknüpfung machen und muss diese nicht jeweils als eigenen Case runterschreiben. Das kann auch einiges an wiederholtem Code verhindern. Etwas, das auch interessant ist, dass man auf beliebige Datentypen destrukturieren kann, nicht nur Cases-Classes, denn es gibt das sogenannte “Extractor-Pattern”. Wenn ich einen Extractor geschrieben habe, kann ich den in einem Pattern verwenden. Ein Extractor ist letztendlich nur ein Objekt oder eine Klasse mit einer sogenannten “Unapply-Methode”, die eine Option zurück gibt - das im Detail zu beschreiben ist wahrscheinlich zu viel für diesen Podcast, dazu habe ich in meinem Blog zwei Beiträge, die das ausführlich beschreiben.
Stefan Tilkov: Das soll für die erste Folge dieses Podcasts absolut reichen, wir sind auch am Limit unserer Zeit. Es geht in der zweiten Folge nahtlos mit den Geheimnissen der fortgeschrittenen Scala-Verwendung weiter.
Vielen Dank an dieser Stelle und bis zum nächsten Mal.