ZLE — язык программирования без условных операторов и циклов

от автора

Это ещё один «эзотерический» язык, не относитесь к нему слишком серьёзно 🙂

Некоторые языки хвастаются отсутствием циклов — например в Erlang или Scheme циклы реализованы через хвостовую рекурсию. Иногда «отсутствием циклов» называют конструкцию в духе (1..10).forEach(something) — а отсутствием условного оператора, какую-нибудь разновидность match. Честно говоря, выглядит просто как альтернативный синтаксис.

А как можно «совсем без»? Вот в машине Тьюринга и подобных автоматах мы переключаем «состояние» и дальнейшее исполнение программы зависит от того в какое состояние мы попали. Это похоже на GOTO у которого параметр не обязан быть константой.

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

Конечно, это «прототипичная» версия — в ней не хватает многих фишек — и она абсолютно открытая к вашим предложениям и идеям!

Общие замечания

Наш язык похож на Бейсик из которого выкинуто почти всё. В то же время он умеет работать с числовыми и строковыми данными, с массивами (тоже с числовыми или строковыми индексами — в духе JS и PHP). Также и метки для переходов могут быть и числовыми и строковыми. Выражения поддерживают все популярные бинарные операции (арифметические, сравнения, логические).

Интерпретатор на скорую ручку написан на JS, поэтому он умеет взаимодействовать (через операцию EXEC) с элементами веб-страницы. Исходный код в гитхабе, но нам больше интересна «песочница» упомянутая далее — для экспериментов с языком:

так выглядит работа с интерпретатором в "песочнице"

так выглядит работа с интерпретатором в «песочнице»

Название ZLE пока просто рабочее. Из известных мне языков оно как минимум в польском означает «Плохо!» — ну вот и хорошо, это отражает суть 🙂 Аббревиатура, как оказалось, используется также для «Zsh Line Editor» но т.к. это не язык, надеюсь, не страшно. Рабочая «расшифровка» — Zany Language for Enthusiasts — «ненормальный» язык для энтузиастов.

Первые шаги — оператор EXEC и песочница

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

exec "alarm", "Preved, Medved!" exec "console.log", "I'm borken" exec "Math.exp", 4 exec "prompt", "your name?"

В принципе в нашем случае с его помощью можно дотянуться до любых функций DOM. Если нужен результат возвращённый функцией, он будет записан в переменную _ (как в случае с Math.exp и prompt).

Откройте страничку с «песочницей» — здесь небольшая textarea для ввода кода, кнопка для запуска — и ниже зона куда можно писать вызывая рукотворную js-функцию output — это немного удобнее чем alert. Попробуйте ввести и выполнить такую программу:

exec "prompt", "What is your name?" exec "output", "Glad to see you, " + _ + "!"

При запуске вы должны увидеть стандартный диаложек спрашивающий как вас зовут. Введите что-нибудь (например, Pedros, если не придумать лучше). В поле вывода появится Glad to see you, Pedros!

Присваивание, переменные, массивы

Присваивание пишется обычным образом, с одинарным знаком равенства. Можно (как в старом добром бейсике) использовать оператор LET (милый архаизм, т.к. код был обокраден с php-basic где этот оператор оставался для совместимости).

Переменные как обычно начинаются с буквы (или подчёркивания), могут содержать цифры, точки и знаки подчёркивания.

Массивы одномерные, зато допускают строковые индексы (то есть они же хэш-таблицы).

x = 13 y = x * x z[5] = "what a beautiful number" exec "output", y + y + " - " + z[5]

Как видим, объявление переменных и массивов не требуется, но при обращении к переменной (или ячейке массива) в которых ещё не было проставлено значения мы получим ошибку. Из ошибки можно догадаться что массивы устроены в одном пространстве с переменными но не пересекаются.

Наконец, про GOTO и метки

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

В качестве первого примера рассмотрим программу печатающие квадраты чисел от 1 до 10:

i = 1                              # инициализируем счетчик repeat:                            # метка для повторения exec "output", i + ": " + (i*i)    # печатаем счетчик и квадрат его goto i                             # пытаемся выйти из цикла i=i+1; goto repeat                 # увеличиваем счетчик и возвращаемся назад 10 exec "output", "Well done!"     # строка с номером 10 - сюда мы выходим

Заметим пару мелочей для удобства: можно писать комментарии со знаком #, также можно писать несколько команд в одной строке через точку-с-запятой.

Смысл первых трёх строк понятен из комментариев.

В четвёртой то самое GOTO — мы пытаемся перейти на строчку с меткой (номером) содержащимся в переменной i. Что случится когда i=10 понятно — произойдёт переход на последнюю строку программы (она здесь отмечена в бейсиковом стиле номером 10). Если же требуемой метки не обнаруживается, переход не происходит и программа просто выполняется дальше. Такое поведение пока кажется наиболее простым, но в принципе это обсуждаемо.

