Время от времени у меня возникает желание придумать свой собственный маленький язык программирования и написать интерпретатор. В этот раз я начал писать на scala, узнал про библиотеку parser combinators, и был поражён: оказывается, можно писать парсеры легко и просто. Чтобы не превращать статью в пособие по "рисованию совы", ниже приведёна реализация разбора и вычисления выражений типа "1 + 2 * sin(pi / 2)". Сам парсинг и вычисление выражения занимают всего лишь 44 непустых строчки — не то чтобы я сильно стремился сократить их количество, но выглядит это реально просто и лаконично. Проект на github. Для сравнения: Итак, если вам не терпится увидеть результат: Посмотрите на следущую строчку: Она подозрительно похожа на описание грамматики, но это валидный код, в котором среда разработки может сразу же обнаружить и подсветить большинство ошибок. Это возможно по следующим причинам: Думаю, мне удалось Вас заинтересовать, поэтому дальше всё будет по порядку. Когда-то эти классы были включены в стандартную библиотеку языка, но потом их вынесли в отдельную библиотеку. В конце я привёл ссылки, по которым можно найти более подробную информацию. Итак, самое простое — RegexParsers. Добавляют неявные преобразования из строк и регулярных выражений в парсеры. Кстати, значок ~ обозначает не только метод у парсера, но и имя case класса, хранящего пару значений. Кусочек кода из Parsers.scala: Допустим, мы хотим собрать из нескольких парсеров один: Что произойдёт? В данном случае скобки не несут никакой полезной информации. Можно сделать так: В этот раз скобки будут отброшены. Выражения с оператором <~ советуют заключать в скобки, так как у него не очень высокий приоритет.
def Теперь должно быть понятно, что делает следующий код: Я немножко поленился и превращаю строку в число стандартными методами. Время надо экономить) Поскольку наше описание парсеров — это код, неоднозначные грамматики всё равно работают. В примере с парсингом number | funcCall | id мы пытаемся распарсить number, в случае неудачи — вызов функции и т.д. Порядок может быть важным, например (id | funcCall) при попытке распарсить "sin(x)" радостно распарсит Id("sin"), и парсер funcCall не будет вызван. Для корректной работы лучше написать (funcCall | id). Допустим, мы хотим распарсить последовательность единичек: Парсинг ones начинается с того, что мы вызываем парсинг ones, который снова ... Попытка распарсить единички приведёт к переполнению стека. В данном случае можно изменить описание так, чтобы каждый раз "поглощалось" что-нибудь. Например: Но не всегда грамматику легко переписать. Выражния типа 3-2-1 должны распознаваться именно как (3-2)-1, вариант 3-(2-1) не подойдёт. С делением будет аналогичная проблема. Как это сделать без усложнения грамматики? Нас спасут packrat — парсеры. Их идея заключается в том, что парсер может хранить "для себя" некоторую информацию о вызовах. Например, чтобы сохранять результат работы и не парсить одно и то же дважды… или чтобы корректно работать в случаях с рекурсией.  в трейте PackratParsers содержится неявное преобразование строчек и прочего в парсеры "нужного" типа. PackratParser лучше создавать только один раз и хранить в переменной. Кроме того, если парсер p использует q, а q использует p, стоит использовать ленивую инициализацию. Думаю, теперь понятно, как можно легко и непринуждённо распарсить 3-2-1 как (3-2)-1. Возможно, у вас возникает вопрос: где парсер хранит информацию? Если её хранить прямо внутри PackratParser, то вызов парсера для другого ввода может дать некорректные результаты. Так вот, необходимая информация хранится вместе с "входными" данными парсера. Можно заглянуть в код библиотеки и убедиться в этом: Поэтому парсер принимает на вход не строку, а new PackratReader(new CharSequenceReader(string)) Что самое крутое — использование packrat парсеров ни к чему не обязывает, их можно комбинировать с обычными парсерами и наоборот.  Код целиком: Результат парсинга — либо дерево, либо сообщение об ошибке. case классы — просто классы-обёртки над значениями, они все реализуют интерфейс Expression. слово sealed обозначает, что реализующие этот интерфейс классы должны содержаться в том же самом файле. Это позволяет с уверенностью говорить, что Expression может быть одного из четырёх типов. Код, который вычисляет выражения, тоже прост. Я предполагаю, что на вход подаются корректные выражения. Фишечки скалы — можно объявить функцию eval внутри функции apply для повышения читаемости кода. Вторая фишечка — в качестве аргумента по-умолчанию мы подсовываем Map.empty. Она пустая, поэтому может быть любого типа, она неизменяемая, поэтому она останется пустой, и реально мы получим ссылки на один и тот же объект — синглтон. Map.empty имеет метод apply(a: In):Out — мы можем считать её функцией. Парсинг и вычислени выражений готовы. Посчитаем получившиеся строки кода (непустые): И всё — причём код легко читается, его легко изменять и он практчиески не содержит ничего лишнего. Красота! Об этом стоит подумать ещё на этапе написания парсера, но проверяющий код ни на что не влияет, потому приведён только только сейчас. (конечно, можно написать тестики… но эта статья о написании парсеров, а не тестов, поэтому я сделал как можно проще) Для серьёзных целей существуют генераторы парсеров и прочие штуки. Но если Вам хочется парсить что-нибудь относительно простое, экспериментировать и получать мгновенную обратную связь — можно использовать описанный выше подход. Информации на русском почти нет, да и на английском тоже не очень много. Я надеюсь, что статья поможет кому-нибудь. Полезные ссылочки: Приведённый выше код я выложил на github Используется система сборки sbt. Достаточно установить её, перейти в папку с проектом и набрать в консоли "sbt run" P.S. У меня всё ещё есть цель дописать свой интерпретатор lua-подобного языка с шахматами и поэтессами. Я вроде бы продумал синтаксис языка, потом в процессе написания парсера наткнулся на противоречия — пришлось отказаться от пары поэтесс и заменить шахматы на шашки, но результат всё равно получается довольно интересным. Программист