[Из песочницы] Изучение комбинаторных парсеров с Rust

?v=1

Привет, Хабр! Представляю вашему вниманию перевод статьи «Learning Parser Combinators With Rust».

Эта статья учит основам комбинаторных парсеров людей, которые уже знакомы с Rust. Предполагается, что никаких других знаний не требуется, а всё, что не имеет прямого отношения к Rust, а также некоторые неожиданные аспекты его использования, будут объяснены. Эта статья не поможет вам выучить Rust, если вы его ещё не знаете, и в этом случае, вы, вероятнее всего, не поймёте комбинаторные парсеры хорошо. Если вы хотите изучить Rust, я рекомендую книгу «Язык программирования Rust».


С точки зрения новичка

В жизни каждого программиста наступает момент, когда он нуждается в парсере.

Начинающий программист спросит: «Что такое парсер?»

Программист среднего уровня скажет: «Это просто, я напишу регулярное выражение».

Мастер-программист скажет: «Отойди, я знаю lex и yacc».

Новичок мыслит правильнее всех.

Не то, чтобы регулярные выражения плохи. (Но, пожалуйста, не пытайтесь писать сложный парсер как регулярное выражение.) Не то, чтобы не было никакой радости в использовании мощных инструментов, таких как генераторы парсеров и лексеров, которые были отточенные до совершенства на протяжении тысячелетий. Но изучение основ парсеров — это удовольствие. Это также то, что вы будете упускать, если у вас паника при использовании регулярных выражений или генераторов синтаксических анализаторов, оба из которых являются только абстракциями над реальной проблемой. В сознании новичка, как сказал человек, много возможностей. По мнению эксперта, есть только один правильный вариант.

В этой статье мы узнаем, как построить парсер с нуля с
использованием метода, распространённого в функциональных языках программирования, известного как комбинаторный парсер. У них есть преимущество быть удивительно мощными, как только вы схватите их основную идею, в то же время оставаясь очень близко к базовому принципу. Поскольку единственными абстракциями здесь будут те, которые вы создадите сами. Основные комбинаторы, которые будете использовать здесь вы построите сами.


Как работать с этой статьёй

Настоятельно рекомендуется начать с нового проекта Rust и писать фрагменты кода в src/lib.rs по мере чтения (вы можете вставить его непосредственно со страницы, но вводить его лучше, так как это автоматически гарантирует вам что вы его прочитайте полностью). Все части кода, которые Вам понадобятся, расположены в статье по порядку. Имейте в виду, что иногда вводятся изменённые версии функций, которые вы написали ранее, и в этих случаях вы должны заменить старую версию на новую.

Код был написан для rustc версии 1.34.0 с использованием 2018 редакции языка. Вы можете использовать любую версию компилятора, убедитесь, что вы используете выпуск с поддержкой 2018 редакции (проверьте, что ваш Cargo.toml содержит edition = "2018"). Для данного проекта не нужны внешние зависимости.

Для выполнения тестов, представленных в статье, как и следовало ожидать, используется cargo test.


Xcruciating язык разметки

Мы собираемся написать парсер для упрощённой версии XML. Это выглядит так:


  

XML-элементы открываются символом < и идентификатором, состоящим из буквы, за которой следует любое количество букв, цифр и -. За этим следуют пробелы и необязательный список пар атрибутов: другой идентификатор как определено ранее, за которым следуют = и строка в двойных кавычках. Наконец, есть либо закрывающий символ /> для обозначения одного элемента без детей, либо > для обозначения существования следующей последовательности дочерних элементов, и закрывающий тег, начинающийся с , за которым следует идентификатор, который должен соответствовать открывающему тегу, и закрывающий символ >.

Это все, что мы будем поддерживать. Не будет ни пространств имён, ни текстовых узлов и совершенно точно не будет проверки схемы. Мы даже не будем беспокоиться о поддержке экранирования кавычек для строк — они начинаются с первой двойной кавычки, и они заканчиваются на следующей, и все.

Мы собираемся разобрать эти элементы в структуру, которая выглядит следующим образом:

#[derive(Clone, Debug, PartialEq, Eq)]
struct Element {
    name: String,
    attributes: Vec<(String, String)>,
    children: Vec,
}

Никаких причудливых типов, только строка для имени (это идентификатор в начале каждого тега), атрибуты в виде пар строк (идентификатор и значение) и список дочерних элементов, которые выглядят точно так же, как родительский.

(Если вы печатаете, убедитесь, что вы включили секцию derive. Она понадобятся вам позже.)


