Skip to content

Instantly share code, notes, and snippets.

@sma
Created June 8, 2014 22:31
Show Gist options
  • Save sma/1bd0dae954ebad5feee2 to your computer and use it in GitHub Desktop.
Save sma/1bd0dae954ebad5feee2 to your computer and use it in GitHub Desktop.
Playing around with Swift which is more painful than I thought

SWIFT

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.

Strings sind kompliziert

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?

Parser

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")

Wie geht es weiter

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment