Zum Inhalt springen

Kurs:Programmieren in Aleph/Wie sag ich es in Aleph

Aus Wikiversity

Erste Eingaben

[Bearbeiten]

Aleph ist sowohl Interpreter als auch Compiler. Es ist aber zuerst ein interaktives System. Damit wird jede Eingabe als ausführbar angesehen. Es besitzt also die Eigenschaften eines Kommandozeilen-Interpreters.

Die wichtigste Einrichtung ist der Stack – präziser Datenstack (DS). Auf ihm laufen alle Daten zusammen. Der Stack ist der einzige Ort, der Werte enthält.

In einem ersten Beispiel wird eine Addition ausgeführt. An dieser Stelle wird noch ein letztes Mal auf den Unterschied von Objekt und Wert eingegangen.

Eingabe:

3 4 + .

Auf den Start-Button klicken. Auf diesen Hinweis wird in Zukunft verzichtet.

Ausgabe:

7

Die Abläufe im System werden jetzt sehr detailliert besprochen. Der Datenstack – ab jetzt nur noch Stack genannt – wird ebenfalls dargestellt. Es gibt noch einen weiteren Stack, der aber erst beim Compiler wichtig ist.

Ablauf einer Addition

[Bearbeiten]

Der Ablauf erfolgt in zwei Richtungen. Die Eingabe wird von links nach rechts abgearbeitet. Der Stack von oben nach unten. Die Beschreibung beginnt bei der Eingabe, wobei der Stack als leer angenommen wird.

  1. Zeichenfolge ("3") aus dem Eingabebereich isolieren.
  2. Zeichenfolge ("3") kann als Wert (3) interpretiert werden.
  3. Wert der Zeichenfolge ermitteln und auf dem Stack ablegen (3 auf Stack).
  4. Zeichenfolge ("4") aus dem Eingabebereich isolieren.
  5. Zeichenfolge ("4") kann als Wert (4) interpretiert werden.
  6. Wert der Zeichenfolge ermitteln und auf dem Stack ablegen (4 auf Stack).
  7. Zeichenfolge ("+") aus dem Eingabebereich isolieren.
  8. Zeichenfolge ("+") kann nicht als Wert interpretiert werden. Zeichenfolge ist das Command +.
    1. Oberstes Element vom Stack nehmen und zwischenspeichern.
    2. Oberstes Element vom Stack nehmen und zwischenspeichern.
    3. Zwischengespeicherte Elemente addieren.
    4. Ergebnis auf dem Stack ablegen.

Die episch lange Beschreibung ist in obigem Bild kurz zusammengefasst. Die Klammer '}' soll die Summierung der beiden obersten Elemente verdeutlichen.

Addition mit GUI in IDE

Der eben beschriebene Ablauf findet bei allen binären Operatoren statt. Natürlich mit der jeweiligen Operation. Um Trugschlüssen aus der Schreibweise vorzubeugen sei hier gleich folgender Hinweis gegeben:

Ja! Es ist UPN (oder postfix).

Nein! Es ist kein Forth und kein Java-Bytecode.

Inhalt des Stacks anzeigen

[Bearbeiten]

Bereits hier muss auf die zentrale Bedeutung des Stacks hingewiesen werden. Der gesamte Datenverkehr konzentriert sich hier. Eine Möglichkeit zur Anzeige des aktuellen Inhalts ist also nicht nur nützlich, sondern für die Programmierung notwendig.

Dieses Command hat die Bezeichnung ".S" (dot-S).

Eingabe:

3 4 5       .S
newline . + .S 
newline . + .S

Ausgabe:

Stack = [ 5 4 3 ]
Stack = [ 9 3 ]
Stack = [ 12 ]

Hier wird ein Command namens "newline" benutzt. Es dürfte wohl weitgehend selbsterklärend sein. Weil aber beinahe jedes Betriebssystem eigene Vorstellungen bei Zeilenvorschüben hat, wurde es in Aleph als Command integriert. Normalerweise genügt es, in den Ausgabestring ein "... \n ..." einzufügen.

Die Ausgabe des Stacks erfolgt von oben nach unten (bezogen auf die Abbildung in "Ablauf einer Addition"). Ganz links steht also das oberste Element, ganz rechts das unterste. Der Ort des obersten Elements wird mit TOS für Top_Of_Stack bezeichnet.

Aufgabe:

Wo ist die Position des obersten Elements in der Anzeige des Stacks?

  1. Immer ganz links, unabhängig von sonstigen Einflüssen.
  2. Je nach dem letzen Command, denn es positioniert das Element erst.
  3. Immer ganz rechts, denn die Eingabe erfolgte ja von links nach rechts.

Lösung

Punkt- vor Strichrechnung

[Bearbeiten]

Wenigstens ein Beispiel zum Umgang mit verschiedenen Notation muss am Anfang dieses Kurses gegeben sein. Es geht um Klammern, die es im normalen Modus von Aleph nicht gibt.

Infix-Notation: 3+4*5

Prefix-Notation: +(3, *(4, 5))

Postfix-Notation: 3 4 5 * +

Mal ehrlich – in den meisten Programmiersprachen wird prefix-Notation benutzt.

Stimmt nicht? Doch, denn wann werden denn schon simple Operatoren benutzt?

Bereits die Addition mit BigNumber-Instanzen (gibt’s auch in C/C++) erlaubt kein '+' Die Addition wird hier mit "add( a, b)", die Multiplikation mit "mul( a, b)" durchgeführt. Die Arbeit mit '+' und anderen infix-Operatoren wird mehr und mehr zur Ausnahme. Im Gesamtpaket von Aleph ist ein Parser infix-Notation vorhanden. Er wandelt wahlweise nach pre- oder postfix. Die Wandlung nach prefix ist für das Listprocessing in Aleph vorhanden. Am Anfang dieses Kurses – also hier – ist erst einmal die postfix-Notation relevant.

Eine kleine Änderung im obigen infix-Ausdruck und schon scheint postfix am Ende.

Aus 3+4*5 wird (3+4)*5 mit dem Ergebnis 35 statt 23.