Определение парсера

Ну что ж, пришло время написать парсер.

Разбор — это процесс получения структурированных данных из потока информации. Парсер — это то, что выявляет эту структуру.

В дисциплине, которую мы собираемся исследовать, синтаксический анализатор, в его самой простой форме, является функцией, которая принимает некоторый ввод и возвращает либо проанализированный вывод вместе с оставшейся частью ввода, либо ошибку, говорящую «Я не смог разобрать это».

Так же выглядит синтаксический анализатор в его более сложных формах. Вы можете усложнить, что означают ввод, вывод и ошибка, и если вам нужны хорошие сообщения об ошибках, которые вам понадобятся, но синтаксический анализатор останется прежним: что-то, что потребляет ввод и выдаёт какой-то проанализированный вывод вместе с тем, что осталось от ввода или позволяет вам знать, что он не может проанализировать ввод в вывод.

Давайте запишем это как тип функции.

Fn(Input) -> Result<(Input, Output), Error>

Более конкретно: в нашем случае мы хотим заполнить типы и получить функцию, похожую на эту. Всё, что мы собираемся сделать, это преобразовать строку в структуру Element. В данный момент мы не хотим вдаваться в тонкости сообщений об ошибках, поэтому просто вернём часть строки, которую не смогли разобрать. В результате наша функция будет выглядеть следующим образом:

Fn(&str) -> Result<(&str, Element), &str>

Мы используем строковый срез, потому что это эффективный указатель на фрагмент строки, и далее мы можем разделять его, как нам угодно, «потребляя» входные данные, вырезая проанализированный фрагмент и возвращая остаток вместе с результатом.

Возможно, было бы чище использовать &[u8] (срез байт, соответствующий символам ASCII) в качестве типа ввода, особенно потому что строковые срезы ведут себя немного иначе, чем большинство срезов — особенно в том, что вы не можете индексировать их с помощью одного числа input[0], вы должны использовать фрагмент input[0..1]. С другой стороны, у них есть много методов, которые полезны для разбора строк, которые не имеют срезы байтов.

На самом деле, мы будем полагаться на методы, а не на использование индексов символов, потому что Unicode. В UTF-8, а все строки Rust являются корректными UTF-8 строками, эти индексы не всегда соответствуют отдельным символам, и всем заинтересованным сторонам лучше, чтобы мы попросили стандартную библиотеку просто поработать с этим для нас.


Наш первый парсер

Давайте попробуем написать парсер, который просто смотрит на первый символ в строке
и решает, является ли он буквой a.

fn the_letter_a(input: &str) -> Result<(&str, ()), &str> {
  match input.chars().next() {
      Some('a') => Ok((&input['a'.len_utf8()..], ())),
      _ => Err(input),
  }
}

Во-первых, давайте посмотрим на типы ввода и вывода: мы берём строковый срез как ввод и мы возвращаем Result с (&str, ()), либо ошибку с &str. Пара (&str, ()) — это интересный момент: как мы говорили о том, что мы должны вернуть кортеж следующего содержания,
результат разбора и оставшуюся часть ввода. &str — это следующий вход, и результат — это просто тип блока (), потому что если этот синтаксический анализатор успешно работает, он может иметь только один результат (мы нашли букву a), и нам не особенно нужно возвращать
буква a в этом случае нам просто нужно указать, что нам удалось её найти.

Итак, давайте посмотрим на код самого парсера. Мы не шутили о том, что опираясь на стандартную библиотеку мы сможем избежать появления головной боли с Unicode: мы получаем итератор по символам строки при помощи метода chars() и берём из него первый символ. Это будет элемент типа char, завёрнутый в Option, в котором None будет означать, что мы пытались вытащить char из пустой строки.

Что ещё хуже, char не обязательно даже то, что вы думаете о нем как о символе Unicode. Это, скорее всего, будет то, что Unicode называет «grapheme cluster», который может состоять из нескольких char, которые на самом деле представляют собой «scalar values», который на два уровня ниже кластеров графем. Однако, этот путь ведёт к безумию, и для наших целей мы честно даже вряд ли увидим chars вне ASCII, так что давай остановимся на этом.

Мы сопоставляем с шаблоном Some('a'), что является конкретным результатом, который мы ищем, и если это соответствует, мы возвращаем наше значение успеха:
Ok((&input['a'.len_utf8()..], ())). То есть мы удаляем часть, которую мы только что проанализировали ('a') из среза строки и вернём остальное вместе с нашим анализируемым значением, которое является просто пустым
(). Всегда помня о монстре Unicode, мы перед нарезкой узнаём длину в UTF-8 символа 'a' через стандартную библиотеку — она равна 1 (но никогда не забывайте про монстра Unicode).

