Funktionen (SUB und FUNCTION)


Viele, die schon gut programmieren und sich diese Einführung anschauen, werden es unmöglich finden, dass hier das Prinzip der Funktionen erst im so ca. 25. Kapitel kommt. Das Prinzip wird meist als so elementar betrachtet, dass es in den meisten anderen Einführungen gleich in der ersten Hälfte des ersten Kapitels gebracht wird.

Nun, dass diese Einführung nicht ist wie andere, darauf wird schon im ersten Kapitel verwiesen. Eine Besonderheit mag dabei die Nutzung des folgenden Prinzips sein: Leiden ist ein guter Lehrmeister. Den Sinn und den Verwendungszweck eines Werkzeugs lernt man dann besonders gut, wenn man es vermisst. Der Schweiss ist es, der in Erinnerung bleibt ;-)))

Namensräume

Im folgenden wird es um eine Variante zum GOSUB-Befehl gehen. Wir haben ja schon gelernt, dass es ein sehr wichtiges Prinzip ist, sein grosses Programmierproblem in lauter kleine, möglichst allgemeingültige Programmierprobleme zu zerlegen. Das ist eine der grössten Künste des Programmierens und noch dazu eine, die sehr Spass macht, wenn man sie beherrscht. Denn dann kann man all die kleinen Lösungen, die man in den Monaten und Jahren programmiert hat, immer wieder benutzen. Es entstehen immer mehr kleine Unterprogramme, die z.B. Schlüsselwörter in Strings suchen, sie suchen und ersetzen, sortieren, Teilstrings auf ganz bestimmte Art und Weisen in Zahlen umwandeln, Strings formatieren usw.. Genauso mit Grafik, Dateien u.v.m.

Wenn Sie versucht haben, so ausführlich Unterprogramme zu nutzen, werden Sie allerdings immer wieder auf ein grosses und ärgerliches Hindernis gestossen sein: Die Variablen. Wenn man sich angewöhnt, wie in der Informatik üblich, für Schleifen z.B. die Variablen I, J und K herzunehmen - in Unterprogrammen geht das nicht. Denn meist sind die gleichen Variablen ja schon im Hauptprogramm verwendet worden. Also muss man dann im Unterprogramm I1, J1 und K1 oder sowas nehmen. Aber dann ruft das Unterprogramm noch ein Unterprogramm auf. Man denkt dabei nicht daran, dass dieses ja auch I1, J1 usw. verwendet - und schon kracht's. Und man begreift erst mal gar nicht, warum. Denn es kommt ja keine Fehlermeldung.

In der Informatik nennt man so etwas eine "Name Space Pollution", eine Namensraumverschmutzung. Bisher wird das Problem der Namensraumverschmutzung so gross gewesen sein, dass eine sinnvolle Wiederverwendung von Unterprogrammen nur dann möglich war, wenn man bei jedem Programmierprojekt an alle Unterprogramme Hand anlegt und ihre Variablennamen mühsam so hinbiegt, dass sie sich in der aktuell verwendeten Kombination nicht überschneiden - was für ein Umstand!

Das wird jetzt besser. Und dazu gibt es in QBASIC SUB's. SUB's sind Unterprogramme, innerhalb der Variablennamen ihre eigene Bedeutung haben. Eine Variable I innerhalb des SUB hallo1 ist eine andere Variable, als das I innerhalb des SUB hallo2 und alle beide sind andere Variablen, als das I innerhalb des Hauptprogramms. Und das gilt für alle anderen Variablen genauso. Ein Variablenname ist also jetzt nicht mehr eins zu eins mit einem Speicherplatz verknüpft, sondern er verweist auf verschiedene Speicher, je nachdem, in welchen SUB oder Hauptprogramm wir uns befinden. Wozu das gut ist, brauche ich nun gewiss nicht mehr zu erklären! Wir sagen zu so etwas auch: Der Raum innerhalb eines SUB's ist ein Namensraum. Und wir lernen jetzt, dass QBASIC offenbar mehrere Namensräume verwalten kann.