Mit ein wenig Nachdenken ergibt sich natürlich auch in postfix eine entsprechende Schreibweise.

Aus 3 4 5 * + wird nun einfach 3 4 + 5 * und 35 ist das Ergebnis.

Auch für diese Änderung hier noch einmal die drei Notationen:

Infix-Notation: (3+4)*5

Prefix-Notation: *(+(3, 4), 5))

Postfix-Notation: 3 4 + 5 *

Die Arbeit mit dem Stack ist und postfix-Notation ist am Anfang gewöhnungsbedürftg. Eigentlich ist sie ausgesprochen schwierig. Die Elemente liegen praktisch nie in der Reihenfolge vor, wie sie gebraucht werden. Im folgenden Teil werden Commands besprochen, die hier sehr hilfreich sind.

Manipulation des Stacks

[Bearbeiten]

Die hier zu besprechenden Commands werden am schnellsten durch Ausprobieren verständlich. Neben dem Stack gehören sie zu den wichtigsten Elementen von Aleph. Viele dieser Commands sind auch als Befehle in der JVM vorhanden. Jedoch sind sie dort mit einem Zusatz zur Kennzeichnung des jeweils erforderlichen Speicherbedarfs versehen.

Vertauschen

[Bearbeiten]

Das Command swap vertauscht die beiden obersten Elemente auf dem Stack. Einfach ausprobieren liefert hier den schnellsten Lernerfolg. Also erst einmal ein paar Elemente auf dem Stack unterbringen. Natürlich nicht ohne sich gleich einen Überblick zu verschaffen.

Eingabe:

1 2 3 .S

Ausgabe:

Stack = [ 3 2 1 ]

Das oberste Element ist 3, mit 2 gleich darunter. Nach einem "swap" sollte also die Reihenfolge "2, 3, 1“ sein. Also ausprobieren. Achtung! Erst der Clear-Button.

Eingabe:

swap newline . .S

Ausgabe:

Stack = [ 2 3 1 ]

So weit so gut. Aber nur die beiden obersten Elemente vertauschen zu können, erscheint recht wenig. Natürlich können auch andere, tiefer liegende, Elemente erreicht werden. In anderen Sprachen (C/C++, Java) wird in diesem Zusammenhang der Befehl "peek" verwendet. Er gestattet den Zugriff auf jedes Element im Stack. Aleph legt aber Wert auf Kontinuität. Ausgangspunkt ist stets der TOS (Top_Of_Stack). Um das hier vorhandene Element mit dem an n-ter Stelle liegenden zu vertauschen, gibt es das Command "nswap". Hier muss zusätzlich die Position des zu vertauschenden Elements angegeben werden.

Eingabe:

1 nswap newline . .S

Ausgabe:

Stack = [ 1 3 2 ]

Kontinuität gilt auch für Grenzsituationen. Deshalb muss "swap" auch durch "nswap" zu realisieren sein. Wie das Beispiel vermuten lässt, ist "swap" äquvalent zu "0 nswap". Einfach mal ausprobieren.

Duplizieren von Elementen

[Bearbeiten]

Es kommt oft vor, dass Elemente öfter benötigt werden. Beispielsweise Indizes in Schleifen oder die Summation vieler Elemente. In diesen Fällen muss die Möglichkeit bestehen, bestimmte Elemente auf dem Stack zu duplizieren.

Sollten noch alte Elemente auf dem Stack liegen, oder irgendwelche störenden Meldungen im Ausgabebereich stehen, einfach Neustart von Aleph. Deshalb auch der Hinweis auf eine IDE.

Eingabe:

1 2 3
dup .
newline . .S

Das Command zum Duplizieren des obersten Elements lautet "dup".

Ausgabe:

3
Stack = [ 3 2 1 ]

Wie bei der Vertauschung gibt es auch beim Duplizieren die Möglichkeit, bestimmte Elemente des Stacks zu duplizieren. Der Name des Commands lautet "ndup" und verlangt die Angabe der Position des zu duplizierenden Elements. Das Duplikat befindet sich dann an TOS, also oben auf dem Stack.

Wieder besteht Kontinuität, weshalb "0 ndup" äquivalent zum einfachen "dup" ist.

Das folgende Beispiel ist schon etwas anspruchsvoller. Wenn das Ergebnis nicht gleich nachvollzogen werden kann, macht nichts. Die Sequenz "newline . .S" an den unverstandenen Stellen einsetzen und neu starten.

Eingabe:

2 ndup 
2 ndup 
2 ndup
. . .
newline . .S

Ausgabe:

321
Stack = [ 3 2 1 ]

Befehlsfolgen wie diese wirken sehr konstruiert. Sie kommen so praktisch nie vor, zeigen aber, dass es zunehmend schwieriger wird, die Übersicht über den Stack zu behalten. Abhilfe schafft eine besondere Technik der Kommentierung. Vorher aber soll noch das letzte Command zur Stackmanipulation besprochen werden.

Entfernen von Elementen

[Bearbeiten]

Auf dem Stack liegen immer noch die drei ursprünglichen Elemente. Das oberste Element wird mit dem Command "drop" entfernt ("fallen lassen" wäre die korrekte Übersetzung).

Eingabe:

drop

Ausgabe:

Stack = [ 2 1 ]

Wie könnte es anders sein, auch zu "drop" gibt es ein "ndrop". Der Vollständigkeit halber wird es in einem Beispiel gezeigt.

Eingabe:

1 ndrop

Ausgabe:

Stack = [ 2 ]

Geschafft! Die Commands zur Manipulation des Stacks sind besprochen. Es mag an einigen Stellen langweilig, an anderen Stellen auch schwierig zu verstehen gewesen sein. Insgesamt war es wohl nur ungewohnt.

Aufgabe:

Welche Aussagen zu folgenden Commands zur Manipulation des Stacks sind korrekt?

  • swap und nswap
  1. swap vertauscht die letzten beiden, nswap vertauscht die nten beiden Elemente.
  2. swap vertauscht die beiden obersten, nswap vertauscht die nten beiden Elemente.
  3. swap vertauscht die beiden obersten Elemente, nswap vertauscht das oberste mit dem nten Elemente.
  • drop und ndrop
  1. drop entfernt das oberste, ndrop das unterste Element.
  2. drop entfernt das unterste, ndrop das nte Element.
  3. drop entfernt das oberste, ndrop das nte Element.