Если мы получим любой другой Some(char), или если мы получим None, то возвращаем ошибку. Как вы помните наш тип ошибки просто строковый срез, который мы передали в качестве input и который не удалось разобрать. Он не начинался с a, так что это наша ошибка. Это не большая ошибка, но, по крайней мере, это немного лучше, чем просто «что-то где-то не так».

На самом деле нам не нужен этот парсер для разбора XML, но первое, что мы должны будем сделать, это найти открывающий символ <, так что нам понадобится что-то очень похожее. Нам также нужно будет проанализировать >, / и = конкретно, так, может быть, мы можем сделать функцию, которая строит парсер для символа, который мы хотим?


Создание парсера

Давайте даже подумаем об этом: напишем функцию, которая создаёт парсер для статической строки любой длины, а не только одного символа. Это даже проще, потому что фрагмент строки уже является допустимым срезом строки UTF-8, и нам не нужно думать о монстре Unicode.

fn match_literal(expected: &'static str)
    -> impl Fn(&str) -> Result<(&str, ()), &str>
{
    move |input| match input.get(0..expected.len()) {
        Some(next) if next == expected => {
            Ok((&input[expected.len()..], ()))
        }
        _ => Err(input),
    }
}

Теперь это выглядит немного иначе.

Прежде всего, давайте посмотрим на типы. Теперь наша функция принимает expected строку в качестве аргумента и возвращает что-то похожее на парсер, вместо того, чтобы самой быть парсером. Это функция, которая возвращает функцию более высокого порядка. По сути, мы пишем функцию, которая делает функцию, подобную нашей функции the_letter_a ранее.

Таким образом, вместо выполнения работы в теле функции, мы возвращаем замыкание, которое выполняет эту работу и которое соответствует сигнатуре нашего типа для анализатора из предыдущего.

Сопоставление с шаблоном выглядит одинаково, за исключением того, что мы не можем сопоставить наш строковый литерал напрямую, потому что мы не знаем, что именно, поэтому мы используем условие сопоставления if next == expected. В остальном он точно такой же, как и раньше, просто внутри тела замыкания.


Тестирование нашего парсера

Давайте напишем тест для этого, чтобы убедиться, что мы правильно поняли.

#[test]
fn literal_parser() {
    let parse_joe = match_literal("Hello Joe!");
    assert_eq!(
        Ok(("", ())),
        parse_joe("Hello Joe!")
    );
    assert_eq!(
        Ok((" Hello Robert!", ())),
        parse_joe("Hello Joe! Hello Robert!")
    );
    assert_eq!(
        Err("Hello Mike!"),
        parse_joe("Hello Mike!")
    );
}

Сначала мы создаём парсер: match_literal("Hello Joe!"). Он должен потреблять строку "Hello Joe!" и вернуть остаток строки, или завершиться с ошибкой и вернуть всю строку.

В первом случае мы просто передаём ему именно ту строку, которую ожидаем, и видим, что она возвращает пустую строку и значение (), которое означает «мы проанализировали ожидаемую строку, и вам не нужно, чтобы она возвращалась».

Во втором, мы подаём строку "Hello Joe! Hello Robert!" и видим, что он действительно использует строку "Hello Joe!" и возвращает остаток от ввода: " Hello Robert!" (ведущий пробел и всё остальное).

В третьем, мы вводим неверный ввод: "Hello Mike!" и обратите внимание, что он действительно отклоняет ввод с ошибкой. Не то, чтобы Mike не правильно, как правило просто не то, что искал парсер.


Парсер для чего-то менее специфичного

Так что это позволяет нам анализировать <, >, = и даже и />. Мы уже практически закончили!

Следующий после открывающего символа < — это имя элемента. Мы не можем сделать это с помощью простого сравнения строк. Но мы могли бы сделать это с помощью регулярного выражения…

…, но давайте сдерживаться. Это будет регулярное выражение, которое будет очень легко реплицировать в простом коде, и для этого нам не нужно использовать пакет regex. Давайте посмотрим, сможем ли мы написать собственный парсер для этого, используя только стандартную библиотеку Rust.

Вспомним правило для идентификатора имени элемента, оно выглядит следующим образом: один алфавитный символ, за которым следует ноль или более из любого алфавитного символа,
символ, число или тире -.

fn identifier(input: &str) -> Result<(&str, String), &str> {
    let mut matched = String::new();
    let mut chars = input.chars();

    match chars.next() {
        Some(next) if next.is_alphabetic() => matched.push(next),
        _ => return Err(input),
    }

    while let Some(next) = chars.next() {
        if next.is_alphanumeric() || next == '-' {
            matched.push(next);
        } else {
            break;
        }
    }

    let next_index = matched.len();
    Ok((&input[next_index..], matched))
}

Как всегда, мы сначала смотрим на тип. На этот раз мы не пишем функцию для создания парсера, мы просто пишем сам парсер, как в первый раз. Заметным отличием здесь является то, что вместо типа результата () мы возвращаем String в кортеже вместе с оставшимися входными данными. Эта String будет содержать идентификатор, который мы только что проанализировали.

Имея это в виду, сначала мы создаём пустую String и называем её matched. Это будет наш результат. Мы также получаем итератор для символов в input, который мы собираемся начать разделять.

Первый шаг — посмотреть, есть ли символ в начале. Мы вытаскиваем первый символ из итератора и проверяем, является ли он буквой: next.is_alphabetic(). Стандартная библиотека Rust, конечно поможет нам с Unicode — она будет соответствовать буквам в любом алфавите, а не только в ASCII. Если это буква, мы помещаем её в нашу строку в переменную matched, а если нет, мы не смотрим на идентификатор элемента и немедленно возвращаемся с ошибкой.

На втором шаге мы продолжаем вытаскивать символы из итератора, отправляя их в создаваемую нами строку до тех пор, пока не встретим символ не удовлетворяющий функцию is_alphanumeric() (она похожа на is_alphabetic() за исключением того, что она также включает числа в любом алфавите) или тире '-'.

Когда мы в первый раз видим что-то, что не соответствует этим критериям, мы заканчиваем синтаксический анализ, прерываем цикл и возвращаем String, не забывая вырезать из input фрагмент, который мы использовали. Аналогично, если в итераторе заканчиваются символы, это означает, что мы достигли конца ввода.

Стоит отметить, что мы не возвращаемся с ошибкой, когда видим что-то не буквенно-цифровое или тире. У нас уже есть достаточно, чтобы создать действительный идентификатор после того, как мы сопоставим эту первую букву, и вполне нормально, что после разбора нашего идентификатора во входной строке будет больше элементов для анализа, поэтому мы просто прекращаем синтаксический анализ и возвращаем наш результат. Только если мы не можем найти даже эту первую букву, мы на самом деле возвращаем ошибку, потому что в этом случае здесь точно не было идентификатора.

Помните, что структура Element — это то, во что мы собираемся разобрать наш XML документ.

struct Element {
    name: String,
    attributes: Vec<(String, String)>,
    children: Vec,
}

На самом деле мы только что закончили анализатор для первой части, поля name. String, который возвращает наш парсер, идёт прямо туда. Это также правильный парсер для первой части каждого attribute.

Давайте проверим это.

#[test]
fn identifier_parser() {
    assert_eq!(
        Ok(("", "i-am-an-identifier".to_string())),
        identifier("i-am-an-identifier")
    );
    assert_eq!(
        Ok((" entirely an identifier", "not".to_string())),
        identifier("not entirely an identifier")
    );
    assert_eq!(
        Err("!not at all an identifier"),
        identifier("!not at all an identifier")
    );
}

Мы видим, что в первом случае строка "i-am-an-identifier" анализируется полностью, оставляя только пустую строку. Во втором случае синтаксический анализатор возвращает "not" в качестве идентификатора, а остальная часть строки возвращается в качестве оставшегося ввода. В третьем случае синтаксический анализатор терпит неудачу сразу, потому что первый символ, который он находит, не буква.


Комбинаторы

Теперь мы можем проанализировать открывающий символ <, и мы можем проанализировать следующий идентификатор, но нам нужно проанализировать оба. Поэтому следующим шагом будет написание другой функции комбинаторного синтаксического анализатора, но такой, которая принимает два синтаксических анализатора в качестве входных данных и возвращает новый синтаксический анализатор, который анализирует их обоих по порядку. Другими словами, комбинатор парсеров, потому что он объединяет два парсера в новый. Посмотрим, сможем ли мы это сделать.

fn pair(parser1: P1, parser2: P2) -> impl Fn(&str) -> Result<(&str, (R1, R2)), &str>
where
    P1: Fn(&str) -> Result<(&str, R1), &str>,
    P2: Fn(&str) -> Result<(&str, R2), &str>,
{
    move |input| match parser1(input) {
        Ok((next_input, result1)) => match parser2(next_input) {
            Ok((final_input, result2)) => Ok((final_input, (result1, result2))),
            Err(err) => Err(err),
        },
        Err(err) => Err(err),
    }
}

Здесь становится немного сложнее, но вы знаете, что делать: начните с рассмотрения типов.

Прежде всего, у нас есть четыре типа переменных: P1, P2, R1 и R2. Это Parser 1, Parser 2, Result 1 и Result 2. P1 и P2 являются функциями, и вы заметите, что они следуют хорошо установленному шаблону функций синтаксического анализатора: так же, как и возвращаемое значение, они принимают &str в качестве входных данных и возвращают Result пары оставшихся входных данных и результата, или ошибку.

Но посмотрите на типы результатов каждой функции: P1 — это синтаксический анализатор, который выдаёт R1 в случае успеха, и P2 также выдаёт R2. И результат последнего синтаксического анализатора, который был возвращён из нашей функции, равен (R1, R2). Таким образом, задача этого синтаксического анализатора состоит в том, чтобы сначала запустить анализатор P1 на входе, сохранить его результат, затем запустить P2 на входе, который вернул P1, и если оба из них сработали, мы объединяем два результата в кортеж (R1, R2).

По коду видно, что это именно то, что он делает. Мы начинаем с запуска первого анализатора на входе, затем второго анализатора, затем объединяем два результата в кортеж и возвращаем его. Если какой-либо из этих парсеров завершится ошибкой, мы немедленно вернёмся с ошибкой, которую он выдал.

Таким образом, мы должны иметь возможность объединить два наших анализатора, match_literal и identifier, чтобы фактически проанализировать первый фрагмент нашего первого тега XML. Давайте напишем тест, чтобы увидеть, работает ли он.

#[test]
fn pair_combinator() {
    let tag_opener = pair(match_literal("<"), identifier);
    assert_eq!(
        Ok(("/>", ((), "my-first-element".to_string()))),
        tag_opener("")
    );
    assert_eq!(Err("oops"), tag_opener("oops"));
    assert_eq!(Err("!oops"), tag_opener("

Кажется, работает! Но посмотрите на этот тип результата: ((), String). Очевидно, что нас интересует только правое значение, String. Это будет иметь место довольно часто — некоторые из наших синтаксических анализаторов сопоставляют только шаблоны во входных данных, не производя значений, и поэтому их выходные данные можно безопасно игнорировать. Чтобы приспособиться к этому шаблону, мы собираемся использовать наш комбинатор pair чтобы написать два других комбинатора: left, который отбрасывает результат первого синтаксического анализатора и возвращает только второй, и его противоположность right. Мы использовали в нашем тесте выше парсер, который отбрасывает левую часть и сохраняет только нашу String, вместо pair.


Ведение в Функтор

Но прежде чем мы зайдём так далеко, давайте представим ещё один комбинатор, который значительно упростит написание этих двух: map.

У этого комбинатора одна задача: изменить тип результата. Например, допустим, у вас есть парсер, который возвращает ((), String) и вы хотели изменить его, чтобы он например возвращал только String.

Для этого мы передаём ему функцию, которая знает, как преобразовать исходный тип в новый. В нашем примере это просто: |(_left, right)| right. Более обобщённо, это выглядело бы как Fn(A) -> B где A — исходный тип результата парсера, а B — новый.

fn map(parser: P, map_fn: F) -> impl Fn(&str) -> Result<(&str, B), &str>
where
    P: Fn(&str) -> Result<(&str, A), &str>,
    F: Fn(A) -> B,
{
    move |input| match parser(input) {
        Ok((next_input, result)) => Ok((next_input, map_fn(result))),
        Err(err) => Err(err),
    }
}

А что говорят типы? P — наш парсер. Возвращает A при успехе. F — это функция, которую мы будем использовать для отображения P как наше возвращаемое значение, которое выглядит так же, как P за исключением того, что его тип результата — B вместо A

В коде мы запускаем parser(input) и, если он успешен, мы берём result и запускаем на нём нашу функцию map_fn(result), превращая A в B, и это наш конвертированный парсер.

На самом деле, давайте побалуем себя и немного укоротим эту функцию, потому что эта map оказывается общим шаблоном, который фактически реализует Result:

fn map(parser: P, map_fn: F) -> impl Fn(&str) -> Result<(&str, B), &str>
where
    P: Fn(&str) -> Result<(&str, A), &str>,
    F: Fn(A) -> B,
{
    move |input|
        parser(input)
            .map(|(next_input, result)| (next_input, map_fn(result)))
}

Этот паттерн — то, что называется «функтором» в Haskell и его математическом родственнике, теории категорий. Если у вас есть вещь с типом A, и у вас есть доступная функция map, вы можете передать функцию из A в B чтобы превратить её в то же самое, но с типом B в ней, это функтор. Вы видите это во многих местах в Rust, таких как Option, Result, Iterator и даже Future, без явного указания на это. И тому есть веская причина: вы не можете по-настоящему выразить функтор как обобщённую вещь в системе типов Rust, потому что в ней отсутствуют типы более высокого порядка, но это другая история, поэтому давайте просто отметим, что это функторы, и вам просто нужно искать функцию map.


Время для Типажа

Возможно, вы уже заметили, что мы продолжаем повторять форму сигнатуры типа синтаксического анализатора: Fn(&str) -> Result<(&str, Output), &str>. Возможно, вы устали от того, что читаете это полностью, так же, как я пишу, поэтому я думаю, что пришло время представить типаж, сделать вещи немного более читабельными и позволить нам добавить некоторую расширяемость к нашему парсеру.

Но прежде всего давайте создадим псевдоним для возвращаемого типа, который мы продолжаем использовать:

type ParseResult<'a, Output> = Result<(&'a str, Output), &'a str>;

Так что теперь, вместо того, чтобы постоянно печатать это чудовище, мы можем просто напечатать ParseResult или подобное. Мы добавили туда время жизни, потому что объявление типа требует этого, но большую часть времени компилятор Rust должен иметь возможность сделать это за вас. Попробуйте убрать время жизни и посмотреть, расстроится ли rustc, а затем просто вставьте его, если это произойдёт.

Время жизни 'a, в данном случае, относится конкретно к времени жизни входных данных.

Теперь типаж. Мы должны указать время жизни и здесь, и когда мы будем использовать этот типаж, мы будем указывать и время жизни. Это немного лишняя работа, но она превосходит предыдущую версию.

trait Parser<'a, Output> {
    fn parse(&self, input: &'a str) -> ParseResult<'a, Output>;
}

На данный момент у него есть только один метод: метод parse(), который должен выглядеть знакомо: он такой же, как функция парсера, которую мы пишем.

Чтобы сделать это ещё проще, мы можем реализовать этот типаж для любой функции, которая соответствует сигнатуре парсера:

impl<'a, F, Output> Parser<'a, Output> for F
where
    F: Fn(&'a str) -> ParseResult,
{
    fn parse(&self, input: &'a str) -> ParseResult<'a, Output> {
        self(input)
    }
}

Таким образом, мы не только можем передавать те же функции, что и ранее, когда парсеры полностью реализуют типаж Parser, мы также открываем возможность использовать другие типы в качестве парсеров.

Но, что более важно, это избавляет нас от необходимости постоянно печатать эти сигнатуры функций. Давайте перепишем функцию map чтобы увидеть, как она работает.

fn map<'a, P, F, A, B>(parser: P, map_fn: F) -> impl Parser<'a, B>
where
    P: Parser<'a, A>,
    F: Fn(A) -> B,
{
    move |input|
        parser.parse(input)
            .map(|(next_input, result)| (next_input, map_fn(result)))
}

Здесь следует особо отметить одну вещь: вместо непосредственного вызова синтаксического анализатора как функции, мы теперь должны перейти к parser.parse(input), потому что мы не знаем, является ли тип P функцией, мы просто знаем, что он реализует Parser, и поэтому мы должны придерживаться интерфейса, который обеспечивает Parser. В противном случае тело функции выглядит точно так же, а типы выглядят намного аккуратнее. Появился новый параметр времени жизни 'a', как источник дополнительного шума, но в целом это довольно значительное улучшение.

Если мы переписываем функцию pair таким же образом, она становится ещё более аккуратной:

fn pair<'a, P1, P2, R1, R2>(parser1: P1, parser2: P2) -> impl Parser<'a, (R1, R2)>
where
    P1: Parser<'a, R1>,
    P2: Parser<'a, R2>,
{
    move |input| match parser1.parse(input) {
        Ok((next_input, result1)) => match parser2.parse(next_input) {
            Ok((final_input, result2)) => Ok((final_input, (result1, result2))),
            Err(err) => Err(err),
        },
        Err(err) => Err(err),
    }
}

То же самое и здесь: единственными изменениями являются приведённые в порядок сигнатуры типов и необходимость использовать parser.parse(input) вместо parser(input).

На самом деле, давайте приведём в порядок тело функции pair, так же как мы сделали с map.

fn pair<'a, P1, P2, R1, R2>(parser1: P1, parser2: P2) -> impl Parser<'a, (R1, R2)>
where
    P1: Parser<'a, R1>,
    P2: Parser<'a, R2>,
{
    move |input| {
        parser1.parse(input).and_then(|(next_input, result1)| {
            parser2.parse(next_input)
                .map(|(last_input, result2)| (last_input, (result1, result2)))
        })
    }
}

Метод and_then в Result похож на map, за исключением того, что функция отображения не меняет значение текущего Result, а возвращает новый Result. Приведённый выше код идентичен эффекту от предыдущей версии выписанной со всеми этими match блоками. К and_then мы вернёмся позже, но сейчас, когда у нас есть красивая и аккуратная map, мы реализуем комбинаторы left и right.


Left и Right

С pair и map на месте, мы можем написать left и right очень кратко:

fn left<'a, P1, P2, R1, R2>(parser1: P1, parser2: P2) -> impl Parser<'a, R1>
where
    P1: Parser<'a, R1>,
    P2: Parser<'a, R2>,
{
    map(pair(parser1, parser2), |(left, _right)| left)
}

fn right<'a, P1, P2, R1, R2>(parser1: P1, parser2: P2) -> impl Parser<'a, R2>
where
    P1: Parser<'a, R1>,
    P2: Parser<'a, R2>,
{
    map(pair(parser1, parser2), |(_left, right)| right)
}

Мы используем комбинатор pair, чтобы объединить два парсера в парсер для кортежа их результатов, а затем мы используем комбинатор map чтобы выбрать только ту часть кортежа, которую мы хотим сохранить.

Перепишем наш тест для первых двух частей нашего элемента тега, теперь он стал немного чище, и в процессе мы получили некоторые важные новые возможности комбинатора синтаксического анализатора.

Мы должны обновить наши два парсера, чтобы сначала использовать Parser и ParseResult. match_literal является более сложным:

fn match_literal<'a>(expected: &'static str) -> impl Parser<'a, ()> {
    move |input: &'a str| match input.get(0..expected.len()) {
        Some(next) if next == expected => Ok((&input[expected.len()..], ())),
        _ => Err(input),
    }
}

В дополнение к изменению типа возвращаемого значения, мы также должны убедиться, что тип входных данных для замыкания — &'a str, иначе rustc расстроится.

Для identifier, просто измените тип возвращаемого значения, и все готово, компилятор позаботится о выводе времени жизни:

fn identifier(input: &str) -> ParseResult {

А теперь тест. () отсутствует в результате. Чего мы и добивались.

#[test]
fn right_combinator() {
    let tag_opener = right(match_literal("<"), identifier);
    assert_eq!(
        Ok(("/>", "my-first-element".to_string())),
        tag_opener.parse("")
    );
    assert_eq!(Err("oops"), tag_opener.parse("oops"));
    assert_eq!(Err("!oops"), tag_opener.parse("


Один или больше

Давайте продолжим разбор этого тега элемента. У нас есть открывающий символ < и у нас есть идентификатор. Что дальше? Это должна быть наша первая пара атрибутов.

Нет, на самом деле, эти атрибуты не являются обязательными. Нам нужно найти способ справиться с вещами, которые не являются обязательными.

Нет, подожди, на самом деле мы должны иметь дело с чем-то ещё, прежде чем мы доберёмся до первой необязательной пары атрибутов: пробел.

Между концом имени элемента и началом первого имени атрибута (если оно есть) есть пробел. Нам нужно разобраться с этим пространством.

Это ещё хуже — нам нужно иметь дело с одним или несколькими пробелами, потому что  является допустимым синтаксисом, даже если он немного перегружен пробелами. Поэтому сейчас самое время подумать о том, можем ли мы написать комбинатор, выражающий идею одного или нескольких синтаксических анализаторов.

Мы уже имели дело с этим в нашем анализаторе identifier, но все это было сделано вручную. Не удивительно, что код для общей идеи не так уж и отличается.

fn one_or_more<'a, P, A>(parser: P) -> impl Parser<'a, Vec>
where
    P: Parser<'a, A>,
{
    move |mut input| {
        let mut result = Vec::new();

        if let Ok((next_input, first_item)) = parser.parse(input) {
            input = next_input;
            result.push(first_item);
        } else {
            return Err(input);
        }

        while let Ok((next_input, next_item)) = parser.parse(input) {
            input = next_input;
            result.push(next_item);
        }

        Ok((input, result))
    }
}

Прежде всего, типом возвращаемого значения парсера, из которого мы строим, является A, а типом возврата комбинированного синтаксического анализатора является Vec — любое количество A.

Код действительно очень похож на identifier. Сначала мы анализируем первый элемент, и если его там нет, мы возвращаемся с ошибкой. Затем мы анализируем как можно больше элементов, пока синтаксический анализатор не даст сбой, и в этот момент мы возвращаем вектор с собранными нами элементами.

Глядя на этот код, спросим себя: насколько легко было бы его адаптировать к идее ноль или более? Нам просто нужно удалить первый запуск парсера:

fn zero_or_more<'a, P, A>(parser: P) -> impl Parser<'a, Vec>
where
    P: Parser<'a, A>,
{
    move |mut input| {
        let mut result = Vec::new();

        while let Ok((next_input, next_item)) = parser.parse(input) {
            input = next_input;
            result.push(next_item);
        }

        Ok((input, result))
    }
}

Давайте напишем несколько тестов, чтобы убедиться, что эти два парсера работают.

#[test]
fn one_or_more_combinator() {
    let parser = one_or_more(match_literal("ha"));
    assert_eq!(Ok(("", vec![(), (), ()])), parser.parse("hahaha"));
    assert_eq!(Err("ahah"), parser.parse("ahah"));
    assert_eq!(Err(""), parser.parse(""));
}

#[test]
fn zero_or_more_combinator() {
    let parser = zero_or_more(match_literal("ha"));
    assert_eq!(Ok(("", vec![(), (), ()])), parser.parse("hahaha"));
    assert_eq!(Ok(("ahah", vec![])), parser.parse("ahah"));
    assert_eq!(Ok(("", vec![])), parser.parse(""));
}

Обратите внимание на разницу между ними: для one_or_more поиск пустой строки является ошибкой, потому что он должен видеть хотя бы один случай своего ниже лежащего парсера, но для zero_or_more пустая строка просто означает нулевой случай, который не является ошибкой.

На этом этапе разумно начать думать о способах обобщения этих двух парсеров, потому что один является точной копией другого, с удалением только одного фрагмента. Может быть заманчиво выразить one_or_more в терминах zero_or_more, что-то вроде этого:

fn one_or_more<'a, P, A>(parser: P) -> impl Parser<'a, Vec>
where
    P: Parser<'a, A>,
{
    map(pair(parser, zero_or_more(parser)), |(head, mut tail)| {
        tail.insert(0, head);
        tail
    })
}

Здесь мы сталкиваемся с проблемами Rust, и я даже не имею в виду проблему отсутствия метода cons для Vec, но я знаю, что каждый программист на Lisp, читающий этот фрагмент кода, думал об этом. Нет, это ещё хуже: это владение.

Мы владеем этим парсером, поэтому мы не можем дважды передать его в качестве аргумента: компилятор начнёт кричать нам, что мы пытаемся переместить уже перемещённое значение. Оказывается нет: без столкновения с ещё целым рядом проблем с проверкой заимствований мы не сможем это сделать (и сейчас мы не будем даже делать этого). А поскольку наши синтаксические анализаторы являются функциями, они не делают ничего такого простого, чтобы реализовать Clone, который бы очень аккуратно спас день, поэтому мы столкнулись с ограничением, которое не позволяет нам легко повторять наши анализаторы в комбинаторах.

Это не обязательно большая проблема. Это означает, что мы не можем выразить one_or_more с помощью комбинаторов, но оказывается, что эти два комбинатора обычно являются единственными, которые вам в любом случае нужны, которые имеют тенденцию повторно использовать другие синтаксические анализаторы, и если вы хотите по-настоящему быть модным, вы можете написать комбинатор, который принимает RangeBound в дополнение к синтаксическому анализатору и повторяет его в соответствии с диапазоном: range(0..) для zero_or_more, range(1..) для one_or_more, range(5..=6) для ровно пяти или шести, сколько вашей душе угодно.

Давайте оставим это как упражнение для читателя. Прямо сейчас у нас всё будет в порядке только с zero_or_more и one

© Habrahabr.ru