Allgemein nennen wir solche Unterprogramme mit eigenen Namensräumen Funktionen oder Methoden oder (wie schon früher gelernt) Routinen. Wir werden den Terminus "Funktion" beibehalten, für SUBs und auch für die gleich noch einzuführenden FUNCTIONs. "Funktionen" im informatischen Sinn haben also nicht unbedingt einen Rückgabewert wie die Sinusfunktion oder Wurzelfunktion.

Navigationsprobleme

Schreiben Sie einmal in ein neues Programm folgende Anweisungen:

SUB hallo1

Sobald Sie die Zeile mit einem ENTER abschliessen, ergänzt QBASIC von selbst eine weitere Zeile:

SUB hallo1
END SUB

Überhaupt übernimmt QBASIC jetzt ziemlich energisch die Regie, was geschrieben werden darf und was nicht. Wenn Sie versuchen, vor das SUB eine Zeile einzufügen, macht QBASIC sofort daraus einen Kommentar. Wenn Sie versuchen, nach dem END SUB noch etwas einzufügen, dann protestiert QBASIC und verbietet Ihnen das glatt. Was ist denn da los?

Stillschweigend hat QBASIC hier ein neues Fenster innerhalb der Entwicklungsumgebung aufgemacht. Sie sehen das, wenn Sie F2 drücken oder auf Ansicht->SUBs gehen. Da sehen Sie eine Liste der Routinen, aus denen das Programm besteht. Und Sie sehen, dass das schon zwei sind: hallo1 und "Unbenannt", das Hauptprogramm, das den Dateinamen trägt. Wenn Sie "Unbenannt" auswählen, dann landen Sie wieder im Hauptprogramm und damit vor einem leeren Bildschirm. Hier ist alles so wie es schon immer war.

Das Konzept ist also, für jedes Unterprogramm, jedes SUB ein eigenes Fenster aufzumachen. Daher das Schreibverbot: Vor und nach dem SUB machen im SUB-Fenster Anweisungen keinen Sinn.

Übrigens ist diese Art, SUB's im eigenen Fenster zu verwalten, eine Spezialität von QBASIC. In anderen BASIC-Versionen und Entwicklungsumgebungen schreibt man die Funktionen ins selbe Fenster wie das Hauptprogramm.

Wir testen SUB's

Wir füllen nun mal unser neues SUB hallo1 mit Inhalt. Und das Hauptprogramm auch:


Hauptprogramm:
---------------
DIM i AS INTEGER

FOR i = 0 TO 3
  PRINT "main", i
  PRINT "----------------------"
  hallo1
NEXT i

Sub's:
----------------
SUB hallo1

  DIM i AS INTEGER

  FOR i = 0 TO 3
    PRINT "hallo1", i
  NEXT i

END SUB

