Pull to refresh

Как писать квайны

Reading time 4 min
Views 37K
Введение

Многие программисты считают написание квайнов (программ, выводящих свой исходный код) непосильной задачей. И действительно — все эти цепные квайны и квайны различного порядка, при взгляде на которые можно потеряться в, казалось бы, бессмысленном наборе символов…

Однако, на самом деле, написать квайн на каком-либо языке не так сложно, как кажется. Сейчас я расскажу, как сделать это на различных языках программирования. Более того, мы не будем использовать «хаки» интерпретеруемых языков вроде операции вывода исходного кода и функций типа eval и напишем квайны на интерпретируемых и компилируемых языках.

Теория

Попробуем написать квайн. Для этого возьмём инструкцию языка для вывода и передадим ей как параметр код программы. Однако, в коде мы снова используем этот же код и так далее — возникает бесконечная рекурсия. Но что можно сделать для того, чтобы не передавать строковую константу? Решение — поместить строку (копию части кода) в переменную. Для удобства назовём такую строку s-строкой, а переменную с этой строкой — s-переменной. Чтобы и в s-переменной не было рекурсии, мы просто исключим из неё фрагмент со значением этой самой переменной. То есть, выглядеть это будет примерно так:

C:
char s[]="char s[]=;";

Примечание. Как в этом фрагменте, так и в квайнах, которые получатся в итоге, для простоты мы не будем соблюдать правила форматирования кода. Тем не менее, их соблюдение вы сможете без труда добавить самостоятельно после прочтения статьи.

Далее, при выводе, мы подставим значение s-строки в её же определение в коде (в примере выше — перед тремя последними символами). Здесь же возникает ещё несколько проблем. Первая проблема — при подставлении в s-строке нельзя использовать символы, которые поведут себя в коде не так, как надо. Например, мы не можем так просто вставить кавычку — ведь вместо того, чтобы стать частью s-строки, она завершит её определение и выводимый код не будет совпадать с исходным, являясь некорректным вообще.

Экранирование применить здесь достаточно сложно — символ экранирования надо тоже экранировать и т.д.. Гораздо проще, например, использовать другой вариант кавычек — так, во многих интерпретируемых языках разрешено использование как одинарных, так и двойных кавычек для задания строки, а отличие состоит в том, что можно без проблем использовать одну кавычку в константе, если она ограничивается другими. То есть, код '"' создаст односимвольную строку с двойной кавычкой, а код "'" — с одиночной. Если использовать этот вариант, удобно задать в начале переменную с какой-либо кавычкой, а затем использовать её при выводе.

Но и этот вариант не универсален: в Си, к примеру, есть лишь один вариант кавычек. Тогда можно использовать другой способ — задавать кавычку кодом символа, печатая символ с таким кодом при выводе.

Следующая проблема — вставка другой строки (или символа с каким-либо кодом) в вывод s-строки. Решение здесь очевидно — брать подстроку s-строки специальной функцией, выводить её, далее выводить то, что надо вставить, затем выводить другую подстроку s-строки. Может показаться, что в Си взятие подстроки для вывода потребует немало кода. Тут нам на помощь придёт мощь функции printf. Так, например, вот варианты кода для различных языков, печатающего часть строки s со второго символа (считая с единицы) по четвёртый включительно:

Python:
print(s[1:4])

Ruby:
print s[1..3]

Perl:
print substr(s,1,2)

C:
printf("%.2s",s+1);

Обычно методы взятия подстроки могут также брать её остаток до конца. Например, напечатаем строку s со второго символа до конца строки (то есть, всю строку кроме первого символа):

Python:
print(s[1:])

Ruby:
print s[1..-1]

Perl:
print substr(s,1)

C:
printf("%s",s+1);

Если такой возможности нет, придётся на место параметра с длиной подстроки поставить заглушку типа «XX», а затем в конце посчитать символы до конца и подставить их вместо «XX» в коде и в s-строке, не изменяя длины различных частей кода. Например, если в длине окажется одна цифра, целесообразно подставить вместо первого икса пробел, ведь, если его удалить, длины частей s-строки изменятся и их придётся пересчитывать.

Интерпретируемые языки

Итак, начнём писать квайны, собрав все суждения выше. На Python я написал такой квайн (работает и на 3.x):
q="'";s='q="";s=;print(s[:3]+q+s[3:7]+q+s+q+s[7:])';print(s[:3]+q+s[3:7]+q+s+q+s[7:])

Здесь переменная q используется как переменная, где хранится одинарная кавычка, далее идёт определение s-переменной со всем кодом, кроме самой s-строки. После этого идёт вывод s-переменной со следующими вставками:
1). Одинарная кавычка как значение переменной q;
2). Одинарная кавычка как начало определения s-строки;
3). Сама s-строка (да-да, s-строка вставляется внутри s-строки);
4). Одинарная кавычка как конец определения s-строки.

Примечание. При написании квайнов по данному методу не забывайте копировать все изменения в коде в копию кода в s-строке.

С минимальными изменениями можно получить квайн только для Python 2.x:
q="'";s='q="";s=;print s[:3]+q+s[3:7]+q+s+q+s[7:]';print s[:3]+q+s[3:7]+q+s+q+s[7:]

Абсолютно аналогичны и квайны на других языках, где мы изменяем лишь некоторые синтаксические особенности:

Ruby:
q="'";s='q="";s=;print s[0..2]+q+s[3..6]+q+s+q+s[7..-1]';print s[0..2]+q+s[3..6]+q+s+q+s[7..-1]

Perl:
$q="'";$s='$q="";$s=;print substr($s,0,4).$q.substr($s,4,5).$q.$s.$q.substr($s,9)';print substr($s,0,4).$q.substr($s,4,5).$q.$s.$q.substr($s,9)

PHP:
<?$q="'";$s='<?$q="";$s=;print substr($s,0,6).$q.substr($s,6,5).$q.$s.$q.substr($s,11);';print substr($s,0,6).$q.substr($s,6,5).$q.$s.$q.substr($s,11);

Компилируемые языки.

Написание квайна на C оказалось чуть более трудной задачей. Здесь я активно использовал коды символов: двойной кавычки — 34, и перевода строки — 13 (он понадобился, чтобы отделить директиву компилятора для включения stdio.h), а также интересный способ взятия подстроки с помощью printf, уже описанный выше.

А вот и сам квайн:
#include <stdio.h>
int main(){const char *s="#include <stdio.h>int main(){const char *s=;printf(%.18s%c%.25s%c%s%c%.8s%c%.33s%c%s,s,10,s+18,34,s,34,s+43,34,s+51,34,s+84);return 0;}";printf("%.18s%c%.25s%c%s%c%.8s%c%.33s%c%s",s,10,s+18,34,s,34,s+43,34,s+51,34,s+84);return 0;}

Заключение

Вот и всё. Я написал квайны на большинстве языков, интерпретаторы и компиляторы которых обнаружил на своём компьютере. Думаю, теперь вы и сами напишете подобную программу на своём любимом языке программирования, если я не упомянул его здесь. В качестве упражнения вы также можете написать квайн на таких языках, как Java, C#, Haskell или Pascal. Не бойтесь трудностей — достаточно попробовать, и всё получится!
Tags:
Hubs:
+76
Comments 56
Comments Comments 56

Articles