Lösungen

Erste Programme

[Bearbeiten]

Für die Programmierung ist eigentlich die Aleph-Dokumentation erforderlich. Alle Commands des Systems sind dort ausführlich beschrieben. Weil aber erstaunlich wenig Befehle vorhanden sind, ist eine Liste im Anhang zum Kurs vorhanden.

Die ersten Programme beschänken sich zunächst auf das vorhandenen Vokabular. So kann die Programmiertechnik und Lesbarkeit des Quellcodes leichter erlernt werden. Besonderes Gewicht wird auf die Kommentierung gelegt, denn sie ist in Aleph-Programmen unverzichtbar.

Sollten bereits Vorkenntnisse auf dem Gebiet der Programmierung bestehen, werden einige Kommentare als überflüssig, vielleicht sogar als übertrieben erscheinen. Die ersten Programme sind aber für Anfänger nicht ohne Probleme.

Was ist ein Aleph-Programm

[Bearbeiten]

Wenn Aleph Interpreter und Compiler zur gleichen Zeit ist, darüber hinaus auch noch interaktiv, dann ist jede Eingabesequenz prinzipiell ein Programm. Hier soll aber von Programmen nur dann die Rede sein, wenn es sich um compilierte Befehlsfolgen handelt. Mit dieser Feststellung ist eine erste Differenzierung zwischen Programmen und Sequenzen gegeben.

Ein Programm sollte stets einen Namen haben. Die meisten Sprachen verwenden zu diesem Zweck Schlüsselworte wie "define", "proc" oder "func", gefolgt vom Bezeichner. Aleph macht hier zunächst keine Ausnahme. Aber aus Respekt vor der Mutter aller Sprachen mit virtueller MaschineForth – lautet hier das Schlüsselwort ":" (Colon).

Das erste Beispiel soll die lästige Sequenz "'newline . .S'" aus dem vorangegangenen Beispielen zusammenfassen. Weil es hier nur um die Anzeige der aktuellen Situation auf dem Stack geht, wird als Name "actS" benutzt.

Eingabe:

: actS       // Kompilierung starten
   newline . // Zeilenvorschub ausgeben
   .S        // anzeigen des Stacks
  ;          // Kompilierung beenden

// Test des neuen Commands

1 2 3        // Stacksituation vorgeben
.            // Stackinhalt aendern
actS         // Test des Commands actS (actualStack)
newline .    // Zeilenvorschub ausgeben
. .          // Stackinhalt aendern
.S           // Stak ueberpruefen  (sollte leer sein)

Ausgabe:

3
Stack = [ 2 1 ]
21Stack = [ ]

Hier wurde erst ein Programm compiliert und gleich als neues Command bereitgestellt. Gleich im Anschluss wird das Programm getestet.

Aufbau im Speicher

[Bearbeiten]

Der prinzipielle Aufbau eines Commands im Speicher ist hier dargetellt. Dabei wurde die Farbgebung an das anfangs dargestellte Organisation des Speichers (Abschnitt-2) angelehnt. Die Klammerung von Namen - z.B. (newline) - und die in Item abweichende Farbgebung sollen die Verwendung von compiliertem verdeutlichen.

Aufbau eines Commands

Die durchgehende Verwendung der doppelten Verkettung wurde die Möglichkeit eröffnet, Programme reversibel zu gestalten. Natürlich müssen dann alle Schleifen und Konditionalanweisungen rekusiv programmiert werden. Auf diesen Aspekt wird eine Erweiterung der rekursiven Programmierung unter Aleph noch eingehen.

Die Abbildung zeigt, was bei der Compilierung vor sich gegangen ist. Eine verbale Beschreibung des prinzipiellen Ablaufs kann nun anhand des Bildes nachvollzogen werden.

  1. : (Colon) erkannt --> Wechsel in den Compilierungszustand.
  2. Neue Command-Instanz erzeugen.
  3. Zeichenfolge "actS" als Name des neuen Commands eintragen.
  4. Zeichenfolge "newline" im Dictionary suchen und neues Item mit der Adresse von (newline) einfügen.
  5. Zeichenfolge "." im Dictionary suchen und neues Item mit der Adresse von (.) einfügen.
  6. Zeichenfolge ".S" im Dictionary suchen und neues Item mit der Adresse von (.S) einfügen.
  7. ; (Semikolon) erkannt --> Command-Instanz ins Dictionary einfügen und Wechseln in den Interpreterzustand.

So funktioniert - sehr vereinfacht - der "Colon-Compiler" von Aleph. Mit ein wenig Erfahrung können auch völlig andere Compiler realisiert werden. Deshalb ist der "Colon-Compiler" auch nicht im Kern des Aleph Wortschatzes vorhanden. In jedem Fall sind jetzt die Grundlagen vorhanden, um erste Programme zu verstehen. Noch können keine Entscheidungen hinsichtlich des weiteren Verlaufs innerhalb der Programme getroffen werden. Dazu müssen noch die verschiedenen Kategorien von Commands erläutert werden.

Verhalten von Commands

[Bearbeiten]

Zunächst muss auf die verschiedenen Arten von Commands eingegangen werden. Aleph unterscheidet vier Arten von Commands. Es sind

  • primary Commands
Diese Anweisungen bestehen nur aus Befehlen der untergeordneten Maschine. Es ist also Java-Code. Die elementaren Commands des Kerns gehören zu dieser Kategorie.
  • secondary Commands
Anweisungen dieser Art sind aus bereits vorhandenen Commands aufgebaut. Das eben erstellte Programm ist ein solches "secondary". Natürlich können secondary-Commands wieder aus secondaries bestehen.
  • immediate Commands
Diese Anweisungen werden vom (Colon-)Compiler anders behandelt. Sie werden bereits ausgeführt wenn eigentlich compiliert wird. So sind alle Konditionalanweisungen (if, else ...) immediate. Oft compilieren diese "immediates" ihrerseits unsichtbare Anweisungen (keine Angst – Aleph versteckt nichts).
  • invisible Commands