Таким образом, пока i не дошло до 10 будет выполняться следующая строчка i=i+1 и goto repeat — переход к повтору всех манипуляций.

Здесь маленькая неоднозначность — по-хорошему repeat в выражении нужно было бы писать в кавычках — иначе goto должен бы воспринять его как переменную, значение которой надо взять и использовать как целевую метку. Но ради простоты записи сделан такой вот «syntactic sugar» — если в выражении только одно слово (имя), то goto сначала проверит есть ли метка с таким именем — и если есть, то трактует имя как константу а не как выражение. В общем, если добавите кавычки, ничего не сломается.

Выражения и операторы

Выражения используются в привычной «инфиксной» нотации и большинство операторов тоже более-менее привычны:

  • арифметические +, -, *, / без пояснений, также %, \ — взятие остатка и целочисленное деление — и наконец возведение в степень ^;

  • сравнения <, >, <=, >=, !=, == в соответствии с «сишным» синтаксисом — а также две дублирующих <>, = неравенство и равенство («бейсиковый» вариант) — все они возвращают 1 в качестве истины и 0 в противном случае;

  • строковые — из уже упомянутых + и операции сравнения адекватно работают со строками (конкатенация, алфавитное сравнение)

  • логические & и | в качестве AND и OR — причем они в качестве истины возвращают один из своих операндов (как в Lua); ложью считается 0 и пустая строка.

Эти операторы позволяют создавать в выражении для GOTO всякие полезные конструкции. Для примера рассмотрим комическую ситуацию когда перед началом игры у пользователя спрашивают возраст:

exec "prompt", "your age?"; age = _  goto "person" + (age - 16) \ (64-16)  exec "output", "you are too young to play!"; end person1: exec "output", "you are too old to play!"; end person0: exec "output", "check passed, let's play!"

Здесь арифметическое выражение в строчке 3 даст в результате 0 для возрастов от 16 до 63 — будет переход на метку person0 — и выполнение программы дальше строки. Если человек оказался старше, результат будет 1 и переход на метку person1 — в ней человека предупреждают что он слишком стар и программа заканчивается командой end. Наконец для слишком молодого человека нужной метки не найдётся и выполнение закончится в строчке 5.

Из очевидных недостатков — выражение и метки довольно туманные — а кроме того человек от 112 лет и старше узнает что он «слишком молод».

Улучшим этот код с помощью операторов сравнения

exec "prompt", "your age?"; age = _  goto "person" + ((age > 64) - (age < 16))  exec "output", "you are too young to play!"; end person1: exec "output", "you are too old to play!"; end person0: exec "output", "check passed, let's play!"

Здесь изменилось только выражение в goto — поскольку операторы сравнения возвращают 0 или 1 то результат будет строго одним из трёх 0, 1, 2 — правда сами метки всё-таки имеют не очень понятные имена.

Можно улучшить и это используя логические операторы

exec "prompt", "your age?"; age = _  goto "person." + ((age > 64 & 'old') | (age < 16 & 'young'))  exec "output", "you are eligible to play!" # some game here end  person.old: exec "output", "you are too old to play!"; end person.young: exec "output", "you are too young to play!"; end

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

Стеки и Подпрограммы

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

Для операций со стеком добавлены привычные команды PUSH и POP — первая сохраняет значение переданного выражения на стеке, вторая наоборот выталкивает ранее сохранённое значение в указанную переменную (или ячейку массива). Их можно использовать и не только для вызовов подпрограмм, но и для временного хранения значений. Быть может интересно было бы добавить несколько команд-операций прямо на стеке в духе FORTH но пока это не цель нашего мини-языка.

Кроме того обе команды принимают дополнительный параметр в котором можно предать имя стека (то есть можно использовать не только стек по умолчанию а любой массив). Пока не 100% уверен что это нужная фича 🙂

push "Arzamas-"          # пуш в дефолтный стек push 16, "other"         # пуш в стек (массив) с именем 'other' pop x                    # поп из дефолтного стека pop y, "other"           # поп из массива с именем 'other' exec "output", x + y

Две переменные с невыразительными именами PC и SP связаны с исполняющей системой интерпретатора — первая это счетчик инструкций (program counter) а вторая является указателем стека (stack pointer).

Первая позволяет выполнять переходы к подпрограммам и возвраты таким образом:

exec "output", "Starting..." push pc+1; goto test_subroutine exec "output", "Complete!" end  test_subroutine: exec "output", "Preved Medved" pop pc

Здесь перед выполнением goto мы заталкиваем в стек адрес инструкции следующей за goto (тут немного тонко: при выполнении самого push счетчик pc уже указывает на следующую за ней goto — т.к. он увеличивается при выборке команды, до исполнения — поэтому прибавив к нему единицу мы получим правильный адрес возврата).

