Это ещё один "эзотерический" язык, не относитесь к нему слишком серьёзно :)
Некоторые языки хвастаются отсутствием циклов - например в 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
- и выполнение программы дальше 7й
строки. Если человек оказался старше, результат будет 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(...) в бейсике).