Hier sehen wir, wie ein SUB aufgerufen wird. Nämlich einfach mit seinem Namen. Ohne irgendein Schlüsselwort davor. Als wenn das SUB ein eigener Befehl von QBASIC wäre. Und genauso sind Funktionen (so nennt man SUB's im allgemeinen) auch gedacht: Sie sollen den Befehlssatz einer Programmiersprache erweitern. Durch den eigenen Namensraum ist das jetzt auch möglich: Man kann einen SUB aufrufen, ohne sich darum zu kümmern, was innerhalb des SUB's passiert. Aber ist das auch so? Schauen wir uns die Ausgabe an:

Die Ausgabe:

main           0
------------------
hallo1         0
hallo1         1
hallo1         2
hallo1         3
main           1
------------------
hallo1         0
hallo1         1
hallo1         2
hallo1         3
main           2
------------------
hallo1         0
hallo1         1
hallo1         2
hallo1         3
main           3
------------------
hallo1         0
hallo1         1
hallo1         2
hallo1         3

Hat funktioniert. Wir haben im Hauptprogramm die Schleifenvariable I verwendet, ohne uns darum zu kümmern, dass I in hallo1 auch verwendet wird. Aber das macht nichts, da in hallo1 I eine andere Variable darstellt. Der Wert vom I des Hauptprogramms bleibt dabei erhalten: Nach dem ersten Aufruf von hallo1 fährt das Hauptprogramm ganz brav mit I=1 fort.

Übung:

Schreiben Sie das gleiche Testprogramm mit GOSUB's und vergleichen Sie das Ergebnis!

Kein GOSUB mehr

Ab jetzt empfehle ich Ihnen, kein GOSUB mehr benutzen. Auch innerhalb von SUB's nicht. Wenn Sie sich das Problem von Namensraumkonflikten unbedingt weiter ans Bein binden wollen, können Sie natürlich weiter daran festhalten, aber da selbst das kleinste Unterprogramm meistens Variablen verwendet, werden Sie dann auch weiterhin viel Ärger haben...

Argumente

Nun haben wir aber ein Problem. Innerhalb des SUB's können wir Variablen anderer SUB's oder des Hauptprogramms nicht sehen. Nehmen wir an, wir wollen dem SUB vom Hauptprogramm mitteilen: "Vertausche die Reihenfolge der Zeichen des folgenden Strings!" Wie "geben" wir dem SUB den String?

Dazu gibt es Argumente. Die schreibt man in Klammern hinter den SUB-Namen:

SUB vertausch (s AS STRING)
  DIM slen AS INTEGER, i AS INTEGER, s1 AS STRING

  slen = LEN(s)
  FOR i = 1 TO slen
    s1 = s1 + MID$(s, slen - i + 1, 1)
  NEXT i

  PRINT s1

END SUB

Diese Funktion nimmt den übergebenen String s, vertauscht die Reihenfolge der Zeichen und gibt das Resultat auf dem Bildschirm aus. Aufrufen können wir sie im Hauptprogramm mit

vertausch 
also z.B.
vertausch "qbasic"

Rückgabe von Werten

So ist unsere Funktion "vertausch" allerdings noch nicht schön. Wer braucht schon eine Funktion, die die Zeichenreihenfolge vertauscht und dann sofort das Resultat auf dem Bildschirm ausgibt? Meistens werden wir den invertierten String irgendwie weiterverwenden wollen. Aber dazu müsste das SUB in der Lage sein, das Resultat wieder an uns zurückzugeben.

Kein Problem. Dazu tauschen wir das Schlüsselwort SUB gegen FUNCTION aus. Und verpassen dem Funktionsnamen selbst einen Datentyp, wie wenn es eine normale Variable wäre. Da in diesem Fall die Funktion einen String zurückgeben soll, ist die Funktion vom Typ String:

FUNCTION vertausch(s AS STRING) AS STRING
(...)
END FUNCTION

Jetzt ist nur noch die Frage, wie der Rückgabewert definiert wird. Nun, logisch wäre es, das ganz genauso wie bei einer Variablen zu machen:

' So geht's in QBASIC leider nicht
FUNCTION vertausch(s AS STRING) AS STRING
(...)
vertausch=...
END FUNCTION

Hier ist QBASIC unkonsequent und lässt nur eine Typdeklaration mittels der Spezifizierer ("Spezialzeichen") zu:

' So geht's in QBASIC
FUNCTION vertausch$(s AS STRING)
(...)
vertausch$=...
END FUNCTION

In neueren BASIC-Versionen funktioniert aber auch die obere, logischere Syntax.

Im konkreten Beispiel:

FUNCTION vertausch$(s AS STRING)
  DIM slen AS INTEGER, i AS INTEGER, s1 AS STRING

  slen = LEN(s)
  FOR i = 1 TO slen
    s1 = s1 + MID$(s, slen - i + 1, 1)
  NEXT i

  vertausch$=s1

END FUNCTION

Im Hauptprogramm verwenden wir die Funktion genauso wie eine der eingebauten Funktionen (sin() oder asc()), allerdings ohne die Klammern um die Argumente. Das mag QBASIC nicht:

Hauptprogramm:
---------------
DIM s AS STRING

s = vertausch$("QBASIC")
PRINT s

Schauen Sie sich einmal das Hauptprogramm an! Ist das nicht prima lesbar und verständlich? Jeder weiss, was dieses Hauptprogramm macht! Und das ist das Geheimnis, warum es in der strukturierten Programmierung möglich wird, riesige Programme zu erstellen: Weil dank der Strukturierung in Funktionen und dank der Reduzierung der Menge an Variablennamen, mit der hantiert wird, die Programme übersichtlich und lesbar bleiben. In unserem Hauptprogramm hätten wir sogar auf das s verzichten können:

Hauptprogramm:
---------------
PRINT vertausch$("QBASIC")

Allerdings will QBASIC bei FUNCTION's Klammern um die Argumente herum haben. Auch da ist kein so richtig einleuchtendes Prinzip erkennbar. QBASIC will, dass SUB's ohne Klammern um die Argumente aufgerufen werden und FUNCTION's mit Klammern, obwohl beides Funktionen sind, nur einmal mit und einmal ohne Rückgabewert. (Das ist ein typisches Beispiel für mangelnde informatische Stringenz in älteren BASIC-Implementierungen, die bei komplexeren Problemen die Gefahr hervorruft, dass die Programme "zerfasern", weil abstraktere, schönere Lösungen nicht möglich sind.)

Rückgabe von mehreren Werten, gemeinsame Variablen

Was mache ich aber, wenn mehrere Werte zurückgegeben werden sollen? Meinetwegen erzeugt die Funktion zwei Koordinaten x und y. Wie gebe ich diese wieder zurück? Nun, da Funktionen (in QBASIC) nur einzelne Zahlen oder höchstens einen String zurückliefern können, müssen wir einen anderen Weg gehen. Wir schreiben einmal folgendes Testprogramm:

SUB testsub(x AS integer)
  x=1
END SUB

'Hauptprogramm
x=0
CALL testsub(x)
PRINT x

Was wird ausgegeben? Eine Eins. Es ist also so, dass Argumente von der Funktion (dem SUB) nicht nur gelesen, sondern auch geschrieben werden können. Und Änderungen bleiben in der aufrufenden Funktion wirksam. Mit anderen Worten: Argumente sind Variablen, die Aufrufer und Funktion gemeinsam verwalten. Man nennt solche Argumente "By Reference"-Argumente.

Eigentlich sind "By Reference"-Argumente nicht so gut, da sie das Prinzip der getrennten Namensräume zwischen den Funktionen aufweichen und daher wieder Fehleranfälligkeit mit sich bringen. Da aber QBASIC einige Beschränkungen im Bereich "Strukturierte Programmierung" aufweist, sind wir in diesem Fall ganz froh um dieses "Feature".

Noch ein Beispiel: Sollen Hauptprogramm und das SUB testsub() die Variable n gemeinsam verwalten, dann übergibt man sie einfach testsub() mit: SUB testsub(x as integer, n as integer). Dabei ist x das eigentliche Argument und n eben nur die gemeinsame Variable, die z.B. einen Programmstatus oder ein Limit enthalten kann. Will man zudem noch einen Wert zurückgeben, dann kann man entweder daraus eine FUNCTION machen (bei nur einem Rückgabewert empfehlenswert) oder der SUB noch ein drittes Argument mit auf den Weg geben SUB testsub(x as integer, n as integer, returnvalue as integer).

Goldene Kommentarregel:

Jedes SUB sollte gleich hinter der Kopfzeile einen Kommentar mit folgenden Angaben enthalten:

Deklaration von Funktionen (DECLARE)

Ihnen wird wahrscheinlich schon aufgefallen sein, dass QBASIC eigenmächtig auch im Hauptprogramm aktiv wird, sobald sie das Programm speichern: Es fügt Zeilen der Form

DECLARE Subname (Argumente)

hinzu. Warum macht es das? Nun, das hat technische Gründe. Der Intepreter muss von Anfang an, also schon bei der ersten intepretierten Programmzeile, wissen, welche Funktionen es im Programm gibt und wie sie aufgerufen werden. Dazu dienen die DECLARE's. Der Interpreter startet also mit den DECLARE's, erst dann geht er zur ersten Zeile des Hauptprogramms. Für Sie als Programmierer haben die DECLARE's eine kleine Falle parat: Falls die DECLARE's schon eingefügt wurden und Sie im Nachhinein einen Funktionskopf noch einmal ändern, dann meckert QBASIC: Falsche Anzahl der Argumente (oder etwas ähnliches). Sie müssen die Änderung immer an zwei Stellen durchführen, bei der Deklaration, also dem DECLARE, und bei der Definition im SUB-Fenster.

%%1