Unsichtbare Anweisungen sind Sprungbefehle. Aleph bietet keine Möglichkeit zur Formulierung von "gotos". Auch ein Umweg über "labels" und "breaks" ist nicht möglich. Weil aber sequentielle "if ... else ..."-Konstruktionen immer auch eine Änderung im Programmfluss – eben ein goto – bedeuten, werden sie von "if" und "else" bereitgestellt. Es werden also bedingte Sprungbefehle compiliert.

Commands können also unterschiedliche Verhaltensweisen haben. Das Semikolon ";", die Kommentarzeichen "//", Hoch- und Doppelhochkomma sind keine Commands und werden deshalb auch nicht copmiliert.

Achtung! Als immediate erklärte Commands können natürlich im ganz normal im Interpreter-Modus benutzt werden, bei Konditionalanweisungen ist das aber sinnlos. Der Interpreter kennt keine Adressen, denn er führt die Commands nur aus. Das "if"-Command würde zwar tatsächlich einen Sprungbefehl compilieren, aber weil der Code keine Bindung an ein Command hat wieder vergessen. Außerdem würden unsinnige Daten auf dem Stack liegen, die "if" eigentlich für die Adressberechnung braucht.

Programm mit Verzweigungen

[Bearbeiten]

Die Verwendung von "if ... else ..." erfolgt in Aleph auch in der postfix-Notation. Bei der herkömmlichen Programmierung hat die if-Anweisung den Charakter einer Funktion, mit der Bedingung als Argument. Das Ergebnis wird an die Ablaufsteuerung übergeben, die dann eine entsprechende Änderung vornimmt.

So verhält es sich auch in Aleph, nur dass hier das Argument auf dem Stack liegt. Das folgende Beispiel zeigt eine sehr einfache Form.

Eingabe:

: <9?                     //       --> val
   9                      // val   --> 9 val
   <                      // 9 val --> flg
   if   " Ja"             // flg   --> str
   else " Nein"
   endif
   .                      // str   -->
 ;

// Test von <9?

 7 <9? newline . // Erwartung: Ja
17 <9? newline . // Erwartung: Nein
 9 <9? newline . // Erwartung: Nein
.S               // Der Stack sollte leer sein

Ausgabe:

Ja
Nein
Nein
Stack = [ ]

Dieses Beispiel ist mit Absicht extrem einfach gehalten. Weil gleich mehrere Aspekte besprochen werden, muss es für den Anwender stets nachvollziehbar bleiben. Die folgenden Punkte beschäftigen sich mit

  • Namensgebung,
  • Absturz der V2M,
  • Anzeige des compilierten Codes.

Zunächst der recht kryptische Name "<9?". Er besteht nur aus Sonderzeichen und Ziffern, also Nichts was in "normalen" Sprachen erlaubt wäre.

Namen können täuschen

[Bearbeiten]

Aleph akzeptiert generell jede Zeichenfolge ohne "white spaces" als Name! Wirklich Alles ohne Leerzeichen wird als Name für neue Commands akzeptiert. Aleph hat keine Syntax. Nur so kann dem Anwender die geforderte Freiheit gewährt werden.

Beispiel:

: 3 "drei" ;

Die Ziffer drei wird mit dem String "drei“ verbunden. Damit erzeugt jedes Vorkommen der einzelnen "3" nicht mehr den Wert der Zahl 3, sondern den String "drei".

3 . newline .
33 .

zeigt denn auch das befremdliche Verhalten.

drei
33

Nicht Alles was möglich ist muss auch getan werden, also vergessen. Das geht in Aleph. I nächsten Abschnitt wird auf das "gezielte Vergessen" eingegangen. Hier wird die Sequenz

"3" find drop forget

unkommentiert benutzt, um das Command "3" zu vergessen.

Die totale Freiheit bei Bezeichnern kann viel zur einfachen Lesbarkeit von Quelltext beitragen. Einfach mal so angewendet kann es aber auch zu Verfremdungen führen.

Absturz der V2M

[Bearbeiten]

Natürlich kann jedes System abstürzen. Aleph macht da keine Ausnahme, aber die "Härte des Aufschlags" kann gedämpft werden. Weil Aleph eine Java-Anwendung ist, erfolgt bei den eisten Abstürzen eine post mortale Anzeige der Ursache und des Java-Stacks. Diese Anzeige ist oft nicht einfach nachzuvollziehen. Besonders unverständlich wird sie für Aleph-Programme. Es ist vergleichbar mit dem Absturz eines Programms in Basic und der Ausgabe was im Assembler schief gegangen ist.

Weil der Entwickler ohnehin einen Verdacht hat, wird den Meldungen ohnehin wenig Aufmerksamkeit geschenkt. Wichtig ist nur, dass der Computer noch arbeitet und nicht neu bebootet werden muss.

Hier soll jetzt ein Absturz bewusst erzeugt werden. Das gesamte Verhalten ist dabei auch noch von der evtl. verwendeten IDE abhängig. Es können hier natürlich nicht alle IDEs besprochen werden. Sie unterscheiden sich auf dieser Ebene so wenig, dass die drei durchgespielten Szenarien durchaus als repräsentativ angesehen werden können.

In der "if ... else ... endif"-Konstruktion wird "endif" weggelassen. Damit können die "unsichtbaren" Springbefehle nicht komplettiert werden, was garantiert zu fehlerhaftem Verhalten führt.

Eingabe:

: <9?                     //       --> val
   9                      // val   --> 9 val
   <                      // 9 val --> flg
   if   " Ja"             // flg   --> str
   else " Nein"
//   endif                   ACHTUNG!!!
   .                      // str   -->
 ;

0 <9?   // Erwartung: Ja --- Absturz
  • Szenario-1: Verwendete IDE ist eXMPL
Nach dem Klick auf den Start-Button passiert nichts. Keine Aktion im Protokoll-Fenster. Nur eine Verlangsamung im handling des Start-Buttons kann bemerkt werden.
Um an die Java-Fehlermeldung zu gelangen muss hier das Fenster der Aleph-GUI (Eingabefenster) geschlossen werden. Dann wird um Protokoll-Fenster die unten gezeigte Fehlermeldung ausgegeben.
  • Szenario-2: Keine Verwendung einer IDE
