Blog

Übereinstimmende Zeichenketten in Scala

Arnout Engelen

Arnout Engelen

Aktualisiert Oktober 21, 2025
5 Minuten

Im Dezember hatte ich viel Spaß bei den Advent of Code Coding Challenges mit einigen Kollegen. Viele dieser Aufgaben, wie z.B. Tag 21, erfordern die Interpretation einer Art von String-Eingabe. Normalerweise würde ich diese Strings vor der Verarbeitung in Case-Klassen zusammenfassen, aber in diesem Fall schien mir das übertrieben: ein schneller Pattern-Match sollte ausreichen. Es stellte sich heraus, dass es mehrere Möglichkeiten gibt, dies zu tun, was auch eine gute Ausrede ist, um unter die Haube zu schauen und zu sehen, auf welchen Scala-Konzepten sie basieren.

Mustervergleiche mit Regexen

In diesem Beitrag werden wir den Befehl "Tausche Position X mit Position Y" als Beispiel verwenden. Wir können eine Regex für diesen Befehl erstellen und sie dann verwenden, um den Befehl abzugleichen: [code language="scala"] val SwapPositions = "Tausche Position (d+) mit Position (d+)".r def applyCommand(in: String, command: String): String = command match { case SwapPositions(x, y) => in.updated(x.toInt, in(y.toInt)).updated(y.toInt, in(x.toInt)) ... } [/code]

Extraktionsmuster: unapply

Wenn Sie bisher nur Pattern-Matches auf Case-Klassen durchgeführt haben, werden Sie vielleicht überrascht sein, dass wir hier Pattern-Matches auf eine scala.util.matching.Regex durchführen können. Dies wird Extractor Pattern genannt: Wenn Sie einen Verweis auf ein Objekt mit einer unapply (oder unapplySeq) Methode in einem Pattern-Match verwenden, wird es das:

  • Übergeben Sie das Objekt, das abgeglichen werden soll, an diese Methode
  • Wenn die unapply-Methode None zurückgibt, stimmt das Muster nicht überein
  • Wenn er Werte zurückgibt, werden diese für den weiteren Abgleich oder die Bindung weitergereicht.

Tatsächlich verfügt scala.util.matching.Regex über eine unapplySeq-Funktion, so dass sie als Extraktor verwendet werden kann.

Extraktoren für die Konvertierung

In unserer naiven Implementierung oben müssen wir .toInt für die übereinstimmenden Ganzzahlen wiederholt aufrufen. Wir können das Extractor Pattern nutzen, um diese Umwandlung während des Abgleichs durchzuführen: [code language="scala"] import scala.util.Try object ToInt { def unapply(in: String): Option[Int] = Try(in.toInt).toOption } val SwapPositions = "Tausche Position (d+) mit Position (d+)".r def applyCommand(in: String, command: String): String = command match { case SwapPositions(ToInt(x), ToInt(y)) => in.updated(x, in(y)).updated(y, in(x)) ... } [/code]

Extraktoren benötigen eine 'Stable Id'.

Sie haben vielleicht bemerkt, dass die eigentliche Regex und die Übereinstimmung nicht an derselben Stelle stehen. In größeren Anwendungen mag das ein Vorteil sein, aber in diesem Fall wäre es schöner, einen Einzeiler wie diesen zu haben: [code language="scala"] def applyCommand(in: String, command: String): String = command match { case "Tausche Position (d+) mit Position (d+)".r(x, y) => in.updated(x.toInt, in(y.toInt)).updated(y.toInt, in(x.toInt)) [/code]... } Leider funktioniert das oben genannte nicht, da die Syntax von Extractor Pattern wie folgt definiert ist: [code] SimplePattern ::= StableId '(' [Patterns] ')' [/code] Da "xxx".r keine StableId ist, können wir sie hier nicht inline verwenden.

String-Interpolation

