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.