Nach dem Klick auf den Start-Button erfolgt die Ausgabe der unten angegebenen Meldung sofort.
  • Szenario-3: Verwendete IDE ist eclipse
Auch hier erfolgt die Ausgabe der Fehlermeldung sofort nach den Klick. Die Konsole muss natürlich geöffnet sein.

Hier die ersten Zeilen der provozierten Fehlermenldung:

java.lang.NullPointerException
        at engine._jump.runTime(_jump.java:43)
        at engine.V2M.execute(V2M.java:288)
usw. usw.

Relevant ist die nicht vorhandene Referenz (NullPointer) im runTime-Part des "unsichtbaren" Commands "_jump". Weil die Konditionalanweisung (if ...) nicht mit einem endif abgeschlossen wurde, fehlen Adressen (Pointer, Referenzen) zur Ablaufsteuerung.

Für ganz harte Entwickler sei hier noch der Ort des Fehlers im Quelltext (.../aleph/engine/_jump.java) gezeigt:

Abstruz un drei Szenen

Die blau markierte Zeile zeigt die Ursache. Es ist kein Ziel (destination.next) angegeben, wodurch die Ablaufsteuerung in einen nicht definierten Zustand geraten könnte.

Anzeige von compiliertem Code

[Bearbeiten]

Dieser Punkt dient auch der Vertiefung des Verständnisses beim Compilieren. Aleph bietet ein Command an, mit dem der compilierte Code von Commands angezeigt werden kann. Der Code der V2M wird dabei in – mehr oder weniger – lesbarer Form angezeigt. Weil es per Definition keine (physischen) Adressen gibt, werden einfache Nummern (beginnend bei 0) verwendet. Die physischen Adressen der Sprungbefehle () werden über relative Adressen im compilierten Code ermittelt. Diese relativen Adressen werden zusätzlich in jeder jump-Instanz gespeichert und erlauben so die Anzeige der Ziele.

Achtung! Jetzt wird wieder vom funktionsfähigen Command ausgegangen. Um sicher zu gehen einfach Aleph beenden, neu starten, korrekten Code hier kopieren und ins Eingabefenster einfügen. Alles zusammen sieht dann ungefähr so aus:

Anzeige compilierten Codes

Um diese Eingabe geht es hier (das Command ist ja vorhanden):

"<9?"   // Name des Commands als String
find    // Suchen eines Commands mit diesem Namen
drop    // GEFUNDEN! Stack = [ true code ]. true muss weg
showDef // Adresse des Codes vorhanden, anzeigen

Und um diese Ausgabe:

Ausgabe:

: <9?
  0: [n_sym](9)
  1: <
  2: [jumpf](5)
  3: [s_sym](" Ja")
  4: [jump](6)
  5: [s_sym](" Nein")
  6: .
;

Sinngemäß steht dort:

0. Identitätsfunktion für den Zahlenwert 9 ausführen [n_sym] (9).

1. Kleiner Relation mit den beiden obersten Elementen auf dem Stack durchführen.

2. Wenn false, dann Sprung nach 5 ausführen [jumpf] (5).

3. Identitätsfunktion für den textuellen Wert " Ja" ausführen [s_sym] (" Ja").

4. Unbedingten Sprung nach 6 ausführen [jumpf] (6).

5. Identitätsfunktion für den textuellen Wert " Nein" ausführen [s_sym] (" Nein").

6. Oberstes Element auf dem Stack ausgeben.

Noch etwas fällt auf, das "endif"-Command wurde nicht compiliert. Es hat auch nur die Aufgabe, die Sprungziele festzulegen.

Eigentlich müsste statt "<" der Klassenname "_lt" (für LessThan) stehen und statt "." "_dot". Die Entwickler fanden wohl, dass die Ausgabe dann nicht mehr so gut zu lesen ist. Warum dann aber "[jump] (6)" statt "goto 6" ausgeben wird bleibt rätselhaft.

Aufgabe:

Warum sind Konditionalanweisugen nur im compilierenden Modus sinnvoll?

  1. Weil im interpretierenden Modus Entscheidungen bereits getroffen sind.
  2. Weil im interpretierenden Modus keine "Sprungziele" vorhanden sind.
  3. Weil im interpretierenden Modus die "Sprungziele" als absolute Adresse vorhanden sind.

Schleifen und Wiederholungen

[Bearbeiten]

Das wiederholte Durchlaufen von mehreren Anweisungen hat zu vielen unterschiedlichen Konstruktionen geführt. Der etablierte Sammelbegriff dafür lautet Schleife (engl. loop). Ob "for"-, "while"-, "do ... while"-, "repeat ... until"-Anweisungen, es sind immer Wiederholungsanweisungen. Erst die Sprache C hat hier ein wenig aufgeräumt. So wurde auf "repeat ... until" komplett verzichtet und "for" über den Präprozessor aus einer äquivalenten "while"-Konstruktion erzeugt (zumindest in den älteren C-Versionen).

Aleph besitzt auch die Möglichkeit bestimmte Sequenzen zu wiederholen. Der Ansatz ist jedoch konsequent am Begriff "loop" orientiert. Die zu wiederholenden Commands werden einfach zwischen die Commands loop und endloop platziert.

In Aleph entspricht ein Loop genau dem Begriff. Die Definition eines entsprechenden Commands, mit anschließender Anzeige des compilierten Codes, gibt einen ersten Eindruck.

: schleife
   loop
    "endlos" . newline .
   endloop
  ;

// Anzeige des Codes

"schleife" // Name des Commands als String
find       // Suchen eines Commands mit diesem Namen
drop       // GEFUNDEN! Stack = [ true code ]. true muss weg
showDef    // Adresse des Codes vorhanden, anzeigen

Ausgabe:

: schleife
  0: [s_sym]("endlos")
  1: .
  2: newline
  3: .
  4: [back](0)
;

