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来实现这一点,事实证明解析器类型就是这样的!正是这些操作符释放了整个世界的可组合性,使我们能够将许多小解析器组合起来,构建一个巨大、复杂的解析器。这就是我们下次要讨论的!