А команда pop pc попросту вытолкнет этот сохранённый ранее адрес обратно в указатель инструкций и «переход» произойдёт сам собой. Здесь закрадывается мысль — ведь присваивая значения к PC мы могли бы вообще без goto обходиться, одними присваиваниями — к сожалению манипулировать физическими номерами строк вместо меток конечно уж слишком неудобно и «низкоуровнево». Хотя такая возможность остается — можете поэкспериментировать!

Переменная SP позволяет «подглядывать» значения в стеке (и даже подменять их). Дело в том что сам стек хранится как массив STACK поэтому значения в нём доступны в программе:

push 8 push 13 exec "output"; stack[sp] + ':' + stack[sp-1]  # напечатает 13:8

Пожалуй, эти трюки можно использовать для передачи параметров в сишном стиле (но эту мысль я ещё не додумал).

Пример побольше — Простые Числа

Попробуем изобразить программу заполняющую массив простыми числами (способом trial division). Массив простых чисел будет P[...] — и в начале работы удобно первые несколько элементов в нём предзаполнить. Тут-то и сказывается один из недостатков текущей версии языка — у нас нет литералов для массивов. Ну не беда, возьмём число 7532 и разобрав его на цифры занесем их в массив. В остальном программа незамысловатая — числа-кандидаты в переменной i проверяются с помощью подпрограммы trynext, которая возвращает в _ либо 0 либо 1 в зависимости от простоты числа. Если число не простое, переходим к метке skipthis. Останавливаемся дойдя до 100.

В данной версии программа печатает найденные числа — но можно убрать команду печати и увеличить лимит, чтобы оценить насколько быстро (медленно) работает интерпретатор.

x = 7532; n = 0 initp: p[n] = x%10; x = x\10; n = n+1; goto x=0|"initp"  i = 11 addmore: push pc+1; goto trynext; goto _|"skipthis" p[n] = i exec "output", n + ": " + i n = n+1 skipthis: i = i+2; goto i>100|"addmore" end  trynext: _ = 1; t = 1 nextdiv: d = p[t]; goto d*d<=i|"isprime" goto i%d|"notprime" t = t+1; goto nextdiv notprime: _ = 0 isprime: pop pc

Пример взаимодействия с JS — игра в кости

Сэм Лойд приводит такую игру в кости под названием Fair Dice Game (слово «fair» лукаво — может означать «честная» или «ярмарочная»): игрок выбирает число от 1 до 6 и бросает три кубика. Если число выпадает хоть на каком-нибудь кубике, он получает выигрыш в размере ставки (удвоенной или утроенной если число появилось на 2 или 3 кубиках сразу). В противном случае его ставку забирает «казино».

Как вам кажется, справедливы ли шансы в этой игре?

скриншот веб-страницы нашей игры

скриншот веб-страницы нашей игры

Вот отличная возможность проверить — небольшая реализация этой игры в веб-страничке. Здесь логика написана на ZLE — в то время как JavaScript используется для связи элементов страницы с интерпретатором. Происходит это так:

  • код «игровой логики» записан в переменную в скрипте, в виде многострочного литерала

  • он тут же парсится с помощью zle.parseLines(...) и готов к выполнению

  • к кнопкам выбора числа (1..6) привязан вызов функции запускающей интерпретатор

  • перед запуском необходимые значения (ставка, выбранное число и пр) проставляются в переменные интерпретатора

  • каждый раз после выполнения кода игровой логики интерпретатором JS проверяет переменную continue и повторяет вызов интерпретатора (с небольшой задержкой) покуда это необходимо — это позволяет из кода на ZLE устроить простейшую «анимацию» бросания костей

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

Полюбуйтесь например на код, который «катает» кости — он выбрасывает всё новые и новые значения для каждого кубика, пока значение не повторится (с предыдущим) — после чего отмечается что данный кубик остановился (в массиве F) — сами значения костей попадают в массив D, а значения на кубике генерируются подпрограммой «cast».

cont: i = 0; s = 0 roll: push pc+1; goto cast goto _ <> d[i] & f[i] | 'rollstop'    # особенно прекрасная строчка d[i]=_; s=s+1; goto rollnext rollstop: f[i]=0 rollnext: i=i+1; goto i=3|'roll' exec "console.log", "rolled " + s continue = s>0

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

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

Заключение

В целом мы видим, что при написании «практического» кода мы обычно используем логические выражения в GOTO, так что на самом деле разница с IF-ELSE-ами не слишком велика. Выражения позволяют, впрочем, конструировать достаточно хитроумные переходы, но это не слишком часто нужно (и не всегда это удаётся сделать удобно). Хотя может быть нужно развивать привычку к таким выражениям чтобы эффективнее их использовать 🙂

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

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


ссылка на оригинал статьи https://habr.com/ru/articles/868726/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *