Programmiersprachen: Reguläre Ausdrücke in Swift neu aufgestellt

Apple führt in Swift 5.7 eine domänenspezifische Sprache ein, um reguläre Ausdrücke sauber zu verarbeiten.

In Pocket speichern vorlesen Druckansicht 17 Kommentare lesen

(Bild: Shutterstock.com/calimedia)

Lesezeit: 7 Min.
Von
  • Wolf Dieter Dallinger
Inhaltsverzeichnis

Reguläre Ausdrücke sind ein mächtiges Werkzeug im Umgang mit Zeichenketten. Doch die bisherige Umsetzung in Apples Programmiersprache Swift ist ein Erbe von Objective-C mit all den Nachteilen: Die Regex sind kryptisch, bleiben schwer zu verstehen und damit eine häufige Quelle von Fehlern. Da die Analyse nicht beim Kompilieren erfolgt, sondern erst zur Laufzeit, sind die Rückgabewerte unzureichend typisiert.

Zu den Paradigmen von Swift gehört, dass der Sourcecode verständlich sein soll und dass Fehler beim Kompilieren statt zur Laufzeit erkannt werden sollen. Zu Letzterem trägt die starke, statische Typisierung bei.

Swift erhielt 2019 Möglichkeiten zum Erstellen domänenspezifischer Sprachen (Domain Specific Languages, DSL), mit denen sich komplexe Strukturen einfach und gut lesbar zusammensetzen lassen. Eine erste Anwendung hiervon war die Komposition von Views in SwiftUI. Mit Version 5.7, die Apple auf der WWDC 2022 vorgestellt hat und die im Herbst erscheinen soll, bekommt Swift mit RegexBuilder eine DSL für reguläre Ausdrücke, die intuitiv verständlich ist. Dazu gesellen sich die neuen generischen Typen Regex<Output> und Regex<Output>.Match. Mit Regex Literal steht zudem ein neues Literal zur Verfügung, mit dem der Compiler klassische reguläre Ausdrücke beim Übersetzen analysieren kann. Der Output ist strikt typisiert, da er beim Kompilieren bekannt ist.

Ein Sahnehäubchen ist das Einbinden von Formattern aus dem Framework Foundation. Die Formatter parsen Typen wie Date und Double in lokalen Formaten und zwar neuerdings auch als Teil eines regulären Ausdrucks. Damit ist es beispielsweise einfach, eine Fließkommazahl oder ein Datum über einen regulären Ausdruck zu erkennen, auch wenn die Zeichenkette in Indonesisch lokalisiert ist. Der Match liefert als Output das fertige Double beziehungsweise Date.

Der Großteil der Neuerungen ist Bestandteil der Standard-Library von Swift und steht daher unter macOS, Linux und Windows zur Verfügung. Swift 5.7 hat bis zum Herbst 2022 Betastatus. Zum Ausprobieren eignet sich die aktuelle Betaversion von Xcode 14, die im Entwicklerportal von Apple zu finden ist.

Das neue generische struct für reguläre Ausdrücke ist [Regex<Output>]. Meistens ist ein regulärer Ausdruck zur Übersetzungszeit bekannt. Ist dies der Fall, dann ist Output ein Tupel. Normalerweise würde man als Elemente des Tupel den Typ Substring oder bei Alternativen sein Optional Substring? erwarten. Regex kann aber ebenso Fließkommazahlen und Datumsangaben erkennen und liefert dafür Double beziehungsweise Date zurück.

Für einen zur Laufzeit erstellten regulären Ausdruck, der beispielsweise einen String mit einem klassischen Regex verarbeitet, ist Output vom Typ AnyRegexOutput. Letzterer ist konform zu Collection und enthält die erkannten Ergebnisse als Elemente, die über den Index ansprechbar sind.

Wendet man einen Regex auf einen String an, erhält man bei Erfolg ein struct Regex<Output>.Match oder bei Misserfolg nil. Die Eigenschaft output von Match enthält das eigentliche Ergebnis vom passenden Output-Typ.

Ein klassischer regulärer Ausdruck in Form eines String kann einen Regex initialisieren:

let regex = try Regex("b(.*)d")
// Erkennt "b", null oder mehr beliebige Charaktere,
// die zurückgegeben werden, und ein "d".
if let match = "abcde".firstMatch(of: regex) {
  print(match.output.count) // 2
  print(match.output[0].substring) // Optional("bcd")
  print(match.output[1].substring) // Optional("c")
}

Der Code analysiert den regulären Ausdruck erst zur Laufzeit. Deshalb ist der eigentliche Output beim Kompilieren nicht bekannt und erhält den Typ AnyRegexOutput. Letzterer ist indiziert und enthält mehrere Elemente. Das erste steht für den gesamten Text, der anhand des Regex erkannt wurde. Die folgenden Elemente sind die explizit als zurückzugeben markierten Stellen. In obigem Code ist das der geklammerte Teil "(.*)". Die Eigenschaft substring eines Elements liefert den erkannten Text als optionalen Substring?.

Da init im Codebeispiel einen Fehler erzeugt, falls der klassische reguläre Ausdruck fehlerhaft ist, muss der Aufruf mit try markiert sein.

Ein String Literal interpretiert Backslashes als spezielle Zeichen. Daher sei noch auf den Extended String Delimiter verwiesen, der spezielle Zeichen in eine Zeichenkette einbettet, die direkt übernommen und nicht ausgewertet werden.

Wünschenswert ist, den regulären Ausdruck bereits beim Kompilieren auszuwerten, weil damit die Typen der zu erkennenden Stellen ebenfalls bekannt sind und Output ein konkretes Tupel sein kann. Swift 5.7 führt dafür das Regex Literal ein, das mit einem Slash statt eines Anführungszeichens beginnt und endet.

Obiges Beispiel sieht mit Regex Literal folgendermaßen aus:

let regex = /b(.*)d/
if let match = "abcde".firstMatch(of: regex) {
  print(match.output.0) // "bcd"
  print(match.output.1) // "c"
}

Der Output von regex ist vom Typ (Substring, Substring). Beachtenswert ist dabei, dass Substring für den regulären Ausdruck nicht optional ist, da alle Stellen immer erkannt werden, wenn die Anwendung des Regex erfolgreich ist. Im Fall eines Fehlschlags liefert die Ausführung nil statt Match zurück.

Sowohl bei klassischen regulären Ausdrücken als auch beim Regex Literal imitiert Match seinen Output. Der Term .output darf daher meistens entfallen: match.0 statt match.output.0 beziehungsweise match[0].substring statt match.output[0].substring.