- 原文地址: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、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。