- 原文地址:Learning Parser Combinators With Rust
- 原文做者:Bodil
- 译文出自:掘金翻译计划
- 本文永久连接:github.com/xitu/gold-m…
- 译者:suhanyujie
若是你没看过本系列的其余几篇文章,建议你按照顺序进行阅读:前端
如今咱们有了构建的代码块,咱们须要经过它用 one_or_more
解析空格符,并用 zero_or_more
解析属性对。android
事实上,得等一下。咱们并不想先解析空格符而后解析属性。若是你考虑到,在没有属性的状况下,空格符也是可选的,而且咱们可能会当即遇到 >
或 />
。但若是有一个属性时,在开头就必定会有空格符。幸运的是,每一个属性之间也必定会有空格符,若是有多个的话,那么咱们看看零个或者多个序列,该序列是在属性后跟随一个或者多个空格符。ios
首先,咱们须要一个针对单个空格的解析器。这里咱们能够从三种方式选择其中一种。git
第一,咱们能够最简单的使用 match_literal
解析器,它带有一个只包含一个空格的字符串。这看起来是否是很傻?由于空格符也至关因而换行符、制表符和许多奇怪的 Unicode 字符,它们都是以空白的形式呈现的。咱们将不得再也不次依赖 Rust 的标准库,固然,char
有一个 is_whitespace
方法,也是相似于它的 is_alphabetic
和 is_alphanumeric
方法。程序员
第二,咱们能够编写一个解析器,它是经过 is_whitespace
来断定解析任意数量的空格,就像咱们前面写到的 identifier
同样。github
第三,咱们能够更明智一点,咱们确实喜欢更明智的作法。咱们能够编写一个解析器 any_char
,它返回一个单独的 char
,只要输入中还有空格符,接着编写一个 pred
组合器,它接受一个解析器和一个断定函数,并将它们像这样组合起来:pred(any_char, |c| c.is_whitespace())
。这样作会有一个好处,它使咱们最终的解析器的编写变得更简单:属性值使用引用字符串。json
any_char
能够看作是一个很是简单的解析器,但咱们必须记住当心那些 UTF-8 陷阱。后端
fn any_char(input: &str) -> ParseResult<char> {
match input.chars().next() {
Some(next) => Ok((&input[next.len_utf8()..], next)),
_ => Err(input),
}
}
复制代码
对于如今咱们富有经验的眼睛来讲,pred
组合器没有给咱们带来惊喜。咱们调用解析器,而后在解析器执行成功时再对返回值调用断定函数,只有当该函数返回 true 时,咱们才真正返回成功,不然就会返回跟解析失败同样多的错误。bash
fn pred<'a, P, A, F>(parser: P, predicate: F) -> impl Parser<'a, A>
where
P: Parser<'a, A>, F: Fn(&A) -> bool, { move |input| { if let Ok((next_input, value)) = parser.parse(input) { if predicate(&value) { return Ok((next_input, value)); } } Err(input) } } 复制代码
快速地写一个测试用例来确保一切是有序进行的:ide
#[test]
fn predicate_combinator() {
let parser = pred(any_char, |c| *c == 'o');
assert_eq!(Ok(("mg", 'o')), parser.parse("omg"));
assert_eq!(Err("lol"), parser.parse("lol"));
}
复制代码
针对这两个地方,咱们能够用一个快速的一行代码来编写咱们的 whitespace_char
解析器:
fn whitespace_char<'a>() -> impl Parser<'a, char> {
pred(any_char, |c| c.is_whitespace())
}
复制代码
如今,咱们有了 whitespace_char
,咱们所作的离咱们的想法更近了,一个或多个空格,以及相似的想法,零个或者多个空格。咱们将其简化一下,分别将它们命名为 space1
和 space0
。
fn space1<'a>() -> impl Parser<'a, Vec<char>> {
one_or_more(whitespace_char())
}
fn space0<'a>() -> impl Parser<'a, Vec<char>> {
zero_or_more(whitespace_char())
}
复制代码
完成这些工做后,终于咱们如今能够解析这些属性了吗?是的,咱们只须要确保为属性组件编写好了单独的解析器。咱们已经获得了属性名的 identifier
(尽管很容易使用 any_char
和 pred
加上 *_or_more
组合器重写它)。=
也即 match_literal("=")
。不过,咱们只须要字符串解析器的引用,因此咱们要构建它。幸运的是,咱们已经实现了咱们所须要的组合器。
fn quoted_string<'a>() -> impl Parser<'a, String> {
map(
right(
match_literal("\""),
left(
zero_or_more(pred(any_char, |c| *c != '"')),
match_literal("\""),
),
),
|chars| chars.into_iter().collect(),
)
}
复制代码
在这里,组合器的嵌套有点烦人,但咱们暂时不打算重构它,而是将重点放在接下来要作的东西上。
最外层的组合器是一个 map
,由于以前提到嵌套很烦人,从这里开始会变得糟糕而且咱们要忍受并理解这一点,咱们试着找到开始执行的地方:第一个引号字符。在 map
中,有一个 right
,而 right
的第一部分是咱们要查找的:match_literal("\"")
。以上就是咱们一开始要着手处理的东西。
right
的第二部分是字符串剩余部分的处理。它位于 left
的内部,咱们会很快的注意到右侧的 left
参数,是咱们要忽略的,也就是另外一个 match_literal("\"")
—— 结束的引号。因此左侧参数是咱们引用的字符串。
咱们利用新的 pred
和 any_char
在这里获得一个解析器,它接收任何字符除了另外一个引号,咱们把它放进 zero_or_more
,因此咱们讲的也是如下这些:
而且,在 right
和 left
之间,咱们会在结果值中丢弃引号,而且获得引号之间的字符串。
等等,那不是字符串。还记得 zero_or_more
返回的是什么吗?一个类型为 Vec<A>
的值,其中类型为 A
的值是由内部解析器返回的。对于 any_char
,返回的是 char
类型。那么咱们获得的不是一个字符串,而是一个类型为 Vec<char>
的值。这是 map
所处的位置:咱们使用它把 Vec<char>
转换为 String
,基于这样一个状况,你能够构建一个产生 String
的迭代器 Iterator<Item = char>
,咱们称之为 vec_of_chars.into_iter().collect()
,多亏了类型推导的力量,咱们才有了 String
。
在咱们继续以前,咱们先写一个快速的测试用例来确保它是正确的,由于若是咱们须要这么多词来解释它,那么它可能不是咱们做为程序员应该相信的东西。
#[test]
fn quoted_string_parser() {
assert_eq!(
Ok(("", "Hello Joe!".to_string())),
quoted_string().parse("\"Hello Joe!\"")
);
}
复制代码
如今,我发誓,真的是要解析这些属性了。
咱们如今能够解析空格符、标识符,=
符号和带引号的字符串。最后,这就是解析属性所需的所有内容。
首先,咱们为属性对写解析器。咱们将会把属性做为 Vec<(String, String)>
存储,你可能还记得这个类型,因此感受可能须要一个针对 (String, String)
的解析器,将其提供给咱们可靠的 zero_or_more
组合器。咱们看看可否造一个。
fn attribute_pair<'a>() -> impl Parser<'a, (String, String)> {
pair(identifier, right(match_literal("="), quoted_string()))
}
复制代码
过轻松了,汗都没出一滴!总结一下:咱们已经有一个便利的组合器用于解析元组的值,也就是 pair
,咱们能够将其做为 identifier
解析器,迭代出一个 String
,以及一个带有 =
的 right
解析器,它的返回值咱们不想保存,而且咱们刚写出来的 quoted_string
解析器会返回给咱们 String
类型的值。
如今,咱们结合一下 zero_or_more
,去构建一个 vector —— 但不要忘了它们之间的空格符。
fn attributes<'a>() -> impl Parser<'a, Vec<(String, String)>> {
zero_or_more(right(space1(), attribute_pair()))
}
复制代码
如下状况会出现零次或者屡次:一个或者多个空白符,其后是一个属性对。咱们经过 right
丢弃空白符并保留属性对。
咱们测试一下它。
#[test]
fn attribute_parser() {
assert_eq!(
Ok((
"",
vec![
("one".to_string(), "1".to_string()),
("two".to_string(), "2".to_string())
]
)),
attributes().parse(" one=\"1\" two=\"2\"")
);
}
复制代码
测试是经过的!先别高兴太早!
实际上,有些问题,在这个状况中,个人 rustc 编译器已经给出提示信息表示个人类型过于复杂,我须要增长可容许的类型范围才能让编译继续。鉴于咱们在同一点上遇到了相似的错误,这是有利的,若是你是这种状况,你须要知道如何处理它。幸运的是,在这些状况下,rustc 一般会给出好的建议,因此当它告诉你在文件顶部添加 #![type_length_limit = "…some big number…"]
注解时,照作就好了。在实际状况中,就是添加 #![type_length_limit = "16777216"]
,这将使咱们更进一步深刻到复杂类型的平流层。全速前进,咱们就要上天了。
在这一点上,这些东西看起来即将要组合到一块儿了,有些解脱了,由于咱们的类型正快速接近于 NP 彻底性理论。咱们只须要处理两种元素标签:单个元素以及带有子元素的父元素,但咱们很是有信心,一旦咱们有了这些,解析子元素就只须要使用 zero_or_more
,是吗?
那么接下来咱们先处理单元素的状况,把子元素的问题放一放。或者,更进一步,咱们先基于这两种元素的共性写一个解析器:开头的 <
,元素名称,而后是属性。让咱们看看可否从几个组合器中获取到 (String, Vec<(String, String)>)
类型的结果。
fn element_start<'a>() -> impl Parser<'a, (String, Vec<(String, String)>)> {
right(match_literal("<"), pair(identifier, attributes()))
}
复制代码
有了这些,咱们就能够快速的写出代码,从而为单元素建立一个解析器。
fn single_element<'a>() -> impl Parser<'a, Element> {
map(
left(element_start(), match_literal("/>")),
|(name, attributes)| Element {
name,
attributes,
children: vec![],
},
)
}
复制代码
万岁,感受咱们已经接近咱们的目标了 —— 实际上咱们正在构建一个 Element
!
让咱们测试一下现代科技的奇迹。
#[test]
fn single_element_parser() {
assert_eq!(
Ok((
"",
Element {
name: "div".to_string(),
attributes: vec![("class".to_string(), "float".to_string())],
children: vec![]
}
)),
single_element().parse("<div class=\"float\"/>")
);
}
复制代码
…… 我想咱们已经逃离出平流层了。
single_element
返回的类型是如此的复杂,以致于编译器不能顺利的完成编译,除非咱们提早给出足够大内存空间的类型,甚至要求更大的类型。很明显,咱们不能再忽略这个问题了,由于它是一个很是简单的解析器,却须要数分钟的编译时间 —— 这会致使最终的产品可能须要数小时来编译 —— 这彷佛有些不合理。
在继续以前,你最好将这两个函数和测试用例注释掉,便于咱们进行修复……
若是你曾经尝试过在 Rust 中编写递归类型的东西,那么你可能已经知道这个问题的解决方案。
关于递归类型的一个简单例子就是单链表。原则上,你能够把它写成相似于这样的枚举形式:
enum List<A> {
Cons(A, List<A>),
Nil,
}
复制代码
很明显,rustc 编译器会对递归类型 List<A>
给出报错信息,提示它具备无限的大小,由于在每一个 List::<A>::Cons
内部均可能有另外一个 List<A>
,这意味着 List<A>
能够一直直到无穷大。就 rustc 编译器而言,咱们须要一个无限列表,而且要求它能分配一个无限列表。
在许多语言中,对于类型系统来讲,一个无限列表原则上不是问题,并且对 Rust 来讲也不是什么问题。问题是,前面提到的,在 Rust 中,咱们须要可以分配它,或者,更确切的说,咱们须要可以在构造类型时先肯定类型的大小,当类型是无限的时候,这意味着大小也必须是无限的。
解决办法是采用间接的方法。咱们不是将 List::Cons
改成 A
的一个元素和另外一个 A
的列表,反而是使用一个 A
元素和一个指向 A
列表的指针。咱们已知指针的大小,无论它指向什么,它都是相同的大小,因此咱们的 List::Cons
如今是一个固定大小的而且可预测的,无论列表的大小如何。把一个已有的数据变成将数据存储于堆上,而且用指针指向该堆内存的方法,在 Rust 中,就是使用 Box
处理它。
enum List<A> {
Cons(A, Box<List<A>>),
Nil,
}
复制代码
Box
的另外一个有趣特性是,其中的类型是能够抽象的。这意味着,咱们可让类型检查器处理一个很是简洁的 Box<dyn Parser<'a, A>>
,而不是处理当前的很是复杂的解析器函数类型。
听起来很不错。有什么缺陷吗?好吧,咱们可能会由于使用指针的方式而损失一两次循环,也可能会让编译器失去一些优化解析器的机会。可是想起 Knuth 的关于过早优化的提醒:一切都会好起来的。损失这些循环是值得的。你在这里是学习关于解析器组合器,而不是学习手工编写专业的 SIMD 解析器(尽管它们自己会使人兴奋)
所以,抛开目前咱们使用的简单函数,让咱们继续基于即将要完成的解析器函数来实现 Parser
。
struct BoxedParser<'a, Output> { parser: Box<dyn Parser<'a, Output> + 'a>, } impl<'a, Output> BoxedParser<'a, Output> { fn new<P>(parser: P) -> Self where P: Parser<'a, Output> + 'a, { BoxedParser { parser: Box::new(parser), } } } impl<'a, Output> Parser<'a, Output> for BoxedParser<'a, Output> {
fn parse(&self, input: &'a str) -> ParseResult<'a, Output> {
self.parser.parse(input)
}
}
复制代码
为了更好地实现,咱们建立了一个新的类型 BoxedParser
用于保存 Box 相关的数据。咱们利用其它的解析器(包括另外一个 BoxedParser
,虽然这没太大做用)来建立新的 BoxedParser
,咱们提供一个新的函数 BoxedParser::new(parser)
,它只是将解析器放在新类型的 Box
中。最后,咱们为它实现 Parser
,这样,它就能够做为解析器交换着使用。
这使咱们具有将解析器放入一个 Box
中的能力,而 BoxedParser
将会以函数的角色为 Parser
执行一些逻辑。正如前面提到的,这意味着将 Box 包装的解析器移到堆中,而且必须删除指向该堆区域的指针,这可能会多花费几纳秒的时间,因此实际上咱们可能想先不用 Box 包装全部数据。只是把一些更活跃的组合器数据经过 Box 包装就够了。
本做品版权归 Bodil Stokke 全部,在知识共享署名-非商业性-相同方式共享 4.0 协议之条款下提供受权许可。要查看此许可证,请访问 creativecommons.org/licenses/by…
1: 他不是你真正的叔叔 2: 请不要成为聚会上的那我的。
若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。