*(>:^]*(*>{<-!<:^>[:((-<)<(<!-)>>-_)_<<]>:]<]]}*<)]*(:)*=<*)>]
Muss mit den -ln
Befehlszeilen-Flags ausgeführt werden (daher +4 Byte). Druckt 0
für zusammengesetzte Zahlen und 1
für Primzahlen.
Probieren Sie es online!
Ich denke, dies ist das erste nicht triviale Stack Cats-Programm.
Erläuterung
Eine kurze Einführung in Stack Cats:
- Stack Cats verarbeitet eine unendliche Anzahl von Stapeln, wobei ein Bandkopf auf einen aktuellen Stapel zeigt. Jeder Stapel ist anfangs mit unendlich vielen Nullen gefüllt. Ich werde diese Nullen in meinem Wortlaut im Allgemeinen ignorieren. Wenn ich also "das Ende des Stapels" sage, meine ich den niedrigsten Wert ungleich Null, und wenn ich sage, dass "der Stapel leer ist", meine ich, dass nur Nullen darauf sind.
- Bevor das Programm startet,
-1
wird a auf den Anfangsstapel geschoben, und dann wird die gesamte Eingabe darüber geschoben. In diesem Fall wird -n
die Eingabe aufgrund des Flags als dezimale Ganzzahl gelesen.
- Am Ende des Programms wird der aktuelle Stapel für die Ausgabe verwendet. Wenn
-1
unten ein steht, wird es ignoriert. Aufgrund des -n
Flags werden die Werte aus dem Stapel einfach als durch Zeilenvorschub getrennte Dezimalzahlen gedruckt.
- Stack Cats ist eine umkehrbare Programmiersprache: Jeder Code kann rückgängig gemacht werden (ohne dass Stack Cats einen expliziten Verlauf verfolgt). Genauer gesagt, um einen Code umzukehren, spiegeln Sie ihn einfach, z . B.
<<(\-_)
wird (_-/)>>
. Dieses Entwurfsziel schränkt ziemlich stark ein, welche Arten von Operatoren und Steuerungsflusskonstrukten in der Sprache existieren und welche Arten von Funktionen für den globalen Speicherstatus berechnet werden können.
Um das Ganze abzurunden, muss jedes Stack Cats-Programm selbstsymmetrisch sein. Möglicherweise stellen Sie fest, dass dies für den obigen Quellcode nicht der Fall ist. Das ist, wofür das -l
Flag ist: Es spiegelt implizit den Code auf der linken Seite, wobei das erste Zeichen für die Mitte verwendet wird. Daher ist das eigentliche Programm:
[<(*>=*(:)*[(>*{[[>[:<[>>_(_-<<(-!>)>(>-)):]<^:>!->}<*)*[^:<)*(>:^]*(*>{<-!<:^>[:((-<)<(<!-)>>-_)_<<]>:]<]]}*<)]*(:)*=<*)>]
Effektiv mit dem gesamten Code zu programmieren ist höchst nicht trivial und nicht intuitiv und hat noch nicht wirklich herausgefunden, wie ein Mensch es möglicherweise tun kann. Wir haben ein solches Programm brutal für einfachere Aufgaben gezwungen , wären aber nicht in der Lage gewesen, dies von Hand zu erreichen. Zum Glück haben wir ein Grundmuster gefunden, mit dem Sie eine Hälfte des Programms ignorieren können. Dies ist sicherlich nicht optimal, aber derzeit die einzige bekannte Methode, um in Stack Cats effektiv zu programmieren.
In dieser Antwort lautet die Vorlage des Musters also wie folgt (es gibt einige Unterschiede in der Art und Weise, wie es ausgeführt wird):
[<(...)*(...)>]
Wenn das Programm startet, sieht das Stapelband folgendermaßen aus (zum Beispiel für die Eingabe 4
):
4
... -1 ...
0
^
Der [
bewegt die Oberseite des Stapels nach links (und den Bandkopf entlang) - wir nennen dies "Pushing". Und das <
bewegt den Tonkopf alleine. Nach den ersten beiden Befehlen haben wir folgende Situation:
... 4 -1 ...
0 0 0
^
Nun (...)
ist die eine Schleife, die ganz einfach als Bedingung verwendet werden kann: Die Schleife wird nur betreten und verlassen, wenn die Oberseite des aktuellen Stapels positiv ist. Da es momentan null ist, überspringen wir die gesamte erste Hälfte des Programms. Jetzt ist der mittlere Befehl *
. Dies ist einfach XOR 1
, dh es schaltet das niedrigstwertige Bit oben auf dem Stapel um und verwandelt in diesem Fall das 0
in ein 1
:
... 1 4 -1 ...
0 0 0
^
Nun begegnen wir dem Spiegelbild der (...)
. Dieses Mal ist die Oberseite des Stapels ist positiv und wir tun den Code eingeben. Bevor wir uns ansehen, was in den Klammern vor sich geht, lassen Sie mich erklären, wie wir am Ende abschließen: Wir möchten sicherstellen, dass am Ende dieses Blocks der Bandkopf wieder einen positiven Wert aufweist (so dass die Die Schleife endet nach einer einzelnen Iteration und wird einfach als lineare Bedingung verwendet.) Der Stapel rechts enthält die Ausgabe und der Stapel rechts davon enthält a -1
. Wenn dies der Fall ist, verlassen wir die Schleife, >
bewegen uns auf den Ausgabewert und verschieben ]
ihn auf den, -1
so dass wir einen sauberen Stapel für die Ausgabe haben.
Das ist das. Jetzt können wir in den Klammern tun, was immer wir wollen, um die Primalität zu überprüfen, solange wir sicherstellen, dass wir die Dinge wie im vorherigen Abschnitt am Ende beschrieben einrichten (was leicht mit etwas Schieben und Bewegen des Bandkopfs erledigt werden kann). Ich habe zuerst versucht, das Problem mit Wilsons Theorem zu lösen, bin aber zu weit über 100 Bytes gekommen, weil die Berechnung der faktoriellen Quadrate in Stack Cats eigentlich ziemlich teuer ist (zumindest habe ich keinen kurzen Weg gefunden). Also ging ich stattdessen zur Probedivision und das stellte sich tatsächlich als viel einfacher heraus. Schauen wir uns das erste lineare Bit an:
>:^]
Sie haben bereits zwei dieser Befehle gesehen. Außerdem :
tauscht die oberen zwei Werte des aktuellen Stapels und ^
XORs den zweiten Wert in den Spitzenwert. Dies :^
ergibt ein allgemeines Muster, um einen Wert auf einem leeren Stapel zu duplizieren (wir ziehen eine Null über den Wert und wandeln die Null in um 0 XOR x = x
). Danach sieht unser Band so aus:
4
... 1 4 -1 ...
0 0 0
^
Der von mir implementierte Trial-Division-Algorithmus funktioniert nicht für Eingaben 1
, daher sollten wir den Code in diesem Fall überspringen. Wir können leicht Karte 1
auf 0
und alles andere auf positive Werte mit *
, so ist hier, wie wir das tun:
*(*...)
Das heißt, wir verwandeln uns 1
in 0
, überspringen einen großen Teil des Codes, wenn wir ihn tatsächlich erhalten 0
, aber innerhalb machen wir den Code sofort rückgängig, *
damit wir unseren Eingabewert zurückbekommen. Wir müssen nur noch einmal sicherstellen, dass wir am Ende der Klammern mit einem positiven Wert enden, damit sie keine Schleife bilden. Innerhalb der Bedingung bewegen wir einen Stapel nach rechts >
und starten dann die Hauptversuchs-Teilungsschleife:
{<-!<:^>[:((-<)<(<!-)>>-_)_<<]>:]<]]}
Klammern (im Gegensatz zu Klammern) definieren eine andere Art von Schleife: Es handelt sich um eine Do-While-Schleife, dh, sie wird immer für mindestens eine Iteration ausgeführt. Der andere Unterschied ist die Abbruchbedingung: Bei der Eingabe der Schleife merkt sich Stack Cat den Höchstwert des aktuellen Stacks ( 0
in unserem Fall). Die Schleife wird dann ausgeführt, bis derselbe Wert am Ende einer Iteration wieder angezeigt wird. Das ist für uns praktisch: In jeder Iteration berechnen wir einfach den Rest des nächsten potenziellen Divisors und verschieben ihn auf diesen Stapel, mit dem wir die Schleife beginnen. Wenn wir einen Divisor finden, ist der Rest 0
und die Schleife stoppt. Wir werden versuchen, Divisoren ab n-1
und dann bis zu dekrementieren 1
. Das heißt, a) wir wissen, dass dies endet, wenn wir erreichen1
spätestens dann und b) können wir feststellen, ob es sich bei der Zahl um eine Primzahl handelt oder nicht, indem wir den letzten Divisor untersuchen, den wir ausprobiert haben (wenn es sich um 1
eine Primzahl handelt, ist es keine).
Lasst uns anfangen. Am Anfang gibt es einen kurzen linearen Abschnitt:
<-!<:^>[:
Sie wissen, was die meisten dieser Dinge inzwischen tun. Die neuen Befehle sind -
und !
. Stack Cats hat keine Inkrementierungs- oder Dekrementierungsoperatoren. Es hat jedoch -
(Negation, dh Multiplikation mit -1
) und !
(bitweise NICHT, dh Multiplikation mit -1
und Dekrementierung). Diese können entweder zu einem Inkrement !-
oder einem Dekrement kombiniert werden -!
. Also dekrementieren wir die Kopie von n
oben auf -1
, machen dann eine weitere Kopie von n
links auf dem Stapel, holen dann den neuen Testteiler und legen ihn darunter n
. Bei der ersten Iteration erhalten wir also Folgendes:
4
3
... 1 4 -1 ...
0 0 0
^
Bei weiteren Iterationen wird der Wert 3
durch den nächsten Testteiler usw. ersetzt (wobei die beiden Kopien von n
an dieser Stelle immer den gleichen Wert haben).
((-<)<(<!-)>>-_)
Dies ist die Modulo-Berechnung. Da Schleifen bei positiven Werten enden, besteht die Idee darin, mit -n
dem Trial-Divisor d
zu beginnen und diesen wiederholt hinzuzufügen , bis ein positiver Wert erhalten wird. Sobald wir dies tun, subtrahieren wir das Ergebnis von d
und dies gibt uns den Rest. Das Knifflige dabei ist, dass wir nicht einfach einen -n
auf den Stapel legen und eine Schleife starten können, die sich addiert d
: Wenn der obere Teil des Stapels negativ ist, wird die Schleife nicht betreten. Dies sind die Einschränkungen einer umkehrbaren Programmiersprache.
Um dieses Problem zu umgehen, beginnen wir n
oben auf dem Stapel, negieren es jedoch nur bei der ersten Iteration. Das klingt wieder einfacher, als es sich herausstellt ...
(-<)
Wenn die Oberseite des Stapels positiv ist (dh nur bei der ersten Iteration), negieren wir sie mit -
. Dies können wir jedoch nicht einfach tun, (-)
da wir dann die Schleife erst verlassen würden, wenn sie-
zweimal angewendet wurde. Wir verschieben also eine Zelle nach links, <
weil wir wissen, dass es dort einen positiven Wert gibt 1
. Okay, jetzt haben wir n
die erste Iteration verlässlich negiert . Aber wir haben ein neues Problem: Der Bandkopf befindet sich jetzt bei der ersten Iteration an einer anderen Position als bei jeder anderen. Wir müssen dies konsolidieren, bevor wir weitermachen. Der nächste <
bewegt den Bandkopf nach links. Die Situation bei der ersten Iteration:
-4
3
... 1 4 -1 ...
0 0 0 0
^
Und in der zweiten Iteration (denken Sie daran, wir haben d
einmal in -n
jetzt hinzugefügt ):
-1
3
... 1 4 -1 ...
0 0 0
^
Die nächste Bedingung führt diese Pfade erneut zusammen:
(<!-)
Bei der ersten Iteration zeigt der Bandkopf auf eine Null, sodass diese vollständig übersprungen wird. Bei weiteren Iterationen zeigt der Bandkopf jedoch auf eine Eins. Führen Sie dies also aus, bewegen Sie sich nach links und erhöhen Sie die Zelle dort. Da wir wissen, dass die Zelle bei Null beginnt, ist sie jetzt immer positiv, sodass wir die Schleife verlassen können. Dies stellt sicher, dass wir immer zwei Stapel vom Hauptstapel übrig haben und jetzt mit zurückgehen können >>
. Dann machen wir am Ende der Modulo-Schleife -_
. Du weißt es schon -
. _
ist Subtraktion , was ^
zu XOR ist: Wenn die Oberseite des Stapels ist a
und der Wert darunter ist b
es ersetzt a
mit b-a
. Da wir zuerst negiert a
jedoch -_
ersetzt a
mit b+a
, wodurch das Hinzufügend
in unsere laufende Summe.
Nachdem die Schleife beendet ist (wir haben einen positiven Wert erreicht), sieht das Band folgendermaßen aus:
2
3
... 1 1 4 -1 ...
0 0 0 0
^
Der am weitesten links stehende Wert kann eine beliebige positive Zahl sein. In der Tat ist es die Anzahl der Iterationen minus eins. Es gibt jetzt noch ein kurzes lineares Bit:
_<<]>:]<]]
Wie ich bereits sagte, müssen wir das Ergebnis von subtrahieren d
, um den tatsächlichen Rest ( 3-2 = 1 = 4 % 3
) zu erhalten, also machen wir es einfach noch _
einmal. Als nächstes müssen wir den Stapel bereinigen, den wir links erhöht haben: Wenn wir den nächsten Divisor versuchen, muss er wieder Null sein, damit die erste Iteration funktioniert. Also bewegen wir uns dorthin und schieben diesen positiven Wert mit auf den anderen Hilfsstapel <<]
und bewegen uns dann mit einem anderen auf unseren operativen Stapel zurück >
. Wir ziehen d
mit :
und schieben es zurück auf das -1
mit ]
und dann bewegen wir den Rest auf unseren bedingten Stapel mit <]]
. Das ist das Ende der Testteilungsschleife: Dies wird fortgesetzt, bis wir einen Rest von Null erhalten. In diesem Fall enthält der Stapel auf der linken Seiten
der größte Teiler (außer n
).
Nachdem die Schleife beendet ist, müssen *<
wir die Pfade erst wieder mit der Eingabe verbinden 1
. Das *
verwandelt die Null einfach in eine 1
, die wir gleich brauchen, und dann bewegen wir uns zum Divisor mit <
(so dass wir uns auf dem gleichen Stapel wie für die Eingabe befinden 1
).
An dieser Stelle hilft es, drei verschiedene Arten von Eingaben zu vergleichen. Erstens, der Sonderfall, n = 1
in dem wir nichts von diesem Teilprozess gemacht haben:
0
... 1 1 -1 ...
0 0 0
^
Dann ist unser vorheriges Beispiel n = 4
eine zusammengesetzte Zahl:
2
1 2 1
... 1 4 -1 1 ...
0 0 0 0
^
Und schließlich n = 3
eine Primzahl:
3
1 1 1
... 1 3 -1 1 ...
0 0 0 0
^
Für Primzahlen haben wir also einen 1
auf diesem Stapel, und für zusammengesetzte Zahlen haben wir entweder eine 0
oder eine positive Zahl größer als 2
. Wir machen aus dieser Situation das 0
oder das, was 1
wir brauchen, mit dem folgenden abschließenden Code:
]*(:)*=<*
]
schiebt diesen Wert einfach nach rechts. Dann *
verwendet die bedingte Situation stark zu vereinfachen: durch das niedrigstwertige Bit Makeln, wir drehen 1
(prime) in 0
, 0
(Composite) in den positiven Wert 1
, und alle anderen positiven Werte bleiben nach wie vor positiv. Jetzt müssen wir nur noch zwischen 0
positiv und positiv unterscheiden. Dort setzen wir einen anderen ein (:)
. Wenn die Spitze des Stapels ist 0
(und die Eingabe eine Primzahl war), wird dies einfach übersprungen. Aber wenn die Spitze des Stapels positiv ist (und die Eingabe eine zusammengesetzte Zahl war), wird sie durch die vertauscht 1
, so dass wir nun 0
für zusammengesetzte und haben1
für Primzahlen - nur zwei unterschiedliche Werte. Natürlich sind sie das Gegenteil von dem, was wir ausgeben wollen, aber das kann leicht mit einem anderen behoben werden *
.
Jetzt müssen wir nur noch das von unserem umgebenden Framework erwartete Stapelmuster wiederherstellen: Bandkopf auf einen positiven Wert, Ergebnis oben rechts -1
auf dem Stapel und ein einzelnes auf dem Stapel rechts davon . Dafür ist =<*
. =
tauscht die Oberseiten der beiden benachbarten Stapel aus und verschiebt dabei den -1
nach rechts vom Ergebnis, z. B. für die 4
erneute Eingabe :
2 0
1 3
... 1 4 1 -1 ...
0 0 0 0 0
^
Dann bewegen wir uns einfach nach links <
und machen aus dieser Null eine Eins mit *
. Und das ist das.
Wenn Sie mehr über die Funktionsweise des Programms erfahren möchten, können Sie die Debug-Optionen verwenden. Fügen Sie entweder das -d
Flag hinzu und fügen "
Sie es an einer beliebigen Stelle ein, an der Sie den aktuellen Speicherstatus anzeigen möchten, z. B. so , oder verwenden Sie das -D
Flag , um eine vollständige Ablaufverfolgung des gesamten Programms abzurufen . Alternativ können Sie Timwis EsotericIDE verwenden, das einen Stack Cats-Interpreter mit einem schrittweisen Debugger enthält.