Nur die vierte Zeile verrät, dass es sich um eine Schleife handelt. Ähnlich der "if...endif"-Konstruktion, wird hier das "loop"-Command nicht compiliert. Es stellt nur die relative Adresse für den von "endloop" erzeugten Spungbefehl "back" bereit. Der Sprungbefehl trägt wohl auch deshalb den Namen "back". Das mag auch die Erklärung für die vorhin bemängelte Namensgebung der Springbefehle sein. Nur bei Schleifen wird zurückgesprungen, alle anderen Verzweigungen sind vorwärts gerichtet.

Das Command "schleife" wird nie verlassen. Es muss aber eine Möglichkeit geben, sonst macht die "loop"-Anweisung wenig Sinn. Aleph benutzt hierfür das "leave"-Command. Wie der Name schon sagt, dient es zum Verlassen der Schleife.

: count
   0        // lve             --> cnt lve 
   loop
    1 ndup  // cnt lve         --> lve cnt lve
    1 ndup  // lve cnt lve     --> 0 lve cnt lve
    =       // cnt lve cnt lve --> flg cnt lve
    leave   // flg cnt lve     --> cnt lve    leave if true
    1 +     // cnt lve         --> cnt+ lve   increment cnt
    dup . " " .
   endloop
   "fertig " .
 ;

// Test count

10 count .S

In den Kommentaren steht "lve" für den "LeaVE"-Wert und "cnt" für den "CouNT"-Wert. Sind beide gleich wird die Schleife über das "leave"-Command verlassen.

Ausgabe:

1 2 3 4 5 6 7 8 9 10 fertig Stack = [ 10 10 ]

Es ist sehr leicht, aus der Kombination von Bedingung und "leave"-Kommand alle gezeigten Konstruktionen von Schleifen zu erzeugen.

Wichtig! In Aleph ist nur eine "leave"-Anweisung pro Schleife erlaubt.

Aber Aleph würde seinem Namen nicht gerecht werden, wenn es keine Alternative zu dieser Einschränkung gäbe. Das Command "count" wird mit einer kleinen Änderung als "NEWcount" formuliert. Die "leave"-Anweisung ist "auskommentiert" und wird durch die unmittelbar folgende Zeile ersetzt.

: NEWcount
   0        // lve             --> cnt lve 
   loop
    1 ndup  // cnt lve         --> lve cnt lve
    1 ndup  // lve cnt lve     --> 0 lve cnt lve
    =       // cnt lve cnt lve --> flg cnt lve
//    leave   // flg cnt lve     --> cnt lve    leave if true
    if exit endif
    1 +     // cnt lve         --> cnt+ lve   increment cnt
    dup . " " .
   endloop
   "fertig " .
 ;

// Test count

10 count .S

Wieder ist die Aussage des Begriffs (hier also exit) ausschlaggebend für das Verhalten. Statt etwas zu verlassen (leave), wird hier rausgegangen (exit). Der Unterschied ist, dass exit einen Ausgang aus dem Programm (Command) meint und leave nur ein Verlassen der Schleife bedeutet.

Das Ergebnis ist fast das Gleiche.

1 2 3 4 5 6 7 8 9 10 Stack = [ 10 10 ]

Nach dem Ende des Zählvorgangs wird der String "fertig " nicht ausgegeben. Aus dem Command wurde ja bereits vorher rausgegangen. Jedes Programm kann beliebig viele derartige Ausgänge (exits) haben.

Das ist übrigens auch der Grund, warum es in Aleph keine "switch ... case"-Anweisung gibt. Ein Command mit einer mehreren der eben gezeigten Anweisungen ist wesentlich flexibler.

Es ist wohl allmählich mehr als auffällig dürfte, dass in keinem Beispiel eine Variable verwendet wurde. Selbst die Schleifen arbeiten ohne Variablen. Die Begründung wird im folgenden Punkt gegeben.

Aufgabe:

Wie können Konditionalanweisungen (if ... else ... endif) im nicht compilierenden Zustand realisiert werden?

  1. Lesen aller Zeichenketten bis "Schlüsselwort" (else, endif) erkannt und entsprechend handeln.
  2. Erzwingen der Compilierung und dann die vorhandenen Commands verwenden.
  3. Gar nicht. Es ist unmöglich derartiges Verhalten zu realisieren.

Variablen die auch variabel sind

[Bearbeiten]

Die Überschrift lässt es bereits erahnen – die Variablen in Aleph sind irgendwie anders. Aleph nimmt natürlich auch den Begriff Variable wörtlich. Einen guten Einblick ergibt sich mit der bekannten Gleichung einer Geraden:

In der herkömmlichen Programmierung handelt es sich hier um eine Zuweisung des Ergebnisses der Berechnung vom an die Variable y. Die Variable y enthält also den Wert des Ergebnisses. Aber ist das wirklich korrekt?

In der Gleichung steht wörtlich: "y ist (gleich) m mal x plus b". Damit verkörpert y den gesamten Ausdruck und nicht nur den Wert seiner Berechnung. Wenn nur der Wert gemeint ist, dann ist y eben äquivalent zu , aber in der Gleichung steht dass y eben ist. Die Variable y ist ein Repräsentant für die ganze Sequenz . Die Verwendung von y in einem Programm muss gleichbedeutend sein mit dem Einsatz aller Anweisungen zur Ausführung von . Genau diese weit gefasste Ansicht von Variabilität ist bei den Variablen in Aleph vorhanden. Natürlich kann einer Variablen auch einfach eine Zahl zugewiesen werden.

Für Variablen In Aleph gilt stets:

Variablen enthalten nicht etwas, Variablen sind etwas.

Damit wären Variablen nur Commands, denen etwas zugewiesen wird. Es kann sich dabei um Sequenzen, oder einzelne Objekte handeln. Dieser Schluss drängt sich auf, aber es gibt noch einen kleinen Unterschied. Aleph-Variablen können leer sein. Mit leer ist hier gemeint, dass sie tatsächlich nichts enthalten, darstellen, repräsentieren. Variablen können frei sein. Diese Eigenschaft wird beim Listprocessing hoch geschätzt.

