После того, как я научился как следует перекладывать JSON’ы, я решил получше изучить еще какой-нибудь необычный инструмент. В юниксах есть такой древний (старше языка C) калькулятор — dc. Причем этот калькулятор до сих пор жив в том смысле, что почти везде входит в стандартную поставку. Даже на маках есть. Но еще, как выяснилось, это своего рода язык программирования. Мимо такого я пройти не смог.
Первые шаги
К сожалению, интерфейс у нашего калькулятора не поражает интуитивностью. Если запустить его, нас не поприветствуют буквально ничем. Ну что же, раз это калькулятор, попробуем для начала сложить два и два:
2 + 2 dc: stack empty
Н-да. Кажется, мы делаем что-то не так. Выходим из шайтан-машины (попутно узнав, что Ctrl+C работает как надо) и идем читать мануал. Оказывается, что в dc используется обратная польская запись. Поэтому наше выражение должно записываться так:
2 2 +
Окей, ошибки в этот раз не получили, но и результата на экране не наблюдается. Снова идем в мануал. Оказывается, результат вычислений кладется в основной стек, а чтобы его вывести, калькулятору надо об этом дополнительно сообщить. Что ж, введем еще команду p:
p 4
Со стеком стоит разобраться получше: это для dc прямо альфа и омега. Туда складываются не только результаты вычислений: так, например, в dc каждое число — это отдельная команда, смысл которой — положить это число в стек. И арифметические операторы тоже работают не с тем, что написано перед ними, а со стеком: команда + вытаскивает из стека два верхних элемента, складывает их и кладет результат в стек. Поэтому теперь, когда у нас в стеке уже лежит число 4, мы можем положить туда что-нибудь еще, и сложить:
3 # поместим в стек число 3 + # сложим два верхних элемента стека (3 и 4), результат поместим в стек p # напечатаем верхний элемент стека 7
Что ж, считать научились. Теперь было бы неплохо научиться писать: по счастью, в dc можно работать и со строками. Строка — это то, что заключено в квадратные скобки. По традиции напишем хелловорлд, он выглядит так:
[Hello world!] p
Как и с числами, содержимое строки мы положили на стек, а потом распечатали его командой p.
Базовое программирование
Итак, мы научились считать, вводить и выводить числа и строки. Для калькулятора это уже немало! Но для полноценного программирования этого пока не хватает, поэтому продолжаем изучение языка. Мануал говорит, что в dc есть целых 256 регистров, в которые можно что-нибудь записать. Они, кстати, тоже могут быть стеками, но нам пока такое без надобности. Попробуем с ними поработать:
[Hello world!] # Положим в стек строку "Hello world!" sx # Вытащим из стека верхний элемент и сохраним его в регистре x p # Распечатаем верхний элемент стека… dc: stack empty # … и закономерно получим ошибку: ведь в стеке теперь ничего нет! lx # Положим в стек содержимое регистра x p # Распечатаем верхний элемент стека Hello world!
Теперь в нашем распоряжении есть настоящие переменные с глобальной областью видимости: ведь содержимое регистров не меняется, когда мы оперируем стеком! Следующий шаг — научиться писать функции. В dc функции (а точнее, макросы) реализованы так: если мы сохраним в каком-нибудь регистре строку, ее можно попробовать исполнить как подпрограмму. Поэтому функции — это, по существу, те же самые строки, с которыми мы уже умеем работать. Осталось научиться их вызывать, а это совсем просто, достаточно ввести команду lRx, где R — регистр:
[la lb +] sS # Поместим в регистр S строку "la lb +", которая складывает содержимое регистров a и b и кладет сумму в основной стек 1 sa # Поместим в регистр a число 1 2 sb # Поместим в регистр b число 2 lSx # Выполним как подпрограмму строку, содержащуюся в регистре S p # Распечатаем верхний элемент стека 3
Еще не помешало бы ветвление. И операторы ветвления нам тоже завезли! Поскольку мы имеем дело с калькулятором, ветвления тут основаны на сравнениях. Из основного стека достаются два верхних элемента и сравниваются между собой. Если сравнение удовлетворяет заданному условию, то выполняется макрос, сохраненный в заданном регистре. Например:
[[Equal] p] sE # В регистр E положим программу, которая печатает строку "Equal" [[Non equal] p] sN # В регистр N положим программу, которая печатает строку "Non equal" 1 0 # Поместим в основной стек 1 и 0. =E # Вытащим два верхних элемента стека. Если они равны между собой, выполнится подпрограмма из регистра E. Поскольку 1 ≠ 0, она не выполнится. 1 0 # Еще раз поместим в основной стек 1 и 0. !=N # Вытащим два верхних элемента стека. Если они не равны между собой, выполнится подпрограмма из регистра N Non equal
Из базового программирования остаются циклы. Их нам не завезли, но мы можем легко реализовать рекурсию: достаточно организовать вызов макроса из самого себя при определенных условиях. Например, числа от 1 до 10 можно вывести так:
[ li p # Выводим значение итератора 1 + si # Сохраняем новое значение итератора li 11 >C # Рекурсивный вызов макроса, пока число в регистре i меньше 11 ] sC 1 si # Инициализируем итератор lCx # Вызываем макрос
Обратите внимание: когда мы прибавляем единицу в макросе, мы не говорим вначале li, потому что текущее значение уже есть в стеке — мы его засунули туда строчкой выше.
Продвинутое программирование
Теперь мы знаем достаточно, чтобы написать какой-нибудь FizzBuzz. Это можно сделать так:
[[Fizz] P 0 sd] sF # Макрос с печатью слова Fizz и снятием флага печати числа [[Buzz] P 0 sd] sB # Макрос с печатью слова Buzz и снятием флага печати числа [li 3 % 0 =F li 5 % 0 =B] sW # Макрос, печатающий нужное слово [10 P] sP # Макрос, печатающий перенос строки [li p c] sD # Макрос, печатающий число и чистящий стек [ 1 sd # Выставляем флаг печати числа lWx # Вызываем макрос из регистра W, который, возможно, напечатает строку ld 0 =P # Если флаг печати числа не стоит, печатаем перенос строки… ld 1 =D # … а если стоит — печатаем число li 1 + si # Увеличиваем на 1 содержимое регистра i li 101 >M # Если число в регистре i меньше 101, рекурсивно вызываем макрос M ] sM 1 si # Инициализируем итератор lMx # Запускаем подпрограмму из регистра m
В листинге есть две команды, которые я еще не упоминал: P и c. Команда c просто очищает основной стек: хранить там старые числа нам без надобности. Команда P вытаскивает верхний элемент стека (в отличие от p). Если это строка, дальше она печатается без переноса строки, что дает нам возможность «склеить» Fizz и Buzz при последовательных вызовах lFx и lBx. Если это число, то оно выводится как последовательность байтов. В регистре P я сохранил строку [10 P], потому что 10 — это как раз символ переноса строки.
Окей, FizzBuzz писать научились, можно идти собеседоваться на Senior dc developer. Но червь сомнения все же еще гложет — а ну как спросят про последние проекты? Не помешало бы иметь за спиной хотя бы какой-то опыт разработки…
Поэтому я реализовал на dc Peg solitaire. Это такая классическая головоломка, где надо прыгать по игровому полю колышками и убирать их, пока не останется один. Естественным образом состояние поля представляется двоичным числом, с которым можно проводить манипуляции при помощи сложения и вычитания. Формулы валидации в обратной польской записи выглядят совершенно инфернально, но все же это работает :)