Ein Trick, den wir hier anwenden können, ist die String-Interpolation. Sie haben vielleicht gesehen, dass Strings mit dem Präfix s in Scala eine besondere Bedeutung haben, aber Sie können Ihre eigenen Strings wie folgt definieren: [code language="scala"] implicit class RegexHelper(val sc: StringContext) extends AnyVal { def re: scala.util.matching.Regex = sc.parts.mkString.r } def applyCommand(in: String, command: String): String = command match { case re "tausche Position d+ mit Position d+" => [/code]??? ... } Das passt nun korrekt auf den String, erfasst aber nicht die von uns definierten Gruppen. Wir können dies erreichen, indem wir Ausdrücke zu der interpolierten Zeichenkette hinzufügen: [code language="scala"] implicit class RegexHelper(val sc: StringContext) extends AnyVal { def re: scala.util.matching.Regex = sc.parts.mkString.r } def applyCommand(in: String, command: String): String = command match { case re "swap position (d+)$x with position (d+)$y" => in.updated(x.toInt, in(y.toInt)).updated(y.toInt, in(x.toInt)) [/code]... } Das funktioniert, weil re "swap position (d+)$x with position (d+)$y" zu desugariert ist: [code language="scala"] StringContext("swap position (d+)", "with position (d+)", "").re (x, y) [/code] Beachten Sie, dass die Position der Variablen in der Zeichenkette eigentlich keine Rolle spielt: Sie in der Nähe der passenden Gruppen zu platzieren, ist eine reine Frage der Bequemlichkeit/Lesbarkeit. Wir können sogar weitere Übereinstimmungen in diesen Ausdrücken zulassen: [code language="scala"] import scala.util.Try object ToInt { def unapply(in: String): Option[Int] = Try(in.toInt).toOption } implicit class RegexHelper(val sc: StringContext) extends AnyVal { def re: scala.util.matching.Regex = sc.parts.mkString.r } def applyCommand(in: String, command: String): String = command match { case re "Tausche Position (d+)${ToInt(x)} mit Position (d+)${ToInt(y)}" => in.updated(x, in(y)).updated(y, in(x)) ... } [/code]

Weniger allgemeine Muster

Wenn wir nicht die ganze Macht der regulären Ausdrücke benötigen, um unsere Zeichenketten abzugleichen, können wir auch einfach die 'Löcher' in der interpolierten Zeichenkette mit ., was ergibt: [code language="scala"] import scala.util.Try object ToInt { def unapply(in: String): Option[Int] = Try(in.toInt).toOption } implicit class RegexHelper(val sc: StringContext) extends AnyVal { def re: scala.util.matching.Regex = sc.parts .map(java.util.regex.Pattern.quote) .reduce(_ + "(.)" + _) .mkString .r } def applyCommand(in: String, command: String): String = Befehl match { case re "Tausche Position ${ToInt(x)} mit Position ${ToInt(y)}" => in.updated(x, in(y)).updated(y, in(x)) ... } [/code] Dies ist der Ansatz, der mit Ammonite in ammonite.ops ausgeliefert wird.

Leistung

Da die String-Interpolation jedes Mal durchgeführt wird, wenn die Übereinstimmung ausgewertet wird, ist dies nicht der effizienteste Weg, um Strings zu vergleichen. Wenn Sie glauben, dass das Parsen von Zeichenketten ein Engpass für Ihre Anwendung sein könnte, sollten Sie ein Profil erstellen, um zu sehen, ob diese Lösung für Sie geeignet ist.

Fazit

Durch die Kombination von Pattern-Matching, String-Interpolation und regulären Ausdrücken haben wir eine leistungsfähige und prägnante Methode, um Strings in Scala abzugleichen. Während reguläre Ausdrücke bereits wie eine "Buchstabensuppe" aussehen können und wir die Dinge noch ein wenig schlimmer machen, indem wir interpolierte Ausdrücke inline hinzufügen, sollte dies aus Gründen der Lesbarkeit wahrscheinlich nur auf relativ einfache Muster angewendet werden.

Referenzen

Verfasst von

Arnout Engelen

Contact

Let’s discuss how we can support your journey.