[Перевод] Безымянный язык программирования без присваивания имён
Давать имена сложно. Давайте посмотрим, как далеко мы можем зайти без них.
Что это?
Это язык программирования, основанный на трёх парадигмах:
Основная «фишка» языка — избегание любых наименований. Оставаясь верным этой максиме, сам язык тоже не имеет названия. «Язык программирования без имён» (namingless programming language) — это его определение.
Так как в мире есть только один такой язык, название ему не нужно.
Для чего это нужно?
В основном ради развлечения. Это язык для хобби-программирования.
Ну, разумеется, его можно использовать и как инструмент для обучения бесточечному (комбинаторному), стековому или массиво-ориентированному программированию. Или применять его в качестве пытки, я не буду вас судить.
Как выглядит код на таком языке?
Вот так:
i_^_b_H_i_cpp^_)_V_b_v_J_^_E_H_leafL_==^_)_V_H_Z_Z_^_)_V_H_I_^_E_1^_2^_#_G_Z_Z_^_E_1^_2^_#_H_$_L_-^_G_m_G_&_&_
Чёрт возьми!
Ага.
Простите.
Каковы его основные концепции?
Структуры данных
Существует только одна-единственная структура данных. Так как она только одна, название ей не нужно.
Эта структура данных — дерево char. Каждый его узел — это или содержащий char лист, или ветвь, содержащая динамический массив структур данных. Массив может быть и пустым. Пустая ветвь — всё равно ветвь.
Строка — это ветвь, в которой каждая подветвь — это лист.
Число — это строка. Именно так. Язык поддерживает десятичную арифметику со строками. Привет, Кобол!
Массив — это ветвь, в которой каждая подветвь — это строка.
Матрица — это ветвь, в которой каждая подветвь — массив.
Стоит отметить, что ни один из этих терминов не является названиями чего-то специфичного для этого языка. Это широко известные структуры данных, которые можно реализовать в структуре данных языка.
Операции
В языке есть и операция. Не «операции», а «операция», потому что в языке она только одна. Так как она только одна, название ей тоже не нужно.
Операция выглядит как _
, она берёт последний элемент из ветви перед ней и действует соответствующим образом. То есть, по сути, семантика операции задаётся парой, состоящей из префикса (символа перед операцией) и самой операции.
Например, чтобы сложить два и два, нужно:
Поместить char 2 в виде строки. Для этого используется пара
^
и_
.2^_
Затем нужна ещё одна 2 в виде строки. Можно сделать ещё раз
2^_
, но можно и дублировать последний элемент в текущей ветви паройH
и_
.H_
Далее нужно выполнить само сложение. Это сделать легко,
+
и_
.+_
То есть готовая программа будет выглядеть так:
2^_H_+_
А после запуска результат будет таким:
4
Здорово, но ведь программа, написанная на языке без имён, должна иметь имя, так?
Чтобы избежать проблем с именованием создаваемых вами программ, язык использует в качестве исходного кода имя исполняемого файла.
Подумайте над этим.
Вы получаете интерпретатор исполняемых файлов для языка и переименовываете его в валидный код на этом языке. Благодаря этому запускаемый файл сам по себе не должен иметь имени, а сама программа — это имя запускаемого исполняемого файла.
Так что для запуска приведённой в начале статьи пугающей программы нужно сделать следующее:
./build.sh
mv the_namingless_programming_language i_^_b_H_i_cpp^_)_V_b_v_J_^_E_H_leafL_==^_)_V_H_Z_Z_^_)_V_H_I_^_E_1^_2^_#_G_Z_Z_^_E_1^_2^_#_H_$_L_-^_G_m_G_&_&_
./i_^_b_H_i_cpp^_)_V_b_v_J_^_E_H_leafL_==^_)_V_H_Z_Z_^_)_V_H_I_^_E_1^_2^_#_G_Z_Z_^_E_1^_2^_#_H_$_L_-^_G_m_G_&_&_
Разумеется, можно просто скопировать и изменить уже имеющуюся программу. На самом деле, именно так мы и делаем со скриптами Python, шелла или Perl.
Какие существуют пары для операции?
Вот полный список префиксов-пар операции.
. — выход
U — символ подчёркивания
Z — косая черта
N — обратная косая черта
J — разрыв строки
i — точка
L — пробел
I — одинарная кавычка
Y — двойная кавычка
^ — поднять все последние элементы на одинаковый ранг
| — поместить элемент текущей ветви вверх по индексу
# — удалить все элементы, кроме указанного по индексу для выбранной глубины
m — реплицировать элемент много раз
H — дублировать последний элемент
X — отбросить последний элемент
G — поменять местами последние два элемента
A — поднять пустой элемент
$ — подсчитать
v — опустить последний элемент
+ — сложение
- — вычитание
x — умножение
z — деление
= — равно?
% — численно равно?
< - меньше?
> — больше?
( — подстрока?
) — надстрока?
[ — строка начинается с?
] — строка заканчивается на?
T — булево not
W — булево and
M — булево or
C — по возможности интерпретировать как число, в противном случае 0
& — конкатенировать строки
E — разбить строку
D — объединить строки
V — фильтровать по логическому значению
b — загрузить из файла
p — сохранить в файл
o — удалить файл
e — помощь
Кстати, список автоматически генерируется из исходного кода кодом i_^_b_H_i_cpp^_)_V_b_v_J_^_E_H_leafL_==^_)_V_H_Z_Z_^_)_V_H_I_^_E_1^_2^_#_G_Z_Z_^_E_1^_2^_#_H_$_L_-^_G_m_G_&_&_
.
Самая важная пара операции
Выход
Так как исходный код для языка берётся из имени исполняемого файла, а в Windows точка отделяет имя файла от его расширения, она была выбрана как префикс для завершения исполнения. Кроме того, её можно использовать для отделения кода от комментариев:
i_^_b_H_i_cpp^_)_V_b_v_J_^_E_H_leafL_==^_)_V_H_Z_Z_^_)_V_H_I_^_E_1^_2^_#_G_Z_Z_^_E_1^_2^_#_H_$_L_-^_G_m_G_&_&_._list of operation prefixes
Escape-последовательности
Символ подчёркивания
Так как _
— это команда, её нельзя использовать в произвольных строках. Так что escape-последовательность для символа подчёркивания — это U_
.
U выбрана, потому что как бы содержит в своей нижней части символ подчёркивания. И такой паттерн в языке используется часто. Заменяющая символ графема — это обычно символ с чем-то ещё.
Косая черта
В именах файлов обычно нельзя использовать косую черту. А даже когда это возможно, это не очень хорошая идея, поверьте мне. Поэтому существует escape-пара, помещающая в текущую структуру данных /
, а именно Z_
.
Как и в случае с U, Z — это косая черта с двумя дополнительными линиями.
Обратная косая черта
По той же логике обратная косая черта — это пара N_
.»\» ещё с двумя линиями.
Разрыв строки
J немного походит на значок возврата каретки ⏎. Поэтому пара для разрыва строки — это J_
.
Точка
Так как в Windows точка используется как разделитель между именем и расширением, можно использовать в качестве замены точки i_
, просто чтобы не мешать диспетчерам файлов отображать их так, как они привыкли.
Пробел
Хотя в имя файла вполне можно поместить пробел, иногда это усложняет рутинные операции UI, например, копирование, так что мы будем избегать пробелов, воспользовавшись их заменой — парой L_
.
L — горизонтальная черта, обозначающая сам пробел с дополнительной вертикальной чертой.
Одинарная кавычка
I_
обозначает одинарную кавычку. Возможно, вам будет не очень это понятно, пока вы не увидите пару для двойной кавычки.
Двойная кавычка
Y_
— это двойная кавычка. I и Y, одинарная и двойная. Теперь кажется логичным?
Обработка данных
Поднять все последние элементы на одинаковый ранг
Мне понятна ваша растерянность. У этого префикса нет связного имени. Ну, имена давать действительно трудно, именно поэтому я всё это и затеял.
Обычно эта пара превращает кусок строки в подветвь, содержащую кусок строки.
Например, вам нужно сложить 12 и 34. В обычном языке существуют литералы, синтаксис и операции, так что обычно мы записываем 12 + 34
. В этом языке у нас есть в качестве входных данных только строка символов, поэтому чтобы сложить два числа, нужно сначала отделить одно от другого.
При вводе 12
мы только вводим символ 1
, за которым следует символ 2
. Наша текущая ветвь содержит два элемента-листа.
leaf(1) leaf(2)
Теперь мы поднимаем все элементы одинакового ранга, превращая эти два элемента в подветвь с двумя листьями внутри.
12^_
…и получаем…
branch(leaf(1) leaf(2))
Теперь добавляем ещё два символа:
12^_34
branch(leaf(1) leaf(2)) leaf(3) leaf(4)
А затем мы поднимаем последние элементы, пока они не будут на одном ранге, получив следующее:
12^_34^_
branch(leaf(1) leaf(2)) branch(leaf(3) leaf(4))
И только теперь, когда у нас есть две ветви, внутри которых, по сути, находятся строки, можно использовать ещё одну пару операции, чтобы сложить их.
12^_34^_+_
Так мы получаем новую ветвь, содержащую следующее:
branch(leaf(4) leaf(6))
При выводе мы получим таб и 46
. Можно опустить данные обратно при помощи пары v_
, получив 46
без табов, но это уже совершенно другая история.
Поместить элемент текущей ветви вверх по индексу
Это что-то похожее на []
в обычных языках — способ получения доступа к массиву.
Допустим, наша ветвь заполнена следующим образом:
leaf(1) leaf(2) leaf(4) leaf(8)
Так как мы даже можем знать, сколько в ней листьев, можно начать считать индекс с конца. И по… особым причинам мы начинаем с 0.
Так что пара 2^|
, применённая к нашей подветви, получает третий элемент справа и копирует его в самую правую позицию ветви.
leaf(1) leaf(2) leaf(4) leaf(8) leaf(2)
Удалить все элементы, кроме указанного по индексу для выбранной глубины
Это способ доступа ко всем элементам в матрице или в тензоре или дереве, находящемся на определённой глубине и в определённой позиции.
Возьмём для примера следующую программу:
1^_2^_3^_^_4^_5^_6^_^_^_2^_2^_#_
Сначала мы поднимаем три символа, чтобы они стали строкой (или, в терминологии языка, ветвями, содержащими только листья).
branch(leaf(1)) branch(leaf(2)) branch(leaf(3))
Затем мы поднимаем строки (да, все три), так что теперь они представляют массив строк (или, опять-таки в терминологии языка, ветвь, в которой все ветви содержат только листья).
branch(branch(leaf(1)) branch(leaf(2)) branch(leaf(3)))
Затем мы добавляем ещё три строки и тоже поднимаем их.
branch(branch(leaf(1)) branch(leaf(2)) branch(leaf(3))) branch(branch(leaf(4)) branch(leaf(5)) branch(leaf(6)))
Далее мы вычисляем две ветви, создавая матрицу строк (или, в терминологии языка, ветвь, содержащую множественные ветви с равным количеством ветвей, каждая из которых содержит только листья).
branch(branch(branch(leaf(1)) branch(leaf(2)) branch(leaf(3))) branch(branch(leaf(4)) branch(leaf(5)) branch(leaf(6))))
Или в более традиционной записи:
1 2 3
4 5 6
Теперь мы делаем следующее:
2^2^#_
Первая 2
— это индекс. Она также нумерует элементы с нуля, так что 2
на самом деле означает «третий».
Вторая 2
— это глубина. Она тоже начинается с 0. 2
и 2
означает, что нам нужен третий элемент из ветви на третьем уровне вложенности. Это leaf(3)
и leaf(6)
, но тоже упакованные в ветвь, так что это ближе к branch(leaf(3) leaf(6))
.
Этот префикс операции также может брать данные с нулевой глубины, так что, например, эта программа:
1^_2^_3^_2^_0^_#_
Даёт результат 3
.
Реплицировать элемент несколько раз
Тут всё просто. Если в текущей ветви есть элемент, его можно размножить. Например, в 3
раза, выполнив 3^m
:
test^_3^_m_
Получаем:
test
test
test
Дублируем последний элемент
Это укороченная версия репликации. Она всегда добавляет ровно одну копию последнего элемента.
test^_H_
Получаем:
test
test
Отбросить последний элемент
Последний элемент также можно с лёгкостью удалить.
1^_2^_3^_X_
Получаем:
1
2
Поменять местами два последних элемента
Или меняем местами с предыдущим элементом.
1^_2^_3^_G_
Получаем:
1
3
2
Поднимаем пустой элемент
Иногда нам нужна пустая строка, пустой массив или пустая матрица. Нельзя поднимать пустой элемент при помощи ^
, потому что это поднимет все последующие элементы того же ранга, начиная с последнего. Так что есть специальный префикс, поднимающий взявшийся ниоткуда пустой элемент.
A_
Получаем:
branch()
Подсчитать
Эта пара просто возвращает количество подветвей в последнем элементе текущей ветви.
1^_2^_3^_^_$_
Получаем:
3
Опускаем последний элемент
Как и множество подветвей можно поднять в одну ветвь, содержащую их все, так и последний элемент в ветви можно опустить, подтянув всё его содержимое в текущую ветвь.
1^_2^_3^_^_v_
Естественно, получаем:
1
2
3
Кстати, символ является парой графемы «поднять». Разве не здорово?
Арифметика
Я люблю числа с плавающей запятой и написал по ним множество туториалов:
Yet another floating-point tutorial
Estimating floating-point error the easy way
Why is it ok to divide by 0.0?
Я даже написал целую книгу о геометрии для программистов и она состоит наполовину из символьных вычислений, наполовину из числовых, так что не меньше половины книги тоже касается чисел с плавающей запятой.
Но в этом конкретном языке я не хотел добавлять ещё один тип, так что мы выполняем всю арифметику в десятичных строках. Как в Коболе.
Существует одно необычное правило: точность результата — это максимальная точность аргумента. Правило означает, что 1/3
и 1.00/3
дают разные результаты. Первое выражение — 0
, второе — 0.33
. Всё прочее осталось без изменений.
Разумеется, за исключением того, что на этот раз все выражения имеют постфиксную запись, они распространяются на тензоры и вместо операторов у нас пары префикса+операции.
Сложение
Префикс сложения: +
.
2^_2^_+_
4
Если вас заинтересовало, что означает «они распространяются на тензоры», то объясню, что можно прибавлять число к массиву:
1^_2^_^_3^_+_
4
5
Или складывать элементы в парах массивов:
1^_2^_^_3^_4^_^_+_
4
6
Также можно складывать матрицы или даже произвольные деревья при условии, что они имеют одинаковый ранг, размер и конфигурацию. И всё это при помощи одной операции. В этом языке не нужны for
.
Вычитание
Префикс вычитания: -
.
4^_2^_-_
2
Умножение
Так как мы не можем использовать *
для умножения (ведь исходный код берётся из имени файла), префиксом для умножения будет x
.
2^_2^_x_
4
Деление
Аналогично, мы не можем использовать для деления /
. Префиксом для деления будет z
.
4^_2^_z_
4
Логика
Логические операции в этом языке работают практически так же, как и арифметика. Они работают со строками и распространяются на тензоры и деревья. Просто результат преобразования сравнения для пары деревьев будет деревом из 0
и 1
.
Равно?
Этот префикс сравнивает две строки. Строки могут быть также числами.
2^_2^_=_
1
Численно равны?
Это сравнение двух строк. Ожидается, что строки будут нулями, но они могут иметь завершающие нули.
2^_2.00^_=_
1
Меньше?
Ожидает числа. Возвращает 1
, если первое меньше второго. В противном случае 0
:
2^_3^_<_
1
Больше?
Тоже ожидает числа. Возвращает 1
, если первое больше второго. В противном случае 0
:
3^_2^_>_
1
Подстрока?
Ожидает строки. Возвращает 1
, если первая является подстрокой второй, в противном случае 0
:
bob^_notabobbutcontainsone^_(_
1
Надстрока?
Симметрична паре операции «подстрока?». Возвращает 1
, если первая является надстрокой последней, в противном случае 0
:
notabobbutcontainsone^_bob^_)_
1
Строка начинается с?
Ожидает строки. Возвращает 1
, если первая начинается со второй, в противном случае 0
:
bobbutcontainsone^_bob^_\[_
1
Строка заканчивается на?
Ожидает строки. Возвращает 1
, если первая заканчивается на вторую, в противном случае 0
:
notabob^_bob^_\]_
1
Булево not
Ожидает строку только из 0
и 1
. Инвертирует значения в дереве.
1^_T_
0
Булево and
Тоже ожидает только строки из 0
и 1
. Выполняет булеву операцию and (И).
1^_1^_W_
1
Булево or
Как можно догадаться, тоже ожидает только строки из 0
и 1
. Выполняет булеву операцию or (ИЛИ).
0^_1^_M_
1
Строки
По возможности интерпретировать как число, в противном случае 0
В этом языке числа — это строки, которые можно интерпретировать как числа. То есть десять цифр, одна необязательная точка и необязательный знак минуса. Очевидно, что некоторые строки не являются числами, но вам может захотеться, чтобы они ими были. Для такой ситуации существует специальный префикс C
. Он превращает все не-числа в 0
,
123^_not123^_^_C_
123
0
Конкатенировать строки
Очень простая операция, склеивающая две строки.
2^_2^_&_
22
Разбить строку
Разбивает строку по разделителю (тоже строке).
pre,the,post^_,^_E_
pre
the
post
Объединить строки
Соединяет строку обратно в одну строку с разделителем.
pre^_the^_post^_^_-^D_
pre-the-post
Заменить
Я вас обманул! В этом языке нет пары операции «заменить строку строкой». Для выполнения замены нужно использовать идиому «разделить-потом-объединить».
pre,the,post^_,^_E_-^_D_
pre-the-post
Один символ пары я сэкономил для чего-нибудь ещё.
Фильтрация
Пока есть только одна операция фильтрации, а именно:
Фильтровать по логическому значению
Она ожидает два дерева одинаковой конфигурации. Одна со строками, другая тоже со строками, но только из 1
и 0
. Фильтр обходит первое дерево и удаляет каждый элемент, не являющийся 1
в соответствующем дереве.
pre,the,post^_,^_E_H_p^_)_V_
То есть эта программа
поднимает строку «pre, the, post»,
разбивает её по
,
, получая массивpre the post
,дублирует разбитую строку
pre the post
,применяет «является ли надстрокой
p
» к последнему дереву разбиения, что даёт массив1 0 1
.и, наконец, применяет фильтр
1 0 1
к массивуpre the post
, давая в итоге меньший массив со строками, где встречается букваp
:pre post
Файлы
В этом языке в качестве графемы файла используется маленький кружок o
. Есть ровно три префикса операции работы с файлами, и все они содержат маленький кружок.
Загрузить из файла
Графема: b
, как будто файл поступает с медленного накопителя в быструю оперативную память.
somefilei_txt^_b_
Результатом будет то, что находится в файле somefile.txt
. Результат передаётся как строка: ветвь, в которой каждая подветвь — это лист, содержащий значение символа.
Кроме того, эта пара операции по совместительству используется как команда «list directory». Если в качестве аргумента для b
указано имя папки, то результатом будет не строка, а список строк, каждая из которых содержит имя файла или папки.
Сохранить в файл
Графема обратна префиксу «загрузить из файла»: p
. То есть файл спускается вниз, ближе к земле.
somethingL_toL_putL_inL_aL_file^_somefilei_txt^_p_
Результатом будет запись «something to put in a file» в файл somefile.txt
Удалить файл
Эта графема — просто кружок o
. Может показаться, что это не имеет особого смысла, но это показывает, что операция — чистый побочный эффект, никак не влияющий на текущую ветвь. Ну, кроме потребляемого имени файла.
somefilei_txt^_o_
Удаляет файл somefile.txt, если он существует.
Помощь
Последнее — это графема со справочным сообщением, объясняющим, что происходит. (e
). Причина её выбора проста. Когда мы что-то собираем, то получившееся имя файла будет иметь видthe_namingless_programming_language
. Так что когда мы выполняем её в таком виде, первой парой операции, которую встречает интерпретатор, будет e
из the_...
.
Ладно, теперь я всё понял. И мне нравится! Но пока интерпретатор имеет не очень много возможностей. Могу ли я поучаствовать?
Конечно! Чтобы добавить пару операции в язык, нужно сделать три шага.
Изобрести полезную программу, которая не будет работать, если не добавить вашу возможность.
Добавить вашу возможность, чтобы программа запустилась и была полезной.
Подготовить пул-реквест с изменениями в коде и вашей программой, добавленной в список полезных программ.
Список начну я.
Список полезных программ
Спарсить исходный код и перечислить пары операции, которые на данный момент есть в языке
i_^_b_H_i_cpp^_)_V_b_v_J_^_E_H_leafL_==^_)_V_H_Z_Z_^_)_V_H_I_^_E_1^_2^_#_G_Z_Z_^_E_1^_2^_#_H_$_L_-^_G_m_G_&_&_
Эта программа парсит файлы .cpp
в текущей папке, ища в них строки наподобие этой:
} else if(right.branches.back().leaf == 'e') { // help
Затем такие строки обрабатываются для извлечения префикса операции, в данном примере e
, комментария, в данном примере help
.
Затем префиксы и комментарии склеиваются вместе тире -
.
Получившийся в результате массив — это список префиксов операции, поддерживаемых этим языком, с их краткими объяснениями, взятыми из комментариев:
. - exit
U - underscore
Z - slash
N - backslash
J - line break
i - dot
L - space
I - single quote
Y - double quote
^ - elevate all the last elements of the same rank
| - put an element of the current branch on top by index
\# - remove all but the targeted by index element for the selected depth
m - replicate an item multiple times
H - duplicate the last element
X - drop the last element
G - swap the last two elements
A - elevate an empty element
$ - count
v - deelevate last element
\+ - addition
\- - subtraction
x - multiplication
z - division
= - equal?
% - numerically equal?
< - less?
\> - greater?
( - substring?
) - superstring?
\[ - string starts with?
\] - string ends with?
T - Boolean not
W - Boolean and
M - Boolean or
C - interpret as number if possible, 0 otherwise
& - concatenate strings
E - split a string
D - join strings
V - filter by a logical value
b - load from file
p - save to file
o - delete file
e - help