Как и следовало ожидать, предыдущий пост вызвал противоречивые комментарии. Кого-то устраивает и существующий Форт для решения вопросов, кого-то (как и меня) раздражают его особенности.
Давайте сразу расставим все точки над i: я не пытаюсь сочинить замену Форту. Форт — семейство среднеуровневых языков программирования, которое продолжает продуктивно решать поставленные задачи и на покой не собирается. Но я размышляю в другой нише: высокоуровневый стековый язык с упором на лёгкость чтения программ для начинающих (насколько это вообще возможно). Большая традиционность и высокоуровневость имеет свои достоинства, но при этом теряются некоторые особенности (в том числе и положительные) Форта.
У нового воображаемого языка появляется своя философия и свои концепции. Об этом я и продолжу писать.
Выбор конструкций циклов
Каждый творец языка, в котором есть не только рекурсия, рано или поздно задумывается о наборе циклических конструкций. Я предпочитаю сперва обдумать конструкцию общего универсального цикла, из которого можно вывести остальные, а потом исходя из практического опыта добавить дополнительные конструкции для наиболее часто встречающихся случаев.
Основной цикл выглядит так:
repeat
любые-слова
when условие1 do слова1
when условие2 do слова2
when условиеN do словаN
otherwise ветвь-иначе
end-repeat
Страшно, правда? Но на самом деле это лишь формальное описание, работает всё элементарно. Каждую итерацию выполняются все любые-слова после repeat. Затем происходит вычисление условий when. Если условие1 истинно, то выполняются слова1 и цикл начинает новую итерацию с самого начала. Если условие1 ложно, то происходит переход к следующему условию. Если ни одно из условий не верно, выполняется ветвь-иначе.
Этот цикл хорош тем, что из него можно вывести всё, что угодно, то есть это общая основная конструкция.
1. Бесконечный цикл (необязательные when и otherwise опущены, они не нужны):
repeat
любые-слова
end-repeat
Предполагается, что где-то внутри цикла будет if с условием завершения и слово exit-repeat.
2. Цикл с предусловием:
repeat
when условие-истинно? do тело-цикла
otherwise exit-repeat
end-repeat
3. Цикл с постусловием:
repeat
тело-цикла
when условие-выхода do exit-repeat
end-repeat
4. Цикл со счётчиком (возьмём целочисленную переменную counter для примера):
repeat
counter @ (проверка счётчика)
when 100 < do
|counter @ ++| counter set (увеличиваем на единицу счётчик, если он меньше 100)
тело-цикла
otherwise exit-repeat
end-repeat
5. Цикл с выходом в середине:
repeat
какой-то-код
when выходим? do exit-repeat
otherwise продолжаем
какой-то-другой-код
end-repeat
6. И даже цикл Дейкстры!
Посмотрим, что получилось. Бесконечный цикл интуитивно понятен и лаконичен, никаких лишних слов, поэтому просто оставим его как есть. Цикл с постусловием встречается реже for и while, поэтому в отдельной конструкции смысла нет. Если же такой цикл всё-таки понадобился, его можно легко вывести из общей конструкции, благо он получился ясным и лаконичным.
А вот циклы с предусловием и со счётчиком получились более неуклюжими. Так как они часто нужны, их имеет смысл реализовать в виде отдельных слов:
1. Цикл с предусловием:
while условие do
слова
end-while
2. Цикл со счётчиком:
for имя-переменной начальное-значение to конечное-значение step шаг do
слова
end-for
3. И цикл с постусловием (при большом желании):
loop
слова
until условие-выхода
end-loop
Обратите внимание на важный момент: циклы while и loop можно реализовать в виде простой текстовой подстановки. Действительно, если while заменить на «repeat when», а end-while на «otherwise exit-repeat end-repeat», то получится общий цикл. Аналогично цикл с постусловием: loop на «repeat», until на "", а end-loop на «when not do exit-repeat end-repeat». Сам же цикл repeat при желании можно преобразовать в набор if.
То есть нам нужно реализовать в трансляторе только два цикла: repeat и for. Циклы while и loop можно сделать на текстовых подстановках средствами самого языка. Подобный подход принят в Eiffel и Lisp: у нас есть обобщённая конструкция (к примеру, loop в Eiffel и cond в Lisp), которую можно очень гибко использовать. Новые конструкции при возможности реализовываются поверх старой. В Форте принцип противоположен: у нас есть масса частных случаев и инструменты, при помощи которых мы при необходимости сами можем создать нужные конструкции.
У каждого подхода есть свои плюсы и минусы. Подход от «общего к частному» хорош в высокоуровневом программировании, ведь программисту не приходится заглядывать в «потроха» транслятора и мудрить с реализацией очередного велосипеда. В Форте придётся сперва изучить внутренности форт-системы и вводить новые слова, но зато потом появляется возможность неограниченно использовать новое слово, исполняемое быстро и эффективно. Не то, что бы один подход лучше другого, они просто разные. Так как меня волнуют удобство чтения кода и простота для новичков, я принял первый подход как основной.
Условные конструкции
Аналогично поступим с условными выражениями. Обобщённая конструкция (привет, Lisp!):
cond
when условие1 do ветвь1
when условие2 do ветвь2
when условиеN do ветвьN
otherwise ветвь-иначе
end-cond
Работает cond аналогично repeat, но только один раз, а не многократно. Для досрочного выхода предусмотрено слово exit-cond. Cond тоже можно вывести из repeat: нужно просто после каждой when-ветви поставить exit-repeat. Так-то!
Хотя при помощи cond можно делать ветвления любой сложности, некоторые часто встречающиеся шаблоны полезно реализовать отдельно.
1. Простейший условный оператор:
условие if
ветвь1
else
ветвь2 (конечно, необязательная)
end-if
2. А вот конструкция case специфична для стекового программирования. Предположим, у нас есть какая-то переменная x и нам нужно в зависимости от значения x выполнить определённую cond-ветвь. Перед каждым when в cond или перед каждым if (в зависимости от способа реализации) нам придётся ставить dup, то есть заботиться о дублировании и/или drop'е лишних элементов:
: testif
dup 1 = if ." One" else
dup 2 = if ." Two" else
dup 3 = if ." Three"
then then then drop ;
«Шумовые» слова здесь совершенно лишние. Действительно, если такие ситуации регулярно встречаются, почему бы не автоматизировать dupы и dropы, повысив читаемость и лаконичность? А если нам нужно сравнить две переменные? А если три? Это же сколько придётся со стеком перед каждым условием мудрить!
Специально для подобных ситуаций нужна конструкция case — более «умный» вариант cond. Синтаксис очень похож:
case число-сравниваемых-элементов
when условие1 do ветвь1
when условие2 do ветвь2
when условиеN do ветвьN
otherwise ветвь-иначе
end-cond
Основное отличие заключается в том, что каждый раз перед when сравниваемые элементы удваиваются, а перед end-case из стека лишние копии убираются. То есть case = cond + расставленные dup и drop. Число-сравниваемых-элементов указывает, сколько элементов на вершине стека нужно удвоить:
x @
y @
case 2 [x y -- x y x y]
when = do "равны" print-string
when < do "y больше" print-string
otherwise "x больше" print-string
end-case
Вводим новые слова и описываем подстановку
Ну, строительные блоки у нас уже есть. Остался раствор — определение новых слов. Такие слова вызываются через call, если говорить в терминах языка ассемблера:
define имя тело end
define-macro имя тело end-macro
А вот такие слова работают через текстовую подстановку: имя заменяется на тело. Как нетрудно догадаться, while, loop и if можно реализовать на макросах следующим образом:
define-macro while
repeat when
end-macro
define-macro end-while
otherwise exit-repeat
end-repeat
end-macro
define-macro loop
repeat
end-macro
define-macro until
when
end-macro
define-macro end-loop
not do exit-repeat
end-repeat
end-macro
define-macro if
cond when do
end-macro
define-macro else
otherwise
end-macro
define-macro end-if
end-cond
end-macro
Введём ещё одно знакомое слово для красоты:
define-macro break!
do exit-repeat
end-macro
Теперь можно очень лаконично и понятно писать что-то вроде
repeat
...
when 666 = break!
...
end-repeat
Итого: благодаря гибким конструкциям repeat и cond при помощи элементарной текстовой подстановки можно реализовать целый набор строительных блоков на все случаи жизни. В отличие от Форта, совершенно не приходится думать о реализации слов, мы абстрагируемся от деталей реализации.
К примеру, слово when в cond и repeat носит скорее косметический характер: оно позволяет визуально отделить условие от прочих слов. Но на самом деле какую-то роль играет лишь слово do. Хотите предельной лаконичности?
define-macro ==>
when do
end-macro
и пишите
cond
x @ y @ = ==> "равны"
x@ y @ < ==> "y больше"
otherwise "x больше"
end-cond
print-string
совершенно не задумываясь над реализацией транслятора. Это не наша забота, мы на высоком уровне!
В следующий раз разговор пойдёт о разборе транслятором исходного кода программы, о стеках, данных и переменных.
Автор: kedoki