- Оригинальный адрес:Learning Parser Combinators With Rust
- Оригинальный автор:Bodil
- Перевод с:Программа перевода самородков
- Постоянная ссылка на эту статью:GitHub.com/rare earth/gold-no…
- Переводчик:40m41h42t
- Корректор:Samuel Jie,dior
Если вы не читали другие статьи из этой серии, предлагаю прочитать их по порядку:
- Изучаем объединители синтаксических анализаторов с помощью Rust — часть 1
- Изучаем объединители синтаксических анализаторов с помощью Rust — часть 2
- Изучаем объединители парсеров с помощью Rust — часть 3
- Изучаем объединители синтаксических анализаторов с помощью Rust — часть 4
возможность проявить себя
Но, подождите минутку, это дает нам возможность исправить еще одну проблему, которая может быть немного хлопотной.
Помните последний парсер, который мы написали? Поскольку наши комбинаторы являются независимыми функциями, когда мы вкладываем некоторые «комбинаторы», наш код становится немного трудным для понимания. Вспомните нашиquoted_string
Парсер:
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(),
)
}
Было бы более читабельно, если бы мы могли создавать эти методы комбинатора в синтаксическом анализаторе вместо отдельной функции. Если мы объявим комбинатор какParser
А как насчет методов признаков?
Проблема в том, что если мы это сделаем, наше возвращаемое значение потеряет зависимостьimpl Trait
способность, как не разрешено в объявлениях признаковimpl Trait
.
Но теперь у нас естьBoxedParser
. мы не можем объявить возвратimpl Parser<'a, A>
типовых методов, но мы уверены,Можетобъявить о возвращенииBoxedParser<'a, A>
Методы.
В лучшем случае мы даже можем объявить эти методы с реализациями по умолчанию, чтобы нам не приходилось делать это для каждой реализации.Parser
Тип повторной реализации каждого комбинатора.
давайте использоватьmap
функцию, чтобы попробовать, расширяя нашуParser
анализатор черт:
trait Parser<'a, Output> {
fn parse(&self, input: &'a str) -> ParseResult<'a, Output>;
fn map<F, NewOutput>(self, map_fn: F) -> BoxedParser<'a, NewOutput>
where
Self: Sized + 'a,
Output: 'a,
NewOutput: 'a,
F: Fn(Output) -> NewOutput + 'a,
{
BoxedParser::new(map(self, map_fn))
}
}
Есть много'a
, но они оба необходимы. К счастью, мы все еще можем повторно использовать старые функции композиции — и с дополнительным преимуществом, заключающимся не только в улучшении синтаксиса для их применения, но и в избавлении от противоречивыхimpl Trait
Способ.
Теперь мы можем немного улучшить егоquoted_string
Парсер:
fn quoted_string<'a>() -> impl Parser<'a, String> {
right(
match_literal("\""),
left(
zero_or_more(pred(any_char, |c| *c != '"')),
match_literal("\""),
),
)
.map(|chars| chars.into_iter().collect())
}
На первый взгляд очевидно.map()
Будетright()
Результат вызова.
мы также можем датьpair
,left
иright
Сделайте то же самое, но с этими тремя, я думаю, будет более читабельно, когда они функции, так как они отражаютpair
Тип структуры вывода. Если вы не согласны, вполне нормально добавить их к черте, как мы используемmap
то же самое, и вы можете продолжать пробовать это в качестве упражнения.
Тем не менее, есть также функция ожидания, котораяpred
. Давайте сделаем этоParser
Черта добавляет определение:
fn pred<F>(self, pred_fn: F) -> BoxedParser<'a, Output>
where
Self: Sized + 'a,
Output: 'a,
F: Fn(&Output) -> bool + 'a,
{
BoxedParser::new(pred(self, pred_fn))
}
давайте использоватьpred
вызов переписатьquoted_string
Строка в следующем виде:
zero_or_more(any_char.pred(|c| *c != '"')),
Так читать немного лучше, и я думаю, что это следует сохранитьzero_or_more
- читается как "Ноль или большеany_char
и применил приведенное ниже решение», что звучит для меня правильно. Вы также можете продолжать помещатьzero_or_more
иone_or_more
Перейдите к чертам.
Помимо переписыванияquoted_string
В дополнение к ремонтуsingle_element
серединаmap
:
fn single_element<'a>() -> impl Parser<'a, Element> {
left(element_start(), match_literal("/>")).map(|(name, attributes)| Element {
name,
attributes,
children: vec![],
})
}
попробуем раскомментироватьelement_start
И протестируйте его с ранее прокомментированным тестовым кодом, чтобы увидеть, улучшатся ли результаты. Давайте восстановим код в игре и попробуем запустить тесты...
... ну да, время компиляции теперь вернулось к норме. Вы даже можете убрать установленный вверху файла размер шрифта, он вам вообще не нужен.
мы только что боксировали дваmap
иpred
——иУ нас есть лучшие правила сленга!
С дочерними элементами
Теперь давайте напишем парсер для начального тега родительского элемента. за исключением>
вместо/>
Кроме концовки, другие почтиsingle_element
такой же. За ним следует ноль или более дочерних элементов и закрывающий тег. Сначала нам нужно разобрать фактический начальный тег, давайте сделаем это.
fn open_element<'a>() -> impl Parser<'a, Element> {
left(element_start(), match_literal(">")).map(|(name, attributes)| Element {
name,
attributes,
children: vec![],
})
}
Теперь, как нам получить эти дочерние элементы? Это либо отдельный элемент, либо сам родительский элемент, у них также может быть ноль или более дочерних элементов, и у нас есть надежныеzero_or_more
Комбинатор, как его ввести? Есть еще одна вещь, с которой мы еще не разобрались, и это синтаксический анализатор множественного выбора:теперь, когдаможет разобрать один элементсноваРодительский элемент может быть проанализирован.
Для этого нам нужно, чтобы комбинатор пробовал последовательно два парсера: если первый парсер прошел успешно, задача выполнена, и возвращается ее результат. Если это не удастся, мы будем использоватьтот же вводПопробуйте второй синтаксический анализатор вместо возврата ошибки. Если это удается, это нормально, если нет, мы возвращаем ошибку, потому что это означает, что все наши синтаксические анализаторы потерпели неудачу, что является полным отказом.
fn either<'a, P1, P2, A>(parser1: P1, parser2: P2) -> impl Parser<'a, A>
where
P1: Parser<'a, A>,
P2: Parser<'a, A>,
{
move |input| match parser1.parse(input) {
ok @ Ok(_) => ok,
Err(_) => parser2.parse(input),
}
}
Это позволяет нам объявить парсерelement
, который соответствует одному элементу или родительскому элементу (сейчас мы просто используемopen_element
представлять его, когда у нас естьelement
Мы будем иметь дело с дочерними элементами).
fn element<'a>() -> impl Parser<'a, Element> {
either(single_element(), open_element())
}
Теперь добавим парсер закрывающего тега. У него есть интересное свойство: он должен совпадать с начальным тегом, а это означает, что синтаксический анализатор должен знать имя начального тега. Но для этого и нужны параметры функции, верно?
fn close_element<'a>(expected_name: String) -> impl Parser<'a, String> {
right(match_literal("</"), left(identifier, match_literal(">")))
.pred(move |name| name == &expected_name)
}
тотpred
Комбинаторы оказываются очень полезными, не так ли?
Теперь давайте объединим это для реализации полного синтаксического анализатора родительских элементов, синтаксического анализа дочерних элементов и всех других синтаксических анализаторов:
fn parent_element<'a>() -> impl Parser<'a, Element> {
pair(
open_element(),
left(zero_or_more(element()), close_element(…oops)),
)
}
Упс, как нам теперь передать этот параметр вclose_element
Шерстяная ткань? Думаю, это последний комбинатор, который мы собираемся реализовать.
Сейчас мы очень близки к завершению. Как только мы решим последний пустьparent_element
рабочая проблема, мы можем реализовать новыйparent_element
заменятьelement
в парсереopen_element
placeholder, и вот так мы реализовали полнофункциональный анализатор XML.
Помните, когда я сказал, что мы вернемся позжеand_then
? вернулся сейчасand_then
. Собственно, комбинатор нам нуженand_then
: нам нужно что-то с парсером, и такое, которое берет результат парсера и возвращаетновыйФункция парсера, после чего мы ее запустим. это немного похожеpair
, но он просто собирает два результата в кортеж, и мы объединяем их с помощью функции. Это тожеand_then
иResult
иOption
метод используется вместе, но его легче понять, потому чтоResult
иOption
не реальныйДелатьЧто угодно, это просто вещи, которые содержат некоторые данные (или нет, в зависимости от обстоятельств).
Итак, давайте попробуем написать его реализацию.
fn and_then<'a, P, F, A, B, NextP>(parser: P, f: F) -> impl Parser<'a, B>
where
P: Parser<'a, A>,
NextP: Parser<'a, B>,
F: Fn(A) -> NextP,
{
move |input| match parser.parse(input) {
Ok((next_input, result)) => f(result).parse(next_input),
Err(err) => Err(err),
}
}
Глядя на тип будет много переменных типа, но мы знаем входной парсерP
Тип результатаA
. Однако наша функцияF
,один из нихmap
есть один изA
прибытьB
функции, ключевое различие между ними заключается в том, чтоand_then
будет отA
получить функцию дляновый парсер NextP
, тип результата которогоB
. Окончательный тип результатаB
, поэтому можно считать, что изNextP
Все, что выводится, является конечным результатом.
Код немного сложнее: мы начинаем с запуска входного синтаксического анализатора, и если он терпит неудачу, он терпит неудачу и представляет, что мы закончили. Но если это удается, мы сначала вызываем функцию по результатуf
(типA
),f(result)
Возвращается новый парсер с типомB
результат. мы запускаем следующий вводэтосинтаксический анализатор и вернуть результат напрямую. Если это не удается, то оно терпит неудачу, и если это удается, мы получаем типB
значение .
Еще раз: мы сначала бежимP
тип парсера, в случае успеха начинаем с парсераP
Результат вызова функции в качестве аргументаf
чтобы получить наш следующий тип какNextP
Парсер, далее продолжаем запускать и получаем окончательный результат.
Давайте добавим его прямо вParser
черта, потому что это похоже наmap
Опять же, так определенно легче читать.
fn and_then<F, NextParser, NewOutput>(self, f: F) -> BoxedParser<'a, NewOutput>
where
Self: Sized + 'a,
Output: 'a,
NewOutput: 'a,
NextParser: Parser<'a, NewOutput> + 'a,
F: Fn(Output) -> NextParser + 'a,
{
BoxedParser::new(and_then(self, f))
}
Хорошо, какая польза делать это сейчас?
Сначала мыпочтиможет использовать его для достиженияpair
:
fn pair<'a, P1, P2, R1, R2>(parser1: P1, parser2: P2) -> impl Parser<'a, (R1, R2)>
where
P1: Parser<'a, R1> + 'a,
P2: Parser<'a, R2> + 'a,
R1: 'a + Clone,
R2: 'a,
{
parser1.and_then(move |result1| parser2.map(move |result2| (result1.clone(), result2)))
}
Выглядит довольно аккуратно, но есть одна проблема:parser2.map()
использоватьparser2
для создания обернутого синтаксического анализатора функция-оболочкаFn
, вместоFnOnce
, поэтому не разрешается использоватьparser2
Parser, мы можем только ссылаться на него. Другими словами, это проблема Rust. В языках более высокого уровня эти вещи не проблема, они могут быть определены более элегантным способом.pair
.
Однако даже в Rust мы можем использовать функцию задержки генерацииclose_element
Правильная версия парсера, или другими словами, мы можем получить парсер, передав аргумент.
Оглядываясь назад на наши предыдущие неудачные попытки:
fn parent_element<'a>() -> impl Parser<'a, Element> {
pair(
open_element(),
left(zero_or_more(element()), close_element(…oops)),
)
}
использоватьand_then
, теперь мы можем построить правильную версиюclose_element
для достижения этой цели.
fn parent_element<'a>() -> impl Parser<'a, Element> {
open_element().and_then(|el| {
left(zero_or_more(element()), close_element(el.name.clone())).map(move |children| {
let mut el = el.clone();
el.children = children;
el
})
})
}
Сейчас это выглядит немного сложно, потому чтоand_then
должен продолжать проходитьopen_element()
Звоните, мы найдем переход кclose_element
название. это означаетopen_element
Все остальные парсеры после этого должны бытьand_then
Конструкция внутри закрытия. Кроме того, поскольку закрытие сейчасopen_element
изElement
Единственный получатель результата, анализатор, который мы возвращаем, также должен передавать эту информацию дальше.
Мы на сгенерированном парсереmap
Внутреннее закрытие можно сослаться на внешнее закрытиеElement
(el
). Так как мыFn
На него можно ссылаться только один раз вclone()
Это. Мы берем результат внутреннего синтаксического анализатора (дочерний элементVec<Element>
) и добавляем его в наш клонElement
и вернуть его как окончательный результат.
Все, что нам нужно сделать сейчас, это вернуться кelement
парсер и убедитесь, чтоopen_element
изменить наparent_element
, так что он анализирует всю структуру элемента, а не только ее начало, я думаю, мы закончили с комбинатором парсера!
Вы спрашиваете, нужно ли заканчивать слово, начинающееся на М?
Помните, как мы говорили о том, какmap
На планете Haskell паттерны называются «функторами»?
and_then
Шаблоны — это еще одна вещь, которую вы время от времени будете видеть в Rust, и они часто связаны сmap
появляются в одном и том же месте. это в迭代器(Iterator)
известный какflat_map
, но это тот же шаблон, что и Rest.
Это причудливое слово — «монада». если у тебя естьThing<A>
, и у вас естьand_then
функция может преобразовать функцию изA
Перейти кThing<B>
, то вместо этого теперь у вас есть новыйThing<B>
, который является монадой.
например, когда у тебя естьOption<A>
, функция может быть вызвана немедленно, мы уже знаем, что этоSome(A)
все ещеNone
, так что если этоSome(A)
Мы применяем функцию напрямую и выводимSome(B)
.
Его также можно назвать отложенным вызовом. Например, если у вас все еще есть нерешенное решениеFuture<A>
, не пройдетand_then
Немедленно вызовите функцию для созданияFuture<B>
, но создать новыйFuture<B>
, включая обаFuture<A>
снова содержит функцию, затем ждетFuture<A>
Заканчивать. Когда это происходит, он звонит сFuture<A>
функция результата, а боб твой дядя[1], ты получишьFuture<B>
. Другими словами, вFuture
случае, вы можете перейти кand_then
функционировать какПерезвони, потому что он будет вызван с первоначальным будущим результатом, когда он завершится. Это также более интересно, потому что возвращаетновый Future
, он может быть или не быть проанализирован, так что этосоединятьМетод будущего состояния.
Однако, как и функторы, система типов Rust еще не может выражать монады, поэтому нам просто нужно помнить, что этот шаблон называется монадами, и довольно разочаровывает то, что вопреки тому, что было описано в Интернете, это то же самое, что и буррито. не важно. Давайте двигаться дальше.
космос, финал
Последняя вещь.
Теперь у нас должен быть синтаксический анализатор, способный анализировать некоторый XML, но он не совсем поддерживает пробелы. Между тегами должно быть разрешено любое количество пробелов, чтобы мы могли свободно вставлять новые строки между нашими тегами (в принципе, пробелы должны быть разрешены между идентификаторами и литералами, например<div />
, но пропустим).
На этом этапе мы должны быть в состоянии без особого труда собрать быстрый комбинатор.
fn whitespace_wrap<'a, P, A>(parser: P) -> impl Parser<'a, A>
where
P: Parser<'a, A>,
{
right(space0(), left(parser, space0()))
}
если мы будемelement
завернутый в него, он будет игнорироватьelement
вокруг всех начальных и конечных пробелов, что означает, что мы можем использовать столько новых строк и отступов, сколько пожелаем.
fn element<'a>() -> impl Parser<'a, Element> {
whitespace_wrap(either(single_element(), parent_element()))
}
Мы наконец закончили!
Я думаю, мы сделали это! Давайте отметим это, написав интеграционный тест.
#[test]
fn xml_parser() {
let doc = r#"
<top label="Top">
<semi-bottom label="Bottom"/>
<middle>
<bottom label="Another bottom"/>
</middle>
</top>"#;
let parsed_doc = Element {
name: "top".to_string(),
attributes: vec![("label".to_string(), "Top".to_string())],
children: vec![
Element {
name: "semi-bottom".to_string(),
attributes: vec![("label".to_string(), "Bottom".to_string())],
children: vec![],
},
Element {
name: "middle".to_string(),
attributes: vec![],
children: vec![Element {
name: "bottom".to_string(),
attributes: vec![("label".to_string(), "Another bottom".to_string())],
children: vec![],
}],
},
],
};
assert_eq!(Ok(("", parsed_doc)), element().parse(doc));
}
Это не удастся из-за отсутствия закрывающих тегов, просто чтобы убедиться, что мы можем это сделать:
#[test]
fn mismatched_closing_tag() {
let doc = r#"
<top>
<bottom/>
</middle>"#;
assert_eq!(Err("</middle>"), element().parse(doc));
}
Хорошей новостью является то, что возникает ошибка, когда в возвращаемом значении отсутствует закрывающий тег. Плохая новость заключается в том, что на самом деле это неуточнитьПроблема была вызвана отсутствием закрывающих тегов, просто отметил ошибку вгде. Хотя это лучше, чем ничего, но, честно говоря, это будет выглядеть довольно плохо из-за продолжающейся дезинформации. Но на самом деле заставить его генерировать правильное сообщение об ошибке - это другая тема, может быть, по крайней мере, статья длиной.
Давайте сосредоточимся на хороших новостях: мы пишем компилятор с нуля, используя комбинаторы парсеров! Мы знаем, что синтаксические анализаторы образуют функтор и монаду, так что теперь вы можете произвести впечатление на людей на вечеринках своими пугающими познаниями в теории категорий.[2].
Самое главное, теперь мы знаем, как работают комбинаторы парсеров с нуля. Нас больше никто не остановит!
щенок победы
больше ресурсов
Во-первых, я очень строго объясняю вам монады в строгих терминах Rusty, я знаю, если я не указал вам наего основополагающая статья, то Фил Уодлер будет очень расстроен мной, потому что статья более захватывающая — в ней рассказывается, как они связывают комбинаторы парсеров.
Идея этой статьи иpom
Идея комбинаторов парсеров очень похожа, и я очень рекомендую ее, если вы хотите использовать комбинаторы парсеров в том же стиле.
Современное состояние среди комбинаторов парсеров Rust по-прежнемуnom
, в той мере, в какой упоминалось ранееpom
По-видимому, это производное название (нет лучшего комплимента). Но для этого потребовался совершенно иной подход, чем тот дизайн, который мы имеем здесь сегодня.
Еще одна популярная библиотека комбинаторов парсеров для Rust —combine
, тоже стоит посмотреть.
Основополагающая библиотека комбинаторов синтаксических анализаторов HaskellParsec.
Наконец, моя книга о Грэме ХаттонеПрограммирование на ХаскелеЯ впервые узнал о комбинаторах синтаксических анализаторов в , эту книгу стоит прочитать, и она может рассказать вам о Haskell.
- Изучаем объединители синтаксических анализаторов с помощью Rust — часть 1
- Изучаем объединители синтаксических анализаторов с помощью Rust — часть 2
- Изучаем объединители парсеров с помощью Rust — часть 3
- Изучаем объединители синтаксических анализаторов с помощью Rust — часть 4
протокол
Эта статья защищена авторским правом Bodil Stokke и находится под лицензией Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. Чтобы просмотреть копию этой лицензии, посетитеCreative Commons.org/licenses/не….
сноска
Если вы обнаружите ошибки в переводе или в других областях, требующих доработки, добро пожаловать наПрограмма перевода самородковВы также можете получить соответствующие бонусные баллы за доработку перевода и PR. начало статьиПостоянная ссылка на эту статьюЭто ссылка MarkDown этой статьи на GitHub.
Программа перевода самородковэто сообщество, которое переводит высококачественные технические статьи из Интернета сНаггетсДелитесь статьями на английском языке на . Охват контентаAndroid,iOS,внешний интерфейс,задняя часть,блокчейн,продукт,дизайн,искусственный интеллектЕсли вы хотите видеть более качественные переводы, пожалуйста, продолжайте обращать вниманиеПрограмма перевода самородков,официальный Вейбо,Знай колонку.