1. Constructing more parsers
现在我们有了解析器的“final form”:它是一个函数,它获取一个in-out子字符串并生成一个可选的匹配项。因此,让我们再创建几个解析器,这样我们就可以了解这是如何进行的。
首先,让我们构建一个解析器,该解析器将尝试解析字符串开头的double:
let double = Parser<Double> { str in
let prefix = str.prefix(while: { $0.isNumber || $0 == "." })
guard let match = Double(prefix) else { return nil }
str.removeFirst(prefix.count)
return match
}
让我们试试吧!
double.run("42")
// (match 42.0, rest "")
double.run("42.8743289247")
// (match 42.8743289247, rest "")
double.run("42.8743289247 Hello World")
// (match 42.8743289247, rest " Hello World")
此double解析器并不完美,因为我们无法使用具有多个小数点的字符串:
double.run("42.4.1.4.6")
// (nil, rest: "42.4.1.4.6")
再多做一点工作,我们就能把它做好,但我们现在就把它留在这里。
我们还可以制作一个解析器,解析字符串开头的常量字符串。这对于确保字符串中存在某些标记非常有用。这个解析器与其他两个解析器略有不同,因为我们实际上并不关心从字符串中获取解析的数据,而只关心解析是否成功。因此,解析器的类型为Parser
这个解析器的一个有趣之处是,它是一个生成解析器的函数。这允许我们为解析器的行为提供一些预先配置,在本例中,我们提供了希望在输入字符串开头匹配的字符串。
func literal(_ literal: String) -> Parser<Void> {
return Parser<Void> { str in
guard str.hasPrefix(literal) else { return nil }
str.removeFirst(literal.count)
return ()
}
}
例如:
literal("cat").run("cat dog")
// ((), rest " dog"
literal("cat").run("dog cat")
// (nil, rest "dog cat")
在接下来的过程中,我们会遇到一些其他的“解析器生成器”,比如literal,函数返回解析器或将解析器作为输入,以更高阶的方式生成全新的解析器。
我们也可以编写一些看似病态的解析器,但事实证明它们非常方便,我们稍后会看到。例如,我们可以制作一个始终成功且不消耗任何输入内容的解析器:
func always<A>(_ a: A) -> Parser<A> {
return Parser { _ in a }
}
always解析器总是成功的。
always("cat").run("dog")
// (match "cat", rest "dog")
这看起来似乎没有什么意义,但是解析器成功地使用了“cat”,我们还有“dog”要解析。
我们也可以做相反的事情:解析器永远不会成功,但会立即失败,并且不会使用任何输入:
func never<A>() -> Parser<A> {
return Parser { _ in nil }
}
因此,为了使用它,我们可以给出一个显式泛型并运行解析器。
(never() as Parser<Int>).run("dog")
// (nil, rest "dog")
使用这样的泛型函数有点尴尬,但这是必要的,因为Swift不支持“泛型变量”:
// let never<A> = Parser<A> { ... }
我们可以通过静态计算属性来近似这一点,这是我们经常使用的:
extension Parser {
static var never: Parser {
return Parser { _ in nil }
}
}
现在,使用never不会变得更好。
Parser<Int>.never.run("dog")
// (nil, rest "dog")
2. What's the point?
我们现在已经构建了五个解析器:一个从字符串开头扫描一个int的int解析器,一个扫描一个double的double解析器,一个从另一个字符串开头扫描一个精确字符串的literal解析器,然后是一个always解析器和一个never解析器,它们总是或永远不会成功。
好了,现在我们开始讨论一些奇怪的事情。我们定义的解析器总是成功的,但永远不会成功?在我们走得太远之前,让我们慢下来问问“这有什么意义?”。我们开始了这段插曲,演示了一些快速和基础的解析器。他们完成了任务,但我们声称他们不是很好的可扩展性或可组合性。因此,我们沿着这条道路构建了我们自己的解析器类型,尽管我们已经解析了一些东西,但我们仍然没有做任何太复杂的事情。那么,这是怎么回事?
好吧,这一集的真正意义是让我们都熟悉解析器的问题空间,并正确定义解析器是什么。就我们而言,它只是一个接受字符串并返回某种类型的可选值的函数,它可能会使用输入字符串的某个子集。
尽管到目前为止我们定义的类型似乎不能做太多的工作,但我们保证在该类型中潜伏着一个完整的可组合性世界,我们还未去探索它。但在我们讨论所有这些之前,我们认为这种类型已经显示出一些希望。
3. Revisiting coordinate parsing
让我们回到纬度/经度坐标解析函数,并将其更新为使用新的Parser类型。
我们之前将这个解析器定义为一个普通的ole函数,在这里我们使用拆分、检查数组计数、字符相等性检查等进行了大量的手工工作。
func parseLatLong(_ string: String) -> Coordinate? {
let parts = string.split(separator: " ")
guard parts.count == 4 else { return nil }
guard
let lat = Double(parts[0].dropLast()),
let long = Double(parts[2].dropLast())
else { return nil }
let latCardinal = parts[1].dropLast()
guard latCardinal == "N" || latCardinal == "S" else { return nil }
let longCardinal = parts[3]
guard longCardinal == "E" || longCardinal == "W" else { return nil }
let latSign = latCardinal == "N" ? 1.0 : -1
let longSign = longCardinal == "E" ? 1.0 : -1
return Coordinate(latitude: lat * latSign, longitude: long * longSign)
}
让我们使用我们构建的解析器重做这个函数。
我们需要做的第一件事是将字符串输入转换为Substring格式,因为这是解析器理解的:
func parseLatLong(_ coordString: String) -> Coordinate? {
var str = coordString[...]
}
然后我们可以首先从输入的前面解析出一个double:
func parseLatLong(_ coordString: String) -> Coordinate? {
var str = coordString[...]
guard let lat = double.run(&str)
else { return nil }
}
然后我们可以从输入中解析度符号和空格:
func parseLatLong(_ coordString: String) -> Coordinate? {
var str = coordString[...]
guard let lat = double.run(&str)
else { return nil }
guard literal("° ").run(&str) != nil
else { return nil }
在进一步讨论之前,让我们组合我们的guard声明来清理一下。
func parseLatLong(_ coordString: String) -> Coordinate? {
var str = coordString[...]
guard
let lat = double.run(&str),
literal("° ").run(&str) != nil
else { return nil }
现在我们需要从字符串中解析一个“N”或“S”字符,并将其转换为+1或-1。以前我们是通过多个步骤来实现的,但现在我们可以构建专门的解析器来实现这一点:
let northSouth = Parser<Double> { str in
guard
let cardinal = str.first,
cardinal == "N" || cardinal == "S"
else { return nil }
str.removeFirst(1)
return cardinal == "N" ? 1 : -1
}
我们可以简单地使用这个自包含、可重用的解析单元:
func parseLatLong(_ coordString: String) -> Coordinate? {
var str = coordString[...]
guard
let lat = double.run(&str),
literal("° ").run(&str) != nil,
let latSign = northSouth.run(&str)
else { return nil }
}
然后我们需要解析逗号和空格:
func parseLatLong(_ coordString: String) -> Coordinate? {
var str = coordString[...]
guard
let lat = double.run(&str),
literal("° ").run(&str) != nil,
let latSign = northSouth.run(&str),
literal(", ").run(&str) != nil
else { return nil }
}
然后我们通过解析一个double,然后是degree符号,然后是基数方向来重新进行这个过程。
func parseLatLong(_ coordString: String) -> Coordinate? {
var str = coordString[...]
guard
let lat = double.run(&str),
literal("° ").run(&str) != nil,
let latSign = northSouth.run(&str),
literal(", ").run(&str) != nil,
let long = double.run(&str),
literal("° ").run(&str) != nil,
let longSign = eastWest.run(&str)
else { return nil }
但我们需要定义eastWest:
let eastWest = Parser<Double> { str in
guard
let cardinal = str.first,
cardinal == "E" || cardinal == "W"
else { return nil }
str.removeFirst(1)
return cardinal == "E" ? 1 : -1
}
将所有这些结合起来,我们有:
func parseLatLong(_ coordString: String) -> Coordinate? {
var str = coordString[...]
guard
let lat = double.run(&str),
literal("° ").run(&str) != nil,
let latSign = northSouth.run(&str),
literal(", ").run(&str) != nil,
let long = double.run(&str),
literal("° ").run(&str) != nil,
let longSign = eastWest.run(&str)
else { return nil }
return Coordinate(latitude: lat * latSign, longitude: long * longSign)
}
现在,我们之前的解析现在失败了!
print(parseLatLong("40.6782% N- 73.9442% W"))
// nil
如果我们把东西换成正确的符号,它就会通过。
print(parseLatLong("40.6782° N, 73.9442° W"))
// Coordinate(latitude: 40.6782, longitude: -73.9442)
我认为这看起来已经比我们以前做的手工解析好了很多。首先,所有投入的增量消耗都是以线性、逐行的方式发生的,并且讲述了一个非常直接的故事。首先我们解析一个double,然后是度符号,然后是基数方向,然后是逗号,然后是另一个double,然后是度符号,然后是基数方向。一旦所有的数据都从字符串中解析出来,我们就把它们放在一起创建实际的Coordinate值。
这种风格的另一个优点是我们第一次看到了代码重用。我们构建的南北和东西解析器可以在任何地方使用,而不仅仅是解析这个特定的坐标格式。我们可以根据需要剥离任意多个小助手解析器。例如,现在我们重复两次**literal("° ")**解析器,因此我们可能应该将其提取出来:
Scanner式的解析有很多好处,但由于其API尚未针对Swift进行更新,因此使用起来相当麻烦。我们将为您省去从头开始编写代码的细节,但它看起来是这样的:
func parseLatLongWithScanner(_ string: String) -> Coordinate? {
let scanner = Scanner(string: string)
var lat: Double = 0
guard scanner.scanDouble(&lat) else { return nil }
guard scanner.scanString("° ", into: nil) else { return nil }
var northSouth: NSString? = ""
guard scanner.scanCharacters(from: ["N", "S"], into: &northSouth) else { return nil }
let latSign = northSouth == "N" ? 1.0 : -1
guard scanner.scanString(", ", into: nil) else { return nil }
var long: Double = 0
guard scanner.scanDouble(&long) else { return nil }
guard scanner.scanString("° ", into: nil) else { return nil }
var eastWest: NSString? = ""
guard scanner.scanCharacters(from: ["E", "W"], into: &eastWest) else { return nil }
let longSign = eastWest == "E" ? 1.0 : -1
return Coordinate(latitude: lat * latSign, longitude: long * longSign)
}
每次解析某些内容时,我们需要两行代码:一行用于设置可变变量,另一行用于执行扫描,这也需要一个guard来检查扫描是否成功。
因此,至少我们的解析器类型为解析提供了更符合人体工程学的界面,这更有利于共享解析器和支持代码重用。仅此一点就足以成为使用这种类型的理由,但这只是开始。现在我们已经为解析器的结构奠定了基础,我们可以开始定义一组有用的、可重用的解析器,并构建更复杂的解析器。我们将能够使用我们多次讨论过的所有通用操作符,比如map、zip和flatMap来实现这一点,事实证明解析器类型就是这样的!正是这些操作符释放了整个世界的可组合性,使我们能够将许多小解析器组合起来,构建一个巨大、复杂的解析器。这就是我们下次要讨论的!