Wie flexibel Aleph-Variablen wirklich sind, zeigt erst der nächste Abschnitt. Hier soll es bei der Deklaration und dem Belegen von Variablen bleiben.

Eingabe:

"variabel" variable  // Deklaration einer Variablen
123                  //     --> 123   Zahl bereitstellen
"variabel" is        // 123 -->       Zahl der Variablen zuweisen
variabel             //     --> 123   Variable benutzen
.

Wenig beeindruckend. Auch die Möglichkeit der Variablen Strings und andere Objekte zuzuweisen dürfte den Eindruck nicht ändern. Vielleicht aber Folgendes:

"variabel" variable  // Deklaration einer Variablen

: count
   0        // lve             --> cnt lve 
   loop
    1 ndup  // cnt lve         --> lve cnt lve
    1 ndup  // lve cnt lve     --> 0 lve cnt lve
    =       // cnt lve cnt lve --> flg cnt lve
    leave   // flg cnt lve     --> cnt lve    leave if true
    1 +     // cnt lve         --> cnt+ lve   increment cnt
    dup . " " .
   endloop
   "fertig " .
 ;

"count" find drop         // compilierte Sequenz von "count"
"variabel" is             // Variable IST "count"-Comand
"count" find drop forget  // VERGESSEN des "count"-Comands
10 variabel               // macht nichts

Aleph-Variablen erfüllen die anfangs gestellte Forderung. Die Zuweisung muss zwar immer über den Namen als String erfolgen, aber die Vorteile dürften weit überwiegen. Trotzdem soll ein Zitat aus der Aleph-Dokumentation auch hier nicht fehlen:

Freiheit für die Programme, nieder mit den Variablen!

Damit soll auf spärliche Verwendung von Variablen hingewiesen werden. Aleph benötigt prinzipiell keine Variablen, aber sie sind sehr praktisch. Programmieren ohne Variablen ist nicht effektiv; aber die Verwendung zu vieler Variablen ebenfalls.

Die Einführung in die Aleph-Programmierung ist beinahe abgeschlossen. Es fehlen noch Möglichkeiten Programme zu laden und zu speichern.

Aufgabe:

Welche Aussage(n) über Aleph-Variablen ist/sind zutreffend?

  1. Sie können Symbole (Werte) enthalten.
  2. Sie können ausschließlich Symbole (Werte) enthalten.
  3. Sie können in Commands gewandelt werden und enthalten dann die Sequenzen.
  4. Sie können Sequenzen enthalten.
  5. Sie enthalten nur Referenzen, dann aber auf Werte und/oder Sequenzen.

Programme laden

[Bearbeiten]

Eigentlich überflüssig es zu sagen, aber Aleph vertritt auch hier eigene Auffassungen. Aleph liest die Programme. Nur die Quelle des "Lesestoffs" muss angegeben werden. Das Command lautet fälschlicherweise "load", wo es doch eigentlich read heißen müsste. Hier waren die Entwickler etwas inkonsequent.

Gelesen werden von Aleph ausschließlich Buchstaben. Compilierte Programme können weder gespeichert noch gelesen werden. Das ist keine Nachlässigkeit oder Fehler, sondern es ist einfach unmöglich. Von wo soll ein interpretierender Compiler denn seine zu interpretierenden Anweisungen bekommen, wenn nur compilierter Code vorhanden ist? Und was soll ein compilierender Interpreter compilieren? Aleph delegiert das Laden compilierter Programme an die untergeordnete Maschine.

Die einfachste Methode besteht in der Angabe des Dateinamens als String, gefolgt von dem Command "load". Einfach ausprobieren. Den folgenden Quelltext in einer Textdatei im dem Verzeichnis unterbringen, das auch Aleph enthält. Der Name ist beliebig, hier wird einfach "COUNT.TXT" angenommen.

: count
   0        // lve             --> cnt lve 
   loop
    1 ndup  // cnt lve         --> lve cnt lve
    1 ndup  // lve cnt lve     --> 0 lve cnt lve
    =       // cnt lve cnt lve --> flg cnt lve
    leave   // flg cnt lve     --> cnt lve    leave if true
    1 +     // cnt lve         --> cnt+ lve   increment cnt
    dup . " " .
   endloop
   "fertig " .
 ;

// Test count

10 count .S

Das Verzeichnis sollte danach ungefähr so

Verzeichnis mit neuer Datei

aussehen. Jetzt wird Aleph (neu) gestartet und

"COUNT.TXT" load

eingegben. Ein Klick auf den Start-Button zeigt dann auch das bekannte Ergebnis

1 2 3 4 5 6 7 8 9 10 fertig Stack = [ 10 10 ]

Das Command "count" wird compiliert und ausgeführt. Aleph verhält sich genau so, als wäre die Eingabe über die Tastatur erfolgt. Und gleich drängt sich die Frage auf, wie denn der Absturz bei folgenden Szenario aussieht:

Die Datei enthält ein selbst ein "load"-Command für eine Datei die ein "load"-Command enthält, welche wiederum ein "load"-Command enthält ...

Es erfolgt kein Absturz – jedenfalls nicht wegen verschachtelter "load"-Commands. Aleph benutzt diese Technik bereits beim Aufbau der vorhandenen GUI. Aber Aleph geht noch weiter. Programme können auch in String-Objekten vorhanden sein. Diese Strings werden von Aleph ganz normal behandelt, ihr Inhalt kann aber, ähnlich wie der einer Textdatei, ausgeführt werden. Mehr dazu im nächsten Abschnitt. Jetzt zeigt sich auch, warum es unter Aleph sinnlos ist compilierte Programme zu speichern.

Eingabestrom selbst lesen

[Bearbeiten]

Zeichenfolgen aus dem Eingabestrom können auch von den Commands selbst konsumiert werden. Das entsprechende Command lautet "word". Dieses Command liest die nächste Zeichenfolge ohne white spaces und legt sie als String auf dem Stack ab. War der Lesevorgang erfolgreich (war da was), wird zusätzlich true abgelegt. Gab es nichts zu lesen, wird nur false abgelegt.

Eingabe:

word 1234 .S word

Ausgabe:

Stack = [ true "1234" ]

