[譯] 經過 Rust 學習解析器組合器 — 第三部分

經過 Rust 學習解析器組合器 — 第三部分

若是你沒看過本系列的其餘幾篇文章,建議你按照順序進行閱讀:前端

斷定組合器

如今咱們有了構建的代碼塊,咱們須要經過它用 one_or_more 解析空格符,並用 zero_or_more 解析屬性對。android

事實上,得等一下。咱們並不想先解析空格符而後解析屬性。若是你考慮到,在沒有屬性的狀況下,空格符也是可選的,而且咱們可能會當即遇到 >/>。但若是有一個屬性時,在開頭就必定會有空格符。幸運的是,每一個屬性之間也必定會有空格符,若是有多個的話,那麼咱們看看零個或者多個序列,該序列是在屬性後跟隨一個或者多個空格符。ios

首先,咱們須要一個針對單個空格的解析器。這裏咱們能夠從三種方式選擇其中一種。git

第一,咱們能夠最簡單的使用 match_literal 解析器,它帶有一個只包含一個空格的字符串。這看起來是否是很傻?由於空格符也至關因而換行符、製表符和許多奇怪的 Unicode 字符,它們都是以空白的形式呈現的。咱們將不得再也不次依賴 Rust 的標準庫,固然,char 有一個 is_whitespace 方法,也是相似於它的 is_alphabeticis_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,咱們所作的離咱們的想法更近了,一個或多個空格,以及相似的想法,零個或者多個空格。咱們將其簡化一下,分別將它們命名爲 space1space0

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_charpred 加上 *_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("\"") —— 結束的引號。因此左側參數是咱們引用的字符串。

咱們利用新的 predany_char 在這裏獲得一個解析器,它接收任何字符除了另外一個引號,咱們把它放進 zero_or_more,因此咱們講的也是如下這些:

  • 一個引號
  • 隨後是零個或多個除了結束引號之外的字符
  • 隨後是結束引號

而且,在 rightleft 之間,咱們會在結果值中丟棄引號,而且獲得引號之間的字符串。

等等,那不是字符串。還記得 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 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索