Ich wollte einen Combinator-Parser mit Apple neuer Programmiersprache schreiben. Einfach war das nicht. Zugegeben habe ich das iBook mit der Sprachbeschreibung nur überflogen, aber hey, eigentlich ist das doch nur eine neue Syntax für Objective-C.
Der Anfang ist vielversprechend:
println("Hallo, Welt")
Ich muss mich allerdings zwingen, am Ende kein Semikolon zu schreiben.
Doch bereits die nächste Aufgabe stellt mich vor ein großes Problem. Wie greife ich auf das erste Zeichen eines Strings zu? Der naive Ansatz funktioniert nicht:
println("Hallo"[0])
Angeblich hat der String keine subscript
-Methode. Offenbar darf ich eine String-Konstante nicht mit []
(besagter subscript
-Methode in Operatorform) nicht kombinieren.
Der zweite Versuch geht auch nicht:
let s = "Hallo"
println(s[0])
Nun mag []
nicht das Argument. Ein bisschen Suchen in der unkommentierten und wahrscheinlich automatisch generierten Beschreibung von String
zeigt, dass subscript
ein Argument vom Typ String.Index
erwartet. Das ist ein Subtyp von BidirectionalIndex
und das einer von ForwardIndex
, beide haben aber so weit ich das überblicke als Protokolle keine Methoden. Vielleicht kann ich es mit String.Index(0)
erzeugen? Nein.
Weiteres stochern bringt
let s = "Hallo"
println(s[s.startIndex])
Was tatsächlich ein H
ausgibt. Dies ist ein Ding vom Typ Character
, was sich dadurch auszeichnet, dass ich damit quasi gar nichts machen kann, außer es zu vergleichen.
Doch wie erzeuge ich einen Character
? So schon einmal nicht:
let c1 = 'A'
let c2 = Character(65)
So ist es richtig:
let c3 = Character(UnicodeScalar(65))
Eigentlich würde ich viel lieber mit UnicodeScalar
arbeiten, denn das klingt, als ob es ein einzelnes Zeichen wäre. Zudem hat dieser Typ Methoden wie z.B. isDigit
.
Noch mehr stochern bringt, dass ich einem String einen ?UnicodeScalarView
abringen kann, bei dem ich wieder mit startIndex
auf das erste Zeichen zugreifen kann:
let s = "Hallo"
let u = s.unicodeScalars
let c = u[u.startIndex]
println(c)
Das funktioniert, kommt mir aber immer noch viel zu kompliziert vor. Warum gibt es Character
, wenn man damit quasi nichts anfangen kann?
Ein Parser ist eine Funktion, die eine Eingabe konsumiert und ein Paar aus Ergebnis und restlicher Eingabe zurück liefert, oder die Eingabe nicht antastet und einen Fehler liefert.
Hier ist die Eingabe:
class Input {
let source: String
let index: Int
init(source: String, index: Int = 0) {
self.source = source
self.index = index
}
func ch() -> UnicodeScalar {
let u = source.substringFromIndex(index).unicodeScalars
return u[u.startIndex]
}
func skip(offset: Int) -> Input {
return Input(source: source, index: index + offset)
}
}
Für das Ergebnis bietet sich ein enum
an:
enum Result<T> {
case Success(T, Input)
case Failure(String)
}
Leider kompiliert dies nicht, sondern Swift stürzt beim Übersetzen mit einem LLVM-Fehler ab. Generische Typen werden nicht unterstützt. Also muss ich sie weglassen:
enum Result {
case Success(AnyObject, Input)
case Failure(String)
}
Bleibt der Parser:
typealias Parser = Input -> Result
An dieser Stelle ist übrigens mein Programm in Xcode so groß, dass Xcode scrollen müsste, das aber bei Swift-Programmen noch nicht richtig kann und der Cursor so zu springen beginnt, dass man kaum tippen kann.
Hier ist eine Funktion, die einen Parser erzeugt, der ein einzelnes Zeichen akzeptiert:
func char(ch: UnicodeScalar) -> Parser {
return { (input: Input) -> Result in
if input.char() == ch {
return .Success(String(ch), input.skip(1))
}
return .Failure("expected \(ch)")
}
}
Um Parser zu kombinieren, ist hier eine Funktion, die zwei Parser nacheinander ausführt:
func seq(p1: Parser, p2: Parser) -> Parser {
return { (input: Input) -> Result in
let r1 = p1(input)
switch r1 {
case let .Success(o1, i1):
let r2 = p2(i1)
switch r2 {
case let .Success(o2, i2):
return .Success([o1, o2], i2)
case .Failure(_):
return r2
}
case .Failure(_):
return r1
}
}
}
Xcode kommt mit dem Schreiben von switch
-Anweisungen nicht klar und schlägt falsche Einrückungen und Code-Completions vor, was total nervig ist und mich zusammen mit dem Springen des Cursors denken lässt, dass zumindest die erste Xcode 6 Beta ungenügend ist, um mit Swift zu arbeiten.
Nun kann ich Parser erzeugen und benutzen:
let p = seq(char("A"), char("B"))
switch p(Input(source: "AB")) {
case let .Success(success, _):
println(success)
case let .Failure(error):
println(error)
}
Mittels Operator Overloading kann ich das Kombinieren vereinfachen:
@infix func + (p1: Parser, p2: Parser) -> Parser {
return seq(p1, p2)
}
let p = char("A") + char("B")
Die nächsten Schritte wären, mehr Parser-Funktionen zu implementieren, insbesondere noch die Alternative und die Wiederholung. Außerdem braucht man noch einen Parser, der Rekursion in der Definition erlaubt und einen, mit dem man semantische Aktionen an Parser hängen kann.