jak napisać kłajna

To jest tekst o tym, jak napisać kłajn (https://en.wikipedia.org/wiki/Quine_%28computing%29), czyli program, który wypisuje swoje źródło bez korzystania z introspekcji. Zakładam, że czytelnik już kiedyś spotkał się z kłajnami, może nawet próbował napisać kłajna i czuje, dlaczego to jest trudne i ciekawe. Tekst składa się z dwu części: w pierwszej mówię przez przypowieść, na jakiej zasadzie to będzie działać, w drugiej opisuję szczegółowo, jak napisać kłajna.

jak będzie działać kłajn

Teraz opowiem, jak dałoby się zrobić maszynę, która umie zbudować własną kopię. Oczywiście najprościej byłoby, żeby maszyna obejrzała samą siebie, zobaczyła, jak jest zbudowana, i zbudowała takie samo coś. Ale czy dałoby się zbudować maszynę, która umiałaby zbudować własną kopię bez introspekcji?

Najpierw zbudowałbym maszynę, która umie zbudować dowolną rzecz, jeśli tylko ma kartkę z jej planem. Mozna by powiedzieć, że taka maszyna to — dopóki nie włoży się w nią kartki z planem — żadna maszyna, bo jak ją włączyć, to nic nie zrobi, ale to nic. I tak jest fajna, bo wystarczy włożyć w nią kartkę z planem kajaka i będziemy mieli maszynę budującą kajak, wystarczy włożyć w nią kartkę z planem taczek i będziemy mieli maszynę budującą taczki itp. To może włożyć w nią kartkę z jej własnym planem i dostaniemy maszynę budującą własną kopię? Jest jeden problem: co będzie narysowane na tym planie w miejscu, gdzie jest narysowany zasobnik na plan? Czy ten zasobnik będzie pusty? Jeśli tak, to maszyna zbudowana według tego planu nie będzie umiała nic zbudować (więc nie będzie tak naprawdę kopią swojego rodzica — bo rodzic umiał zbudować maszynę potomną, a ona nie umie). A może na tym planie w miejscu, gdzie jest narysowany zasobnik na plan będzie narysowany dokładniutko plan tej maszyny? Już lepiej, tylko co powinno być narysowane na tym planie narysowanym na planie w miejscu, gdzie jest narysowany rysunek zasobnika na plan? Niestety, są dwie możliwości: albo na którymś z planów zasobnik będzie pusty — co oznacza, że maszyna nie zbuduje swojej idealnej kopii — albo nigdy nie skończymy rysować tego planu.

Zachęcam: zatrzymaj się tu na chwilę. Powyobrażaj sobie to wszystko, aż poczujesz się z tym swobodnie. Wyobraź sobie maszynę, która umie zrobić dowolną rzecz, jeśli tylko w jej zasobnik na plan jest włożony plan tej rzeczy. Wyobraź sobie, jak wkładamy w ten zasobnik plan biurka (samochodu, patelni, słuchawek, telefonu), wciskamy przycisk, a ona to robi. Teraz wyobraźmy sobie, że rysujemy plan tej maszyny, wkładamy ten plan w zasobnik na plan i uruchamiamy maszynę. Z maszyny-rodzica wyskakuje maszyna dziecko, uruchamiamy maszynę-dziecko — i co się wtedy stanie? A teraz kolejny eksperyment myślowy: rysujemy plan tej maszyny, ale tym razem na planie, na miejscu, gdzie jest narysowany zasobnik na plan, rysujemy plan biurka, po czym uruchamiamy maszynę, wyskakuje z niej maszyna-dziecko, uruchamiamy tę maszynę-dziecko — i co się teraz stanie? A teraz kolejny eksperyment myślowy: rysujemy plan tej maszyny, ale tym razem na planie, na miejscu, gdzie jest narysowany zasobnik na plan, rysujemy plan tej maszyny, z pustym zbiornikiem na plan. Po czym uruchamiamy maszynę, wyskakuje z niej maszyna-dziecko, uruchamiamy tę maszynę-dziecko... czy widzisz oczyma wyobraźni, co się dzieje?

A można przerobić tę maszynę, żeby działała trochę inaczej: żeby produkowała to, co jest narysowane na planie, a potem żeby kserowała kartkę z planem, na wyprodukowanej rzeczy znajdowała miejsce oznaczone napisem "tutaj" i w to miejsce włożyła tę kserówkę. A na kartce narysujemy dokładny plan tej maszyny, ale uwaga: z pustym zasobnikiem, na którym jest napisane "tutaj". I teraz patrz, co się stanie, kiedy włączymy maszynę: zbuduje swoją kopię ale niewyposażoną w żaden plan, skseruje plan i włoży go w zasobnik. I w ten sposób stworzyła swoją dokładną kopię.

Jeśli w tym momencie masz poczucie, że oszukuję, bo zrobienie kopii planu jest rodzajem introspekcji, jeśli masz do mnie żal, że nie powiedziałem dokładnie, jakie działania są niedozwoloną introspekcją, a jakie — na przykład kserowanie planu — nie, to odpędź od siebie te myśli. Ta cała opowieść o maszynie nie jest samodzielną zagadką. Ona nawet nie musi się idealnie trzymać kupy. Ona służy tylko temu, żebyś zaraz lepiej zrozumiał, dlaczego kłajn, który napiszę za chwilę, jest zbudowany tak, jak jest zbudowany, i jak to się dzieje, że on działa. Mi porównanie tego kłajna (tego, który za chwilę napiszę) do takiej maszyny dużo pomogło, dlatego i tobie o tej maszynie opowiadam.

Zwróćmy uwagę na jeden szczegół, który w przypadku maszyny jest mało istotny, ale będzie miał znaczenie, kiedy już naprawdę będę pisał kłajna. Ta maszyna po wyprodukowaniu maszyny nie może jej od razu oddać osobie, która nacisnęła przycisk — musi jeszcze przez chwilę ją potrzymać w rękach, żeby włożyć do niej kopię planu.

podsumowanie jak działa ta maszyna

A teraz uwaga, podsumowanie, jak działa ta maszyna:

  1. ktoś wciska przycisk, maszyna rusza
  2. na podstawie planu produkuje maszynę umiejącą produkować dowolną rzecz na podstawie planu
  3. robi kopię planu
  4. wkłada tę kopię planu w zrobioną przez siebie maszynę
  5. oddaje zrobioną przez siebie maszynę wyposażoną w plan temu, kto wcisnął przycisk

Zachęcam: zatrzymaj się tu na chwilę. Powyobrażaj sobie to wszystko, aż poczujesz się z tym swobodnie. Wyobrażaj sobie dokładnie, po kolei, porządnie, wizualnie, jak ta maszyna działa, jak produkuje maszynę-dziecko, jak kseruje plan, jak wkłada kserówkę w maszynę dziecko, jak uruchamiamy maszynę-dziecko, co robi ta maszyna-dziecko, jak potem uruchamiamy maszynę-wnuczka... i tak dalej.

piszę kłajna

łatwy kłajn w Pythonie

A teraz napiszę kłajna — czyli program wyświetlający sam siebie (bez korzystania z introspekcji). Najpierw napiszę prostego kłajna w Pythonie, żebyś pokazać, jak się pisze kłajna, a potem jeszcze napiszę kilka wariacji i w Pythonie, i w innych językach, żeby pokazać, jakie mogą się pojawić problemy i jak sobie z nimi poradzić.

Najpierw zauważmy, że program wyświetlający sam siebie to szczególny przypadek programu, który wyświetla coś. Więc zacznę od napisania programu, który wyświetla coś. W Pythonie najprościej zrobić to tak:

print()

To jest program, który może wyświetlić cokolwiek — wystarczy to cokolwiek wstawić w nawiasy. Póki się tam niczego nie wstawi, ten program jest do niczego (jak maszyna bez planu), ale kiedy na przykład wstawię w te nawiasy słowo Polska, otrzymam program wyświetlający słowo Polska:

print('Polska')

Następnie zauważmy, że program wyświetlający sam siebie to program, który wyświetla program, który wyświetla sam siebie. A to z kolei jest szczególny przypadek programu, który wyświetla program, który wyświetla coś. Więc napiszmy program, który wyświetla program, który wyświetla coś. W Pythonie najprościej zrobić to tak:

print('print()')

To jest program, który tworzy program, który może wyświetlić cokolwiek — wystarczy to cokolwiek wstawić w nawiasy. Jest on jak maszyna, która produkuje maszynę, która może wyprodukować cokolwiek, byle dać jej plan. Póki w te nawiasy się niczego nie wstawi, ten program jest do niczego (jak maszyna produkująca maszynę nie wyposażoną w plan), ale kiedy na przykład wstawię w te nawiasy słowo Polska, otrzymam program wyświetlający program wyświetlający słowo Polska:

print('print("Polska")')

Jeszcze raz porównajmy to sobie z maszynami, o których pisałem w pierwszej części. Program print() jest jak maszyna, która może zrobić cokolwiek, byle dać jej plan tego cokolwiek. Napis 'print()' jest jak plan tej maszyny. Program print('print()') jest jak maszyna umiejąca zrobić cokolwiek, jeśli dać jej plan, która w zasobniku na plan ma włożony plan maszyny, która zrobi cokolwiek, jeśli dać jej plan, która zasobnik na plan ma pusty. A program print('print("Polska")') jest jak maszyna umiejąca zrobić cokolwiek, jeśli dać jej plan, która w zasobniku na plan ma włożony plan maszyny, która zrobi cokolwiek, jeśli dać jej plan, która w zasobniku na plan ma włożony plan słowa Polska. Zauważmy, że to nie jest program wypisujący swoje źródło — choć jest to program wypisujący źródło programu podobnego do siebie, więc jesteśmy całkiem blisko.

Teraz musimy zrobić manewr podobny, jak w pierwszej części robiliśmy z maszynami. Pamiętasz, jak w planie maszyny-dziecka na zasobniku na plan napisałem tutaj? No to teraz zrobię podobnie. Patrz:

print('print(tutaj)')

Czy pamiętasz, jak przerabiałem maszynę, żeby przed zwróceniem maszyny-dziecka w jej zasobnik wkładała kopię planu, na podstawie którego została zrobiona? To teraz zrobię podobnie: przerobię ten program, żeby po stworzeniu napisu ale przed wyświetleniem go w miejsce oznaczone słowem tutaj wkładała kopię tego napisu. Na razie niełatwo to zrobić, bo stworzenie napisu i wyświetlenie go jest w jednym poleceniu, więc nie ma kiedy zrobić tej podmianki. No to rozdzielę w tym programie stworzenie napisu od jego wyświetlenia:

napis='print(tutaj)'; print(napis)

Ale zaraz — skoro zmieniłem program, to powinienem zmienić też literał napisu ze źródłem tego programu, żeby rodzic tworzył dziecko podobne do siebie (rozumiesz? w analogii z maszyną powiedziałbym: kiedy zmieniamy maszynę, musimy też zmienić plan maszyny, żeby maszyna-rodzic tworzyła maszynę-dziecko podobne do siebie):

napis='napis=tutaj; print(napis)'; print(napis)

Jak widzisz, zmieniony fragment oznaczyłem na niebiesko. W dalszej części artykułu też będę tak czasem robił.

Teraz dodaję do programu polecenie, które przed wyświetleniem napisu w miejsce słowa tutaj wstawia kopię tego napisu:

napis='napis=tutaj; print(napis)'; napis=napis[:6] + napis + napis[11:]; print(napis)

Zauważmy, że tego podmienienia nie robię przez znalezienie napisu tutaj (czyli nie sposobem znajdź i zastąp, nie przez Pythonową metodę replace), ale przez ciachnięcie tego napisu w zadanym miejscu. Robię tak, bo tak będzie najłatwiej. Potem pokażę ci, co by było, gdybym próbował zrobić to inaczej.

Uruchamiam ten program i patrzę, czy dobrze działa. Oto wynik jego działania:

napis=napis=tutaj; print(napis); print(napis)

Nieźle, ale są jeszcze dwie sprawy do poprawienia. Po pierwsze napis wstawiany w miejsce słowa tutaj ma być wzięty w cudzysłowy. Jest do tego w Pythonie gotowa wbudowana funkcja, repr. No więc jej używam:

napis='napis=tutaj; print(napis)'; napis=napis[:6] + repr(napis) + napis[11:]; print(napis)

Uruchamiam ten program i patrzę, czy dobrze działa. Widzę, że teraz jego wynik wygląda tak:

napis='napis=tutaj; print(napis)'; print(napis)

Dobrze, pojawiły się cudzysłowy. Teraz druga sprawa. Skoro zmieniłem program (bo dodałem kod wstawiający co trzeba w miejsce napisu tutaj), to trzeba zmienić też literał napisu ze źródłem programu. Więc zmieniam program na taki:

napis='napis=tutaj; napis=napis[:6] + repr(napis) + napis[11:]; print(napis)'; napis=napis[:6] + repr(napis) + napis[11:]; print(napis)

Uruchamiam ten program i patrzę, czy dobrze działa. Widzę, że jego wynik jest taki:

napis='napis=tutaj; napis=napis[:6] + repr(napis) + napis[11:]; print(napis)'; napis=napis[:6] + repr(napis) + napis[11:]; print(napis)

Sukces! Kłajn działa!

Kiedy myślisz o tym, jak działa ten kłajn, warto porównywać go sobie z maszyną do robienia maszyn, o której pisałem w pierwszej części. Jest przy tym tylko jedna trudność: w przypadku maszyn różnica między maszyną a planem maszyny jest gruba. W przypadku programów różnica między napisem a jego literałem jest subtelna.

podsumowanie łatwego kłajna w Pythonie

No to jeszcze raz podsumuję w trzech krokach, jak pisałem tego prostego kłajna w Pythonie — bo według tych samych kroków można też pisać kłajny w innych językach:

  1. napisałem program, który tworzy i wypisuje źródło programu, który wypisuje coś
  2. między poleceniem tworzącym napis a poleceniem wypisującym napis dopisałem kod, który w odpowiednim miejscu napisu umieszcza jego literał
  3. ponieważ zmieniłem program, zmieniłem też literał napisu ze źródłem tego programu

a gdyby w tym kłajnie pythonowym użyć znajdź i zastąp

Czy pamiętasz ten moment, kiedy pisałem prostego kłajna w Pythonie i musiałem w napis wstawić jego literał? Mam na myśli moment, kiedy miałem program taki:
napis='napis=tutaj; print(napis)'; print(napis)

I zmieniłem go na taki:

napis='napis=tutaj; print(napis)'; napis=napis[:6] + napis + napis[11:]; print(napis)

Można by pomyśleć, że zamiast ciachać napis na dwie części — tę do szóstego znaku i tę od jedenastego znaku — i wstawiać co trzeba między nie, prościej byłob zrobić znajdź i zamień, czyli użyć metody replace. No to spróbuję. Dodaję zarówno do programu jak i do literału źródła programu polecenie robiące podmianę:

napis='napis=tutaj; napis=napis.replace(\'tutaj\', napis); print(napis)'; napis=napis.replace('tutaj', napis); print(napis)

Uruchamiam ten program i widzę, że coś poszło nie tak. Efekt jego uruchomienia wygląda tak:

napis=napis=tutaj; napis=napis.replace('tutaj', napis); print(napis); napis=napis.replace('napis=tutaj; napis=napis.replace('tutaj', napis); print(napis)', napis); print(napis)

Program nie zadziałał jak trzeba, bo w literale napisu słowo tutaj występuje dwa razy: raz tam, gdzie jest nim oznaczone miejsce, w które ma być wstawione co trzeba, drugi raz tam, gdzie w poleceniu robiącym znajdź i zastąp mówimy, jak wygląda fragment napisu, którego szukamy. Podmienieniu powinno podlec tylko to pierwsze wystąpienie słowa tutaj, a to drugie nie. Ten problem można rozwiązać na kilka sposobów. Można na przykład w poleceniu robiącym znajdź i zastąp spróbować powiedzieć tutaj nie używając słowa tutaj. Na przykład tak:

napis='napis=tutaj; napis=napis.replace(\'TUTAJ\'.lower(), napis); print(napis)'; napis=napis.replace('TUTAJ'.lower(), napis); print(napis)

lub tak:

napis='napis=tutaj; napis=napis.replace(\'jatut\'[::-1], napis); print(napis)'; napis=napis.replace('jatut'[::-1], napis); print(napis)

Uruchamiam ten program, żeby sprawdzić, jak działa. Widzę taki efekt:

napis=napis=tutaj; napis=napis.replace('jatut'[::-1], napis); print(napis); napis=napis.replace('jatut'[::-1], napis); print(napis)

Nieźle. Trzeba jeszcze tylko wstawiany napis zmienić w literał napisu: otoczyć cudzysłowami a występujące w nim znaki specjalne (na przykład cudzysłowy przy 'jatut') wyeskejpować. Na szczęście w Pythonie mamy do tego gotową wbudowaną funkcję repr (ale pomyśl, ile byłoby z tym dłubania, gdybyśmy robili to na przykład Javaskrypcie i takiej funkcji byśmy nie mieli):

napis='napis=tutaj; napis=napis.replace(\'jatut\'[::-1], repr(napis)); print(napis)'; napis=napis.replace('jatut'[::-1], repr(napis)); print(napis)

Uruchamiam ten program. Widzę taki efekt:

napis="napis=tutaj; napis=napis.replace('jatut'[::-1], repr(napis)); print(napis)"; napis=napis.replace('jatut'[::-1], repr(napis)); print(napis)

No, prawie dobrze. Prawie to samo wyszło. Tylko kiedy funkcja repr zmieniała napis ze źródłem programu w literał napisu ze źródłem programu inaczej niż my podeszła do problemu wyeskejpowania pojedyńczych cudzysłowów przy 'jatut': zamiast je eskejpować, napisy otoczyła cudzysłowami niepojedynczymi. Jeśli chcę, żeby wynik programu był naprawdę taki sam, jak jego źródło (a nie tylko taki sam z dokładnością do cudzysłowów), muszę albo zmusić funkcję repr żeby eskejpowała tak jak ja, albo mogę ja przerobić program eskejpując tak, jak robi to repr. Łatwiej zrobić to drugie. Hm, w sumie bardzo łatwo. Nie muszę nic robić: ten powyższy napis, który wypisał mój program, już zawiera cudzysłowy wyeskejpowane tak, jak robi to repr. To on jest kłajnem, który chciałem napisać.

Więc kłajn używający znajdź i zastąp wygląda tak:

napis="napis=tutaj; napis=napis.replace('jatut'[::-1], repr(napis)); print(napis)"; napis=napis.replace('jatut'[::-1], repr(napis)); print(napis)

Kiedy go uruchomię, dostanę taki efekt:

napis="napis=tutaj; napis=napis.replace('jatut'[::-1], repr(napis)); print(napis)"; napis=napis.replace('jatut'[::-1], repr(napis)); print(napis)

a gdybyśmy nie mieli repr

A teraz jeszcze jedna wariacja: napiszę w Pythonie kłajna używającego znajdź i zastąp bez użycia ułatwiającej wiele funkcji repr. Zacznę od momentu, kiedy miałem taki program:

napis='napis=tutaj; napis=napis.replace(\'jatut\'[::-1], napis); print(napis)'; napis=napis.replace('jatut'[::-1], napis); print(napis)

Jak może pamiętasz, jego wynik wyglądał tak:

napis=napis=tutaj; napis=napis.replace('jatut'[::-1], napis); print(napis); napis=napis.replace('jatut'[::-1], napis); print(napis)

Jak widzisz i może pamiętasz, jest to już prawie kłajn. Jeszcze tylko trzeba zrobić, żeby napis wstawiany w miejsce słowa tutaj był zmieniany w literał napisu: to znaczy trzeba otoczyć go cudzysłowem, a wszystkie występujące w nim znaki specjalne wyeskejpować.

Zacznę od otoczenia go cudzysłowami:

napis='napis=tutaj; napis=napis.replace(\'jatut\'[::-1], napis); print(napis)'; napis=napis.replace('jatut'[::-1], '\'' + napis + '\''); print(napis)

Ponieważ dopisałem do programu kawałek kodu, muszę taki sam kawałek kodu dopisać do literału napisu ze źródłem programu. Tylko ponieważ dopiszę ten kawałek w literale, muszę wyeskejpować w nim cudzysłowy i ukośniki. Takie coś:

napis='napis=tutaj; napis=napis.replace(\'jatut\'[::-1], \'\\\'\' + napis + \'\\\'\'); print(napis)'; napis=napis.replace('jatut'[::-1], '\'' + napis + '\''); print(napis)

No dobra. Otoczyłem wstawiany napis cudzysłowem, a teraz wyeskejpuję wszystkie występujące w nim cudzysłowy:

napis='napis=tutaj; napis=napis.replace(\'jatut\'[::-1], \'\\\'\' + napis + \'\\\'\'); print(napis)'; napis=napis.replace('jatut'[::-1], '\'' + napis.replace('\'', '\\\'') + '\''); print(napis)

Ponieważ dopisałem do programu kawałek kodu, muszę taki sam kawałek kodu dopisać do literału napisu ze źródłem programu. Tylko ponieważ dopiszę ten kawałek w literale, muszę wyeskejpować w nim cudzysłowy i ukośniki. Takie coś:

napis='napis=tutaj; napis=napis.replace(\'jatut\'[::-1], \'\\\'\' + napis.replace(\'\\\'\', \'\\\\\\\'\') + \'\\\'\'); print(napis)'; napis=napis.replace('jatut'[::-1], '\'' + napis.replace('\'', '\\\'') + '\''); print(napis)

No dobra. Otoczyłem wstawiany napis cudzysłowem, wyeskejpowałem wszystkie występujące w nim cudzysłowy, teraz wyeskejpuję wszystkie występujące w nim ukośniki:

napis='napis=tutaj; napis=napis.replace(\'jatut\'[::-1], \'\\\'\' + napis.replace(\'\\\'\', \'\\\\\\\'\') + \'\\\'\'); print(napis)'; napis=napis.replace('jatut'[::-1], '\'' + napis.replace('\'', '\\\'').replace('\\', '\\\\') + '\''); print(napis)

Ponieważ dopisałem do programu kawałek kodu, muszę taki sam kawałek kodu dopisać do literału napisu ze źródłem programu. Tylko ponieważ dopiszę ten kawałek w literale, muszę wyeskejpować w nim cudzysłowy i ukośniki. Takie coś:

napis='napis=tutaj; napis=napis.replace(\'jatut\'[::-1], \'\\\'\' + napis.replace(\'\\\'\', \'\\\\\\\'\').replace(\'\\\\\', \'\\\\\\\\\') + \'\\\'\'); print(napis)'; napis=napis.replace('jatut'[::-1], '\'' + napis.replace('\'', '\\\'').replace('\\', '\\\\') + '\''); print(napis)

No dobra. To powinien być gotowy kłajn. Sprawdzę, czy działa. Uruchamiam go i widzę, że efekt jest taki:

napis='napis=tutaj; napis=napis.replace(\\'jatut\\'[::-1], \\'\\\\'\\' + napis.replace(\\'\\\\'\\', \\'\\\\\\\\'\\').replace(\\'\\\\\\', \\'\\\\\\\\\\') + \\'\\\\'\\'); print(napis)'; napis=napis.replace('jatut'[::-1], '\'' + napis.replace('\'', '\\\'').replace('\\', '\\\\') + '\''); print(napis)

Przyjrzyj się dokładnie. Widzisz, że nie wyszło to, co miało wyjść? Widzisz, że wynik tego programu jest inny niż źródło tego programu? Widzisz, że to nawet nie jest poprawny składniowo kod w Pythonie? Coś poszło nie tak.

Jeśli jesteś twardy, usiądź teraz i zastanów się, gdzie popełniliśmy błąd i dlaczego ten kłajn nie działa. Spróbuj naprawić go sam.

W tym kłajnie jest jedna rzecz zrobiona źle. Kiedy eskejpujemy napis, trzeba najpierw wyeskejpować ukośniki, a dopiero potem wyeskejpować cudzysłowy. Muszę więc zamienić miejscami dwie metody replace, w taki sposób:

napis='napis=tutaj; napis=napis.replace(\'jatut\'[::-1], \'\\\'\' + napis.replace(\'\\\\\', \'\\\\\\\\\').replace(\'\\\'\', \'\\\\\\\'\') + \'\\\'\'); print(napis)'; napis=napis.replace('jatut'[::-1], '\'' + napis.replace('\\', '\\\\').replace('\'', '\\\'') + '\''); print(napis)

Próbuję teraz uruchomić ten program. Widzę, że jego wynik wygląda tak:

napis='napis=tutaj; napis=napis.replace(\'jatut\'[::-1], \'\\\'\' + napis.replace(\'\\\\\', \'\\\\\\\\\').replace(\'\\\'\', \'\\\\\\\'\') + \'\\\'\'); print(napis)'; napis=napis.replace('jatut'[::-1], '\'' + napis.replace('\\', '\\\\').replace('\'', '\\\'') + '\''); print(napis)

Uf, działa. Ale nie było łatwo, prawda? Dlatego zapamiętaj moją radę: kiedy piszesz kłajna, unikaj używania cudzysłowów. Jak zaczniesz je eskejpować, to nigdy nie wyjdziesz na sanki. W tym momencie przerywam pisanie tekstu i wychodzę na sanki.

kłajn w javaskrypcie

A teraz napiszę kłajna w Javaskrypcie. Tym razem będę tłumaczył mniej niż tłumaczyłem pisząc w Pythonie — bo zakładam, że już rozumiesz, jak działają kłajny.

Piszę program, który wyświetla program, który wyświetla cokolwiek:

var napis='var napis=tutaj; alert(napis)'; alert(napis)

Między stworzeniem napisu a jego wyświetleniem w miejsce słowa tutaj wstawiam ten napis:

var napis='var napis=tutaj; alert(napis)'; napis = napis.substring(0, 10) + napis + napis.substring(15, napis.length); alert(napis)

Otaczam wstawiany napis cudzysłowami, ale bez użycia cudzysłowów (żeby uniknąć eskejpowania):

var napis='var napis=tutaj; alert(napis)'; napis = napis.substring(0, 10) + String.fromCharCode(39) + napis + String.fromCharCode(39) + napis.substring(15, napis.length); alert(napis)

Ponieważ dopisałem trochę kodu do programu, dopisuję ten sam kod do literału napisu ze źródłem programu:

var napis='var napis=tutaj; napis = napis.substring(0, 10) + String.fromCharCode(39) + napis + String.fromCharCode(39) + napis.substring(15, napis.length); alert(napis)'; napis = napis.substring(0, 10) + String.fromCharCode(39) + napis + String.fromCharCode(39) + napis.substring(15, napis.length); alert(napis)

I już, gotowe. To jest gotowy kłajn.

kłajn w Excelu

No dobra, a jak by napisać kłajna w Excelu (albo w jakimś innym arkuszu kalkulacyjnym)? Nie będzie łatwo, bo nie da się umieścić napisu w zmiennej, żeby potem go użyć, ale się da. Zobaczcie.

To, o czym będę pisał teraz, wygodnie będzie mi zacząć od prostego pythonowego kłajna, trochę — ale niewiele — różnego od tych, które dotąd omówiłem. Będzie to kłajn, który będzie wstawiał w napis literał tego napisu przez znajdź i zastąp (czyli pythonową metodą replace), ale bez użycia cudzysłowów (żeby uniknąć koszmaru eskejpowania).

Oto wzór programu, który wypisuje jakiś napis:

napis=tutaj; print(napis)

Oto program, który wypisuje wzór programu, który wypisuje jakiś napis:

napis='napis=tutaj; print(napis)'; print(napis)

Można też powiedzieć, że jest to wzór programu, który wypisuje program, który wypisuje jakiś napis — na jedno wychodzi.

Teraz zechcę przerobić ten zewnętrzny program, żeby przed wyświetleniem napisu wykonał na nim znajdź i zastąp — w miejsce placeholdera tutaj żeby wstawił literał tego napisu. Ale, jak pamiętamy, warto to zrobić tak, żeby w tym poleceniu robiącym znajdź i zastąp nie używać cudzysłowów. Tylko jak zapisać słowo tutaj bez użycia cudzysłowów? A może by zamiast słowa tutaj użyć jakiegoś innego placeholdera, takiego, który łatwo zapisać bez użycia cudzysłowów? Na przykład znaczka #, który można zapisać jako chr(35)? No to piszę tak:

napis='napis=#; print(napis)'; napis=napis.replace(chr(35), repr(napis)); print(napis)

Skoro zmieniłem program, to muszę też zmienić literał napisu ze źródłem programu:

napis='napis=#; napis=napis.replace(chr(35), repr(napis)); print(napis)'; napis=napis.replace(chr(35), repr(napis)); print(napis)

I już mam kłajna.

Dobrze. To teraz na przykładzie tego kłajna chcę opowiedzieć inny od dotychczasowego przepis na zrobienie kłajna. Taki trochę inny, trochę nieinny. Inny bo będzie się składał z innych kroków, a nieinny, bo będzie prowadził do tego samego celu, dawał takiego samego kłajna. Oto ten przepis:

Napisz wzór programu, który bierze jakiś napis, robi w nim znajdź i zastąp w miejsce placeholdera wstawiając literał tego napisu, po czym wypisuje wynik:

napis=tubędziejakiśnapis; napis=napis.replace(chr(35), repr(napis)); print(napis)

Zapamiętaj sobie ten wzór programu. Będę mówił o nim ten wzór programu.

Napisz sobie gdzieś z boku literał źródła tego wzoru programu:

'napis=tubędziejakiśnapis; napis=napis.replace(chr(35), repr(napis)); print(napis)'

Teraz w tym literale w miejscu, w którym ma być napis, na którym ma być dokonana operacja, umieść placeholdera:

'napis=#; napis=napis.replace(chr(35), repr(napis)); print(napis)'

Zapamiętaj sobie ten literał. Będę mówił o nim ten literał.

A teraz weź ten wzór programu i w miejsce na napis wstaw w nim ten literał (w poniższym kodzie na niebiesko zaznaczyłem wstawiany literał, żebyś łatwiej zobaczył, co tu się dzieje):

napis='napis=#; napis=napis.replace(chr(35), repr(napis)); print(napis)'; napis=napis.replace(chr(35), repr(napis)); print(napis)

I to właśnie jest kłajn, który mieliśmy napisać.

Teraz podsumuję ten nowy przepis na pisanie kłajna:

  1. Napisz wzór programu, który bierze jakiś napis, robi w nim znajdź i zastąp w miejsce placeholdera wstawiając literał tego napisu, po czym wypisuje wynik. Zapamiętaj sobie ten wzór programu. Będę mówił o nim ten wzór programu.
  2. Napisz sobie gdzieś z boku literał źródła tego wzoru programu.
  3. Teraz w tym literale w miejscu, w którym ma być napis, na którym ma być dokonana operacja, umieść placeholdera. Zapamiętaj sobie ten literał. Będę mówił o nim ten literał.
  4. A teraz weź ten wzór programu i w miejsce na napis wstaw w nim ten literał

Ten przepis jest — przypominam — równoważny poprzednio omawianemu przepisowi (to znaczy, że produkuje takie same kłajny). Ma tę wadę, że — moim zdaniem — trudniej z niego zrozumieć, jak to działa. Ma tę zaletę, że — moim zdaniem — w trudnych sytuacjach łatwiej użyć go do napisania kłajna.

A teraz użyję tego przepisu do napisania kłajna w Excelu. A dokładniej: napiszę nie w Excelu, tylko w Libre Office, ale to, co napiszę, w Excelu też powinno zadziałać.

Najpierw napiszę wzór formuły, która bierze jakiś napis, robi w nim znajdź i zastąp w miejsce placeholdera wstawiając literał tego napisu, po czym wypisuje wynik. Będę musiał użyć funkcji podstaw, której używa się tak:

=PODSTAW(napis-w-którym-ma-się-odbyć-podstawienie; co-znaleźć; co-wstawić-w-miejsce-znalezionego)

W trzecim argumencie ma być — jak pamiętamy — literał pierwszego argumentu. Wiążą się z tym dwa problemy: jak (w trzecim argumencie) zmienić dany napis w literał i jak w trzecim argumencie zapisać tu ma być ten sam napis, co w pierwszym argumencie.

Zmienienie napisu w literał napisu jest łatwe. Jeśli nie ma w nim znaków specjalnych (a, jak pamiętamy, nie chcemy, żeby były, bo zacznie się masakra), wystarczy otoczyć go cudzysłowami. W taki sposób:

=ZNAK(34)&jakiśnapis&ZNAK(34)

Na przykład:

=ZNAK(34)&"Rumunia"&ZNAK(34)

A powiedzieć, że w trzecim argumencie ma być ten sam napis, co w pierwszym argumencie, też jest łatwo. Wystarczy w trzecim argumencie napisać ten sam napis, co w pierwszym argumencie. W taki sposób:

=PODSTAW(jakiśnapis;"jakiśplaceholder";tensamnapis)

Na przykład:

=PODSTAW("FIKU XXX MIKU";"XXX";"FIKU XXX MIKU")

Łączę obie techniki i już mam wzór formuły, która bierze jakiś napis, robi w nim znajdź i zastąp w miejsce placeholdera wstawiając literał tego napisu, po czym wypisuje wynik. Oto to:

=PODSTAW(jakiśnapis;ZNAK(35);ZNAK(34)&jakiśnapis&ZNAK(34))  

Zapamiętuję ten wzór formuły, bo w przyszłości będę go potrzebował. Będę o nim mówił ten wzór formuły.

Oto przykład użycia tego wzoru formuły:

=PODSTAW("fiku # miku";ZNAK(35);ZNAK(34)&"fiku # miku"&ZNAK(34))  

Tak oto mam zrealizowany pierwszy punkt planu. Teraz zrealizuję drugi punkt, który każe: napisz sobie gdzieś z boku literał źródła tego wzoru programu. No więc zapisuję sobie na boku taki napis:

"=PODSTAW(jakiśnapis;ZNAK(35);ZNAK(34)&jakiśnapis&ZNAK(34))"

Teraz zrealizuję trzeci punkt planu, który każe: teraz w tym literale w miejscu, w którym ma być napis, na którym ma być dokonana operacja, umieść placeholdera. Proszę bardzo:

"=PODSTAW(#;ZNAK(35);ZNAK(34)&#&ZNAK(34))"

Zapamiętajmy sobie ten literał. Będę mówił o nim ten literał.

A teraz robię ostatni punkt planu. Wezmę ten wzór formuły i w miejsce na napis wstawię w nim ten literał. Wychodzi mi takie coś (na niebiesko oznaczyłem ten literał, żeby łatwiej było zobaczyć, co się tu dzieje):

=PODSTAW("=PODSTAW(#;ZNAK(35);ZNAK(34)&#&ZNAK(34))";ZNAK(35);ZNAK(34)&"=PODSTAW(#;ZNAK(35);ZNAK(34)&#&ZNAK(34))"&ZNAK(34))  

No i to jest ten kłajn. Przetestowałem go w Libre Office i działa.

W polskim Excelu też chyba powinien działać. Kiedy zmieniłem w nim średniki na przecinki i nazwy funkcji z polskich na angielskie, zadziałał w angielskim Excelu:

=SUBSTITUTE("=SUBSTITUTE(#,CHAR(35),CHAR(34)&#&CHAR(34))",CHAR(35),CHAR(34)&"=SUBSTITUTE(#,CHAR(35),CHAR(34)&#&CHAR(34))"&CHAR(34))