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.
- Wolf Dieter Dallinger
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 View
s 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 Formatter
n 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.
Regex, Output und Match
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.
Klassische reguläre Ausdrücke
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.
Beim Übersetzen erkannt
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
.