Язык Piet: программы, выглядящие как абстрактные картины
Существует отдельный класс языков программирования, называемых эзотерическими. Они придуманы не для решения реальных задач, а ради шутки, для исследования границ возможностей языков или для доказательства какой-то идеи.
Например, программы на языке Whitespace состоят только из пробелов и табуляций. Программы на Shakespeare выглядят как сонеты Шекспира, на Rockstar — как power metal баллады. А программу на Malbolge написать практически невозможно — язык специально сделан максимально сложным.
Piet — это один из эзотерических языков, придуманный физиком и автором комиксов Дэвидом Морган-Маром. Название отсылает к художнику Питу Мондриану и его абстрактным композициям.
Программы на Piet выглядят как разноцветные геометрические абстрактные картины. При этом существует много возможных способов написать одну и ту же программу. Например, картинка ниже — один из вариантов "Hello World".
Сущности языка
Простейшая сущность Piet — это кодель (codel) — один пиксель. Слово образовано из слияния слов «код» и «пиксель».
Несколько находящихся рядом пикселей одного цвета образуют цветовой блок (color block).
Piet работает только с целыми числами и символами юникода. Поэтому чаще всего количество пикселей одного цвета используют, чтобы задать конкретное целое число.
В основе языка лежит тип данных стек — это список, работающий по принципу LIFO («последним пришёл — первым вышел»).
Исполнение программы
Программа на языке Piet — это изображение, составленное из цветовых блоков по своим особым правилам. Исполняется эта программа начиная с левого верхнего угла: от одного коделя к другому ориентируясь на значения двух указателей.
Первый — указатель направления (direction pointer). Он может указывать направо, вниз, налево или вверх и нужен, чтобы интерпретатор понимал, какой кодель читать следующим.
Второй — переключатель коделей (codel chooser). Он может указывать направо или налево и помогает интерпретатору определить, в какую сторону двигаться, если возможных вариантов продолжения программы несколько.
Естественными ограничителями программы являются границы изображения и блоки чёрного цвета — через них пройти нельзя. Если указатель направления показывает на чёрный блок, то направление чтения коделей изменится на следующее возможное.
Программа завершается, если за восемь последовательных попыток интерпретатор застревает и не может сдвинуться с текущего цветового блока.
Цветовое кодирование
Как можно понять из примера программы, для написания кода используются шесть основных секторов цветового круга с их оттенками, дополненные белым и чёрным цветами.
Чёрные блоки нужны, чтобы задавать границы исполнения. Белые — просто пустое пространство, интерпретатор их игнорирует.
Остальные цвета кодируют команды языка. Команды определяются через изменения в оттенке и насыщенности двух соседних цветовых блоков.
Я не буду сейчас приводить описания каждой команды, их можно прочитать на сайте автора. Но часть команд я чуть позже подробно объясню на примере.
Интерпретаторы
Для написания и отладки кода я использовала программу Pietron. В сети также можно найти несколько онлайн интерпретаторов, например, вот этот. Сразу предупрежу, что разные интерпретаторы немного вольно трактуют некоторые правила, поэтому программа, написанная в одном редакторе, может сломаться в другом.
Как написать программу на Piet
Изначально идея освоить Piet у меня возникла, когда я решала задачи из Advent of Code этого года. Я хотела решить одну из них, но в итоге начала с чего-то более простого. Так я написала программу, которая вычисляет сумму любого количества переданных на вход положительных целых чисел.
И сейчас я расскажу, из каких блоков она состоит и как работает. Для удобства я визуально разбила содержательную часть программы на части.
1. Инициализация
Я буду суммировать входные значения и хранить в стеке промежуточное значение суммы. Изначально сумма равна нулю, поэтому мне нужно поместить в стек 0.
Это делают три коделя первого блока. Начальный кодель программы может быть любого цвета, это влияет только на цветовую гамму. Я выбрала тёмно-синий цвет. Идущий вторым кодель светло-синего цвета — команда push
, она помещает в стек число, равное количеству коделей предыдущего блока. В результате в стеке окажется число 1. Чтобы превратить 1 в 0 я использовала команду not
, она берёт значение из стека, инвертирует его и помещает обратно в стек.
Как определяются команды
Сразу на примере объясню про цвета и оттенки. Чтобы понять, какой цвет выбрать для следующего блока, нужно смотреть на таблицу команд и цвет предыдущего блока.
Для первого коделя я выбрала тёмно-синий цвет. В этом нет скрытого смысла, мне просто так захотелось. Чтобы получить следом команду push
, как показано в таблице, мне нужно остаться в том же оттенке, но сдвинуть его насыщенность на один шаг темнее. Темнее тёмно-синего в палитре нет, поэтому идём в начало и берём светло-синий.
Похоже получается и команда not
. По таблице из верхней левой ячейки нужно сделать сдвиг на два шага по оттенку, получив красный вместо синего, и на два шага по насыщенности, получив тёмный вместо светлого.
Кстати, можно не пугаться — по таблице вручную ничего определять не придётся. Во всех интерпретаторах языка есть подсказки и расшифровки команд.
2. Суммирование
Когда у меня всё заготовлено, я могу начинать суммировать значения.
Значения я получаю из входящих данных, в интерпретаторах это выглядит как обычное текстовое поле, которое можно заполнить до запуска программы. Мои данные — это положительные числа, каждое на новой строчке. Для удобства последним числом будет цифра 0. Появление нуля я буду использовать как критерий завершения суммирования.
Вторая часть моей программы тоже состоит из всего трёх коделей. Первый из них — команда dup
, дублирование последнего значения в стеке. Непосредственно для суммирования она мне не нужна, но очень поможет в будущем. Дело в том, что большинство команд Piet берут параметры из стека и меняют их. Поэтому единственный способ что-то сделать со значением, но не потерять его при этом — это скопировать его заранее.
Второй кодель — это команда in
. Она читает число из входного потока данных и помещает его в стек.
Третий кодель — команда add
. Она вынимает два последних числа из стека, складывает их и помещает результат обратно в стек. В конце в стеке оказывается старое начальное значение 0 и новая промежуточная сумма 0 + N1, где N1 — первое число из входных данных.
3. Проверка
Это самый большой и наиболее сложный для понимания блок.
Так как количество суммируемых чисел у меня может быть любым, то я не могу бесконечно копировать последовательность команд in-add
, мне нужен цикл. Циклы в Piet делаются визуальным разветвлением цветовых блоков. Исполняется тело цикла, потом делается проверка и если условие продолжения цикла выполняется, то с помощью специальных команд можно направить интерпретатор так, чтобы он вернулся в самое начало программы.
Условия для цикла можно придумать разные. Здесь я остановилась на таком: если после прибавления последнего введённого значения сумма не изменилась, то надо заканчивать цикл. Неизменная сумма будет означать, что во входных данных встретился 0, и это мой критерий завершения программы.
На этом этапе у меня в стеке как раз находятся два числа — предыдущее и текущее значения суммы. И есть команда great
, которая может сказать мне, какое из чисел больше. Но не всё так просто: great
, как и многие другие команды Piet, меняет стек. Я не могу сразу сравнить две суммы, потому что тогда я их потеряю — они исчезнут из стека и будут заменены на результат сравнения.
Поэтому первая команда у меня опять dup
, я хочу продублировать текущее значение суммы, чтобы сохранить его после сравнения. Мой стек станет равен <0, N1, N1>
. Но теперь я уже не могу ничего нормально сравнить. Я ведь хотела сравнивать 0 и N1, а 0 в самом низу.
Чтобы это исправить, мне нужно перемешать стек так, чтобы 0 оказался сверху. Перемешивание делается с помощью команды roll
. Она берёт два последних значения стека <K, L>, где L — последнее, а K — предпоследнее, и циклично сдвигает следующие K позиций в стеке L раз. Например, если K — 4, L — 1, а оставшийся стек <1, 2, 3, 4, 5>, то после roll
стек превратится в <1, 5, 4, 3, 2>.
Мне нужно из <0, N1, N1>
получить <N1, N1, 0>
, то есть вызвать roll(3, 2)
. Параметры 3 и 2 берутся из стека, и сначала их нужно туда записать двумя командами push
. Чтобы записать 3, я делаю предыдущую команду dup
блоком из трёх коделей, а чтобы записать 2, я делаю команду push
блоком из двух.
Теперь можно сделать roll
и получить нужный мне для сравнения стек. После я вызываю команду great
, она берёт два последних значения из стека, сравнивает их и помещает в стек 1, если второе число больше первого и 0, если наоборот.
4. Цикл
У меня всё готово для цикла. Условие звучит так: если в стеке сверху лежит 1, то суммирование должно продолжиться, а если лежит 0, то нужно заканчивать.
Как я писала выше, циклы в Piet визуальные — нужно повернуть направление чтения коделей таким образом, чтобы в результате оказаться в самом начале.
За выбор следующего коделя отвечает указатель направления. Изначально он указывает направо, поэтому программа начинает исполнение из левого верхнего угла по прямой направо.
Указатель направления можно поменять командой point
. Она принимает один числовой параметр и поворачивает указатель нужное число раз. Например, point(1)
изменит указатель со значения «направо» на значение «вниз», а point(2)
— на значение «налево».
У меня всё подготовлено для блока point
— наверху стека может лежать 0 или 1. Если там 0, то исполнение программы продолжится направо, а если 1, то повернёт вниз.
Казалось бы, всё отлично, но есть нюанс. Я хочу вернуться в начало цикла и чтобы первой командой там по-прежнему была команда dup
. Но после блока цвета тёмной мадженты тёмно-красный блок станет командой add
, а тёмно-синий — in
. Если я просто поверну, не сбросив цвета, то я сломаю себе весь цикл.
Но я могу воспользоваться интересной особенностью языка. По таблице команд светло-голубые кодели после тёмной мадженты — команды roll
. По идее они должны менять стек, но если перед ними оказываются блоки белого цвета, то они игнорируются и ничего не делают. Ещё я могу использовать чёрные блоки вокруг для ограничения рабочей области — через них интерпретатор пройти не сможет.
После команд roll
у меня есть одна команда push, которая поместит в стек 1. И я сразу попадаю в тёмно-красный блок, который оказывается командой point
, забирает единицу из стека и поворачивает указатель направления.
Я пришла в начало, следующим стал тёмно-синий блок, который как мне и нужно, представляет команду dup
.
5. Выход из цикла
На последней итерации цикла я получу из входных данных 0. Прибавление нуля не изменит предыдущее значение, и команда roll
ничего не перемешает, ведь в стеке окажутся три одинаковых значения. Условие цикла, команда great
, вернёт 0, а команда point
не поменяет указатель — исполнение программы продолжится в том же направлении, напечатав результат с помощью команды out
. Интерпретатор окажется в белой области без каких-либо команд и завершит исполнение программы.
Итог
Ничего из задач Advent of Code я так в итоге на Piet и не решила.
Но я рада, что наконец реализовала свою давнюю хотелку и написала свою первую полноценную программу, пусть она и довольно простая. Мне было весело и интересно!
Возможно, в будущем я и другие эзотерические языки попробую. А вы пробовали?