[Перевод] Безымянный язык программирования без присваивания имён

8fd85cb460736dfd9d730414cfd0a8ae.png

Давать имена сложно. Давайте посмотрим, как далеко мы можем зайти без них.

Что это?

Это язык программирования, основанный на трёх парадигмах:

Основная «фишка» языка — избегание любых наименований. Оставаясь верным этой максиме, сам язык тоже не имеет названия. «Язык программирования без имён» (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 лист, или ветвь, содержащая динамический массив структур данных. Массив может быть и пустым. Пустая ветвь — всё равно ветвь.

Строка — это ветвь, в которой каждая подветвь — это лист.

Число — это строка. Именно так. Язык поддерживает десятичную арифметику со строками. Привет, Кобол!

Массив — это ветвь, в которой каждая подветвь — это строка.

Матрица — это ветвь, в которой каждая подветвь — массив.

Стоит отметить, что ни один из этих терминов не является названиями чего-то специфичного для этого языка. Это широко известные структуры данных, которые можно реализовать в структуре данных языка.

Операции

В языке есть и операция. Не «операции», а «операция», потому что в языке она только одна. Так как она только одна, название ей тоже не нужно.

Операция выглядит как _ , она берёт последний элемент из ветви перед ней и действует соответствующим образом. То есть, по сути, семантика операции задаётся парой, состоящей из префикса (символа перед операцией) и самой операции.

Например, чтобы сложить два и два, нужно:

  1. Поместить char 2 в виде строки. Для этого используется пара ^ и _.

    2^_

  2. Затем нужна ещё одна 2 в виде строки. Можно сделать ещё раз 2^_, но можно и дублировать последний элемент в текущей ветви парой H и _.

    H_

  3. Далее нужно выполнить само сложение. Это сделать легко, + и _.

    +_

То есть готовая программа будет выглядеть так:

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_

То есть эта программа

  1. поднимает строку «pre, the, post»,

  2. разбивает её по ,, получая массив pre the post,

  3. дублирует разбитую строку pre the post,

  4. применяет «является ли надстрокой p» к последнему дереву разбиения, что даёт массив 1 0 1.

  5. и, наконец, применяет фильтр 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_....

Ладно, теперь я всё понял. И мне нравится! Но пока интерпретатор имеет не очень много возможностей. Могу ли я поучаствовать?

Конечно! Чтобы добавить пару операции в язык, нужно сделать три шага.

  1. Изобрести полезную программу, которая не будет работать, если не добавить вашу возможность.

  2. Добавить вашу возможность, чтобы программа запустилась и была полезной.

  3. Подготовить пул-реквест с изменениями в коде и вашей программой, добавленной в список полезных программ.

Список начну я.

Список полезных программ

Спарсить исходный код и перечислить пары операции, которые на данный момент есть в языке

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

© Habrahabr.ru