1.       Преамбула

Напомню, что в серии статей на Хабре я описываю вольную реализации демонстратора системы взаимодействующих движков Forth в Elixir в рамках парадигмы обработки данных в потоке. Последняя статья https://habr.com/ru/articles/1002748/ из этой серии была посвящена реализации прототипов взаимодействующих движков Forth класса тактовых генераторов.

Сегодня моя цель - обсудить две возможные схемы реализации интерпретатора, показанные на заставке.

2.       Исходная точка вопроса

Ход разработки демонстратора системы идет в стиле «два шага вперед, шаг назад». Последний шаг назад, повлекший капитальную модернизацию работающего интерпретатора Forth, был сделан на этой неделе.

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

Справка из Википедии: "REPL — форма организации простой интерактивной среды программирования в рамках средств интерфейса командной строки."

Я не стал копировать из Интернета иллюстрации REPL интерпретатора, а для единообразия подготовил свою, соответствующую интерпретатору Forth:

Контур
Контур

Составные блоки любого интерпретатора являются стандартными, но для исключения разночтения модулям на схеме дам пояснения:

  • lexer — получает от REPL набор слов и преобразует его в набор лексем или токенов;

  • interpreter — преобразует токены в набор исполняемых элементов, или кодов;

  • executer — поэлементно вычисляет коды из набора в рамках сохраняющегося состояния executer, т.е. вычислителя.

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

3.       Реальная схема

На основании небольшого личного опыта я знал, что схема построения REPL интерпретатора отличается от вышеприведенной. И выглядит она, как показано на следующей картинке:

Челнок
Челнок

После каждой обработки команд в модуле промежуточный результат передается обратно в REPL. В рамках привычного механизма вызова функций/процедур по–другому и не сделать.

Пакеты команд в потоке двигаются «туда и обратно». Поэтому правильно будет назвать такой стиль взаимодействия «челночным».

Между вызовами модулей REPL может производить реорганизацию временные промежуточных переменных.

4.       Постановка задачи

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

В языках Elixir/Erlang сообщения передаются и принимаются между процессами. Поэтому предполагалось lexer, interpreter и executer оформить как процессы состояния на базе GenServer. Но я почему–то не торопился продвигаться в этом направлении.

5.       Фактическая реализация

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

И тут ко мне пришла счастливая идея, воспользоваться встроенным оператором Elixir конвейер (pipeline).

«Оператор конвейера, или pipeline, принимает результат предыдущего выражения и передает его в следующее в качестве первого аргумента. С помощью этого оператора мы получаем аккуратный код без временных переменных. Он читается легко и непринужденно, как художественное произведение. На этапе компиляции данный код превращается в его «лестничную» версию.» [1]

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

6.       Главные полученные результаты:

  • Код сократился с ~30 строк до одной цепочки операторов конвейера:

...
new_state = IO.gets("~Words $ ") |> String.trim |> parse |> interpret(state) |> evaluate

IO.write(“ ok\n”) # вывод сообщения об успешном завершении цикла
loop(new_state)   # концевая рекурсия процесса REPL
...

Как вы уже поняли, "|>" как раз и есть оператор конвейера.

Для детального ознакомления с кодом на GitHub https://github.com/VAK-53/Forth-ibE выложена модернизированная версия интерпретатора Forth-ibE.

  • Исчезли паразитные временные передаточные переменные и операции по их реорганизации между вызовами функций.

  • А главное, что я себе доказал работоспособность контурного стиля обработки кода в интерпретаторе.

7.       Сопутствующие результаты

Капитальная модернизация интерпретатора Forth повлекла за собой:

  • Тотальное использование механизма генерирования исключения внутри кода в случае ошибки и отлавливания их на верхнем уровне в REPL.

  • Группирование обработка ошибок в одном месте и их естественную систематизацию.

  • Ревизию и усовершенствование алгоритмов синтаксического анализа. Критической модернизации подверглась проблемная структура ветвления if-else-then.

8.       Умозаключение

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

Если у вас есть мысли по поводу обработки данных/команд в потоке, милости прошу высказаться в комментах.

9.       Благодарность

Уже после своей реализации потоковой схемы в интерпретаторе, чтобы убедиться в состоятельности своей гипотезы, я решил посмотреть соответствующую компьютерную литературу. Под рукой оказалась распечатка книги Бьярне Страуструпа "Программирование: принципы и практика использования C++".

В книге в главе 6 "Создание программ", в которой рассматривается пример разработки калькулятора, верхний уровень REPL приложения записывается в виде двух операторов:

{
while (cin)
cout « expression() « '\n'
...
}

В начале пример привел меня в состояние озабоченности, т.к. он в какой–то степени похож на мой результат. Но после размышлений я понял, что оператор вывода « в С++ далеко не оператор посылки сообщения send и не конвейер |> в Elixir. Он пересылает значение правой стороны выражения на стандартное устройство вывода.

После этого я вздохнул с облегчением, за что благодарю Бьярне Страуструпа за его учебник.

Примечание не по делу: Я думал, что Б. Страуструп — создатель языка C++, а он еще и отличный технический писатель. Придётся к его книге обратиться ещё несколько раз...

 

Литература:

1.       Саша Юрич, Elixir в действии, – М.: ДМК Пресс, 2020.

2.      Б.Страуструп, Программирование: принципы и практика использования C++, — М. ООО «И.Д. Вильямс», 2011.