Безымянный язык программирования без присваивания имён
- четверг, 29 февраля 2024 г. в 00:00:25

Давать имена сложно. Давайте посмотрим, как далеко мы можем зайти без них.
Это язык программирования, основанный на трёх парадигмах:
Основная «фишка» языка — избегание любых наименований. Оставаясь верным этой максиме, сам язык тоже не имеет названия. «Язык программирования без имён» (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-последовательность для символа подчёркивания — это 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Ожидает строку только из 0 и 1. Инвертирует значения в дереве.
1^_T_
0Тоже ожидает только строки из 0 и 1. Выполняет булеву операцию and (И).
1^_1^_W_
1Как можно догадаться, тоже ожидает только строки из 0 и 1. Выполняет булеву операцию or (ИЛИ).
0^_1^_M_
1В этом языке числа — это строки, которые можно интерпретировать как числа. То есть десять цифр, одна необязательная точка и необязательный знак минуса. Очевидно, что некоторые строки не являются числами, но вам может захотеться, чтобы они ими были. Для такой ситуации существует специальный префикс 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