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

Игровое поле с колышками-звездочками
Игровое поле с колышками-звездочками