Стековое программирование с человеческим лицом (часть вторая)

в 12:10, , рубрики: forth, ненормальное программирование, стековое программирование

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

image

Давайте сразу расставим все точки над 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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js