Um das false-Objekt des letzten "word"-Commands anzuzeigen: Clear-Button anklicken und

.S

eingeben. Die Ausgabe ist dann

Stack = [ true 1234 ]Stack = [ false true 1234 ]

Diese Command kann benutzt werden, um die postfix-Notation zu vermeiden. Als Beispiel sei hier Concatenation zweier Strings gezeigt.

Eingabe:

: concat
   word not if exit endif
   word not if exit endif
   +
  ;

concat eine_ Concatenation .

Das Command concat nimmt die beiden nächsten Zeichenfolgen aus dem Eingabestrom (der Eingabebereich im Fenster), legt sie als Strings auf dem Stack ab, fügt die beiden obersten Elemente (die gelesenen Zeichenketten) zusammen und legt den neuen String stattdessen auf den Stack. Sind keine Zeichenfolgen vorhanden, passiert nicht. Ist nur eine Zeichenfolge m Eingabestrom. ist diese als String auf dem Stack um eine Concatenation mit "Nichts" zu gewährleisten.

Diese Schreibweise erinnert schon eher an die gewohnten Aufrufe von Methoden. Die Argument (zwei Strings) stehen hinter dem funktionalen Teil (Command). So können auch Commands in postfix-Notation benutzt werden.

Commands gezielt ausühren

[Bearbeiten]

Alle Commands sind über das Dictionary erreichbar. Es liegt also nahe, ein Command gezielt über seinen Namen auszuwählen und dann zu starten. Diese Aufgabe soll ein Command namens "starte" übernehmen.

Eingabe:

: concat
   word not if exit endif
   word not if exit endif
   +
  ;

: starte
   word not if exit endif
   find not if exit endif
   execute
  ;

starte concat eine_ Concatenation .

Der Quellcode des Commands "starte" liest die nächste Zeichenfolge aus dem Eingabestrom. Der jetzt auf dem Stack liegende String wird mit "find" im Dictionary gesucht. Handelt es sich um ein Command, wird es mit "execute" ausgeführt.

Im Beispiel ist dieses Command eben "concat", das nun seinerseits zwei Zeichenfolgen aus dem Eingabestrom liest. So sind auch Funktionsaufrufe in einer postfix ähnlichen Notation sehr einfach zu realisieren.

Es handelt es sich natürlich nicht um echte postfix-Ausdrücke, da wären noch viele Klammern nötig, aber es sieht schon ähnlich aus. Ob Klammern tatsächlich immer einen Term schachteln oder nur m Sinne einer definierte Syntax einen Term schachteln müssen, zeigt sich bereits an diesem einfachen Beispiel.

Dictionary anzeigen

[Bearbeiten]

Das Dictionary enthält alle Commands. Es wird über commands "variable" und ":" erweitert und mit der "forget"-Anweisung verkleinert. Eine Auflistung des aktuellen Wortschatzes erfolgt über "commands". Dabei ist das zuerst ausgegebene Command als Letztes hinzugefügt.Es folgt damit ebenfalls der Ogranisation eines Stacks (LIFO = Last_In_First_Out). In dieser Reihenfolge werden die Commands ausgegeben. Auf ein Beispiel wird wegen Trivialität verzichtet.

Aufgaben

[Bearbeiten]

Zeit für ein paar Aufgaben. Sie werden nicht einfach, denn die noch ungewohnte Notation und die angestrebte Vermeidung von Variablen erfordern ein flexibleres Denken bei der Lösungsfindung.

Aufgabe:

Errechnen des Ausdrucks

  • Die Stacksituation ist: Stack = [ c b a ]. Die Reihenfolge der Eingabe war also a, b, c.
  • Eine gefundene Lösung lautet:
4.0 3.0 2.0

2 ndup   // c b a       --> a c b a
dup      // a c b a     --> a a c b a
*        // a a c b a   --> a*a c b a
swap     // a*a c b a   --> c a*a b a
2 ndup   // c a*a b a   --> b c a*a b a
*        // b c a*a b a --> b*c a*a b a
+        // b*c a*a b a --> b*c+a*a b a
1 ndrop  // b*c+a*a b a --> b*c+a*a a
swap     // b*c+a*a a   --> a b*c+a*a
/        // b*c+a*a a   --> (b*c+a*a)/a

. //Ausgabe von (4*4 + 3*2) / 4 = 5.5

Verbessern der angegebenen Lösung durch verkürzen der Sequenz ohne Verwendung von Variablen.

Die Lösung ist nicht sehr schwierig. Der Sinn dieser Aufgabe besteht mehr in einer Übung zur Stackmanipulation als in der Berechnung. Ähnliche Problemstellungen ergeben sich aber häufiger im Zusammenhang mit multiplen Instanziierungen von Objekten ein und derselben Klasse.

Die nächste Aufgabe ist denn auch etwas weitgehender.

Aufgabe:

Erläutern der gefundenen Lösung.

  1. Kann eine Änderung in der Eingabe der Operanden die Sequenz verkürzen?
  2. Kann die Verwendung von Variablen die Sequenz verkürzen?
  3. Ist eine Aufteilung der Sequenz in mehrere Commands sinnvoll?

Zusammenfassung

[Bearbeiten]

Dieser Abschnitt legte die Grundlagen für die Programmierung mit Aleph. Folgende Kenntnisse sollten jetzt vorhanden sein:

  • Verwendung der postfix-Notation
  • Einfache Manipulation des Stacks.
  • Aufbau neuer Commands mit dem Colon-Compiler.
  • Einsatz von Variablen zum speichern und als Command.
  • Laden von Programmen/Sequenzen aus Dateien.

Das Verständnis der internen Abläufe ist vorhanden. Die Programmierung umfangreicher Anwendungen kann beginnen. Der Einsatz von Elementen aus Java (Klassen, Objekte, Methoden) wird im nächsten Abschnitt gezeigt. Es sollten grundsätzliche Kenntnisse in Sprachen wie Java oder C/C++ vorhanden sein. Wird nur Basic, Javascript, Pascal beherrscht, sind Probleme im Verstehen der Programme wahrscheinlich. Trotzdem können diese bewältigt werden, es dauert aber länger.