Ein Schnellkurs in Perl

Lieber NPN (Noch Perl-Newbie :-),

Dies ist ein Schnellkurs in Perl mit Anmerkungen. Ich gehe in den roten Textpassagen meinen Text von April 1997 (schwarz gedruckt) noch einmal durch und überprüfe nun -- ein Jahr später --, ob er wirklich eine "völlig unverständliche kryptische Skriptsprache" beschreibt, die defacto nicht erlernbar ist, oder ob eher meine Erklärungen verbesserungswürdig sind. Man muß eigentlich nur eins wissen:

Der Wert, für den Bezeichner stehen, ist abhängig von einem "Kontext". Der Kontext wird durch ein sog. "lustiges Zeichen" am Bezeichner markiert:

$a = 4;      # $a ist nun 4
@a = 5;      # das 1. Element von @a ist nun 5

Ok, das war schon mal Quatsch. Gebe ich zu. Der Begriff "Kontext" ist hier albern. Sagen wir einfach $a, @a und &a seien völlig bescheuerte Variablennamen, deren Typ (Skalar, Array, Funktion) man am ersten Zeichen erkennt.

Jeder Kontext hat seinen eigenen Namensraum:

print $a, @a; # gibt 45 aus ($a und @a sind verschiedene Variablen)

Das überrascht uns jetzt irgendwie nicht, da wir die lustigen Anfangszeichen ja nun als Bestandteil des Variablennamens sehen. Aber "Kontext" und "Namensraum" klingt eben irgendwie besser.

Wichtig ist der folgende Unterschied:

print $a[0], @a[0]; # gibt 55 aus, nicht 45 (beides bezieht sich auf @a)

Aber hier geht es nun schon los: Wie erklären wir nun, daß $a[0] das 1. Element von @a ist? Sagen wir einfach, daß ja klar ist, daß @a gemeint ist, weil man das ja schließlich am [0] sieht. Und da ein Element eines Arrays normalerweise ein Skalar ist, schreiben wir eben ein $ vorne weg. Das "$", das wie ein "S" aussieht, bedeute eben, daß das, was hier insgesamt steht, ein Skalar sei.

Es gibt eine Listennotation und auch Listenzuweisung:

($a, @a) = (1, 2); # $a ist nun 1 und $a[0] ist 2
print $a, @a;      # gibt 12 aus

<Ironie>Das war nun wahnsinnig schwer zu verstehen.</Ironie> Blöd ist allenfalls das Wort "Listenzuweisung". Wo wird hier denn eine Liste zugewiesen? Richtig ist, daß (1, 2) eine Liste ist und daß die Elemente der rechten Liste nach und nach den Elementen der linken Liste zugewiesen werden. (Genau genommen natürlich gleichzeitig, d.h. "($a, $b) = ($b, $a)" vertauscht die Inhalte von $a und $b.)

Der Kontext ist bei Listenzuweisungen besonders wichtig:

(@a, $a) = ($a, @a); # @a ist nun (1, 2) und $a ist undefiniert
print $a, @a;        # gibt wieder 12 aus, nicht 21

(Zuweisungen an Arrays sind selbst Listenkontext, d.h. bei der Teilzuweisung an @a innerhalb der gesamten Listenzuweisung wird die ganze rechte Liste verbraucht, so daß für $a nichts mehr übrig bleibt.)

Ist das wirklich unverständlicher kryptischer Schwachsinn? Irreführend ist hier allenfalls wieder das Wort "Kontext". Was ist "Listenkontext"? Das hatten wir noch nicht. Im Grunde gibt es das auch gar nicht. Sagen wir einfach, daß es auch Listen gar nicht gibt. Nennen wir es ein Hinschreibeverfahren für "ich will mehrere Werte zuweisen". Und warum soll ein Array nicht mehrere der rechts vom "=" stehenden Werte kriegen? Er pickt sich also aus der linken Liste das erste Element, sieht, daß es ein Array ist, und weist sämtliche Werte der rechten Liste zu, da es keinen Grund gibt, das nicht zu tun. $a kriegt dann eben nichts mehr ab. Die "12" der Ausgabe sind dann die Elemente "1" und "2" von @a in dieser Reihenfolge. Die Ausgabe von $a sieht man ja nicht. Oder wie soll undefiniert sonst aussehen?

Arrays interpolieren flach in Listen:

($a, @a) = (@a, $a); # $a ist nun wieder 1 und $a[0] ist wieder 2
print $a, @a;        # gibt zum dritten Mal 12 aus

Hier haben wir nun wieder erbarmungslos zugeschlagen: "Arrays interpolieren flach in Listen". Du hast schon recht, lieber NPN, an dieser Stelle beendet man das Studium von Perl, denn mit so unverständlichen Sätzen darf man sich gar nicht erst beschäftigen. Das hat man schlichtweg nicht nötig so einen Quatsch.

Gemeint ist aber doch bloß, daß rechts jetzt nicht "( (1, 2), undefiniert)" steht, sondern "(1, 2)". Wie gesagt, wir behaupten im roten Text ja nun, daß es Listen gar nicht gibt. Wenn ich also so einen Unfug wie "(@a, $a)" schreibe, und @a in dem Moment für "(1, 2)" und $a für überhaupt nix steht (undefiniert), dann kann ich also nur gemeint haben "(1, 2)". Damit wird $a nun 1 und @a (also $a[0]) nun wieder 2 zugewiesen.

Perl sucht sich eben immer was, was wohl gemeint sein könnte und das gelegentlich doch in sogar nachvollziehbarer Art und Weise. Man sollte sich in diesem konkreten Beispiel eben im Hinterkopf das ständige Bewußtsein darüber bewahren, daß Listen keine Datenstrukturen sind, sondern Hinschreibeverfahren. Arrays hingegen sind Datenstrukturen. Und damit das schön einfach ist, dürfen wir uns sogar merken: Es gibt keine Arrays von irgendwas anderem als von Skalaren. Und ein Array ist eben kein Skalar (das ist ja gerade der Unterschied), also gibt es auch keine Arrays von Arrays oder mehrdimensionale Arrays. Ist doch prima! (Jedenfalls schön einfach. -- Bis jetzt.)

Mit Interpolation (normalerweise: "das nachträgliche Einfügen oder Ändern in Texten") ist hier das Ersetzen der Variablen durch ihren Wert gemeint. (Dieser Wert kann ein lvalue sein, also etwas, an das man zuweisen kann.) "flach" steht für "identitätsverlustig", d.h. für die potentielle Interpolation einer Variablen in mehrere Werte.

Haben wir das jetzt verstanden? Egal. Trotzdem nochmal: Wenn @a gerade "(1, 2)" ist, also ein Array mit den Elementen $a[0]==1 und $a[1] == 2 (was wir als "(1, 2)" hinschreiben), dann ist (1, 2, 3, @a, 4, 5, 6) die Liste (1, 2, 3, 1, 2, 4, 5, 6). Man könnte auch sagen, daß die Elemente des Arrays @a (eine Datenstruktur) beim Hinschreiben in eine Listennotation zu einem Teil der Liste werden. Nicht das Array wird also zu einem Element der Liste, sondern die Elemente des Arrays. Es wird quasi ausgepackt. Damit ist natürlich auch (1, (2, 3)) das Gleiche wie (1, 2, 3).

Referenzen auf Werte (egal of auf Arrays oder Skalare) sind Skalare:

($b, $c) = (\$a, \@a); # $c ist nun eine Referenz auf @a

Sieh, das ist doch schon mal schön. Es gibt also sowas wie Pointer. Und diese Pointer sind auch noch Skalare, d.h. auf diese Weise könnten wir doch mehrdimensionale Arrays machen. (Wir sehen das weiter unten.)

Das @a interpoliert hier nicht (deswegen die Syntax mit dem '\' bei Referenzen).

Aua. Was hatten wir denn da geschrieben? Gemeint war aber bloß das, was direkt im Anschluß auch erklärt wurde:

Der Ausdruck "\@a" denotiert einen Verweis auf den Wert von @a. Bei der Auswertung von Ausdrücken ('\' ist kein lustiges Zeichen) spricht man nicht von Interpolation. Der Kontext, in den Referenzen dereferenziert werden sollen, wird aber wieder durch lustige Zeichen bestimmt:

print $$b, $$c[0]; # gibt schon wieder 12 aus (denn $a ist 1 und $a[0] ist 2)
($$b, @$c) = (4, 5, 6);
print $a, @a; # gibt 456 aus (denn @$c interpoliert hier wie @a)

Oje oje, meine Güte, ach Du Schreck. Ein "Verweis auf den Wert von @a". Grundgütiger! Was für ein Wert ist das denn? Ich dachte, das wären mehrere? Und hinschreiben könne man diese nur in Listennotation, aber nicht als Datenstruktur! Formulieren wir also besser so: Das \@a ist der Wert (also ein Skalar), der das Array als ein Ding als Solches bezeichnet. Und das $c aus der Zuweisung oben (die Variable, die Skalare speichert) hat nun diesen Wert.

Also: $$b bedeutet nun, daß $b, also der Wert \$a dereferenziert werden soll. Damit ergibt sich der Wert von $a, also 1. Wieso benutzt man zum Dereferenzieren aber wieder ein $-Zeichen? Und wieso sagt man, daß "in skalaren Kontext" dereferenziert wird? Was soll immer dieser Quatsch mit dem Wort "Kontext"?

Nun, hätten wir diesen Text damals (vor einem Jahr) nicht selbst geschrieben, würden wir hier vielleicht wirklich nicht weiterlesen und doch noch versuchen, das zu verstehen bzw. zu erklären: Wir sehen in der zweiten Zeile des letzten Beispiels, die Teilzuweisung "@$c = (5, 6)". Sehen wir doch, oder? (Im Ernst, das müssen wir nun wirklich sehen!) Hier wird also "in Listenkontext hinein dereferenziert"? Aua, aua. Oder "in Arraykontext hinein dereferenziert"? Gibt's das? Und was soll das sein? Und was denn nun? Liste, Array?

Nun, $c ist die Variable, die den Wert \@a hat. Das wissen wir schon. Und noch ein weiteres "lustiges Zeichen" davor (also sowas wie $ oder @ oder & -- diese drei kennen wir schon) dereferenziert. Damit steht @$c also für @a, d.h. @a wird == (5, 6). Das wäre aber doch nicht anders, wenn wir $$c schreiben würden, oder? Doch: Wir kriegen dann eine "Not a SCALAR reference"-Fehlermeldung. Sehr praktisch.

Der Unterschied zwischen @a und @a[0] sei noch einmal verdeutlicht:

@a[0] = (1, 2, 3); # $a[0] wird auf 1 gesetzt, $a[1] nicht angetastet
print @a;          # gibt 16 aus (@a hatte vorher den Wert (5, 6))

Verstehen wir. Bei "@a = (1, 2, 3)" wäre 123, nicht 16 ausgegeben worden.

Wesentlich wird hier auch der Unterschied zwischen @a[0] und $a[0]:

$a[0] = (1, 2, 3); # $a[0] wird auf den letzten Wert der Liste gesetzt
print @a;          # gibt 36 aus, nicht 16

(Bei Zuweisung an einen Skalar gilt ein Kontext, in dem das ',' die klassische C-Semantik hat.)

Ach was. Ich glaube, wir führen nun doch besser die Begriffe "skalarer Kontext" und "Listenkontext" ein. Damit wollen wir aber nicht den Unterschied zwischen $a und @a meinen (@a ist ja auch ein Array, keine Liste), sondern die Unterscheidung zwischen Singular- und Plural-Modus bei der Semantik von Perl-Notationen. Wir legen fest, daß bei Zuweisung an einen Skalar skalarer Kontext gelten möge und bei Zuweisung an ein Array Listenkontext. Listenkontext heiße also, daß im Plural-Modus mehrere Werte bearbeitet werden, und wir können definieren, was die Verwendung von Listen in skalarem Kontext bedeuten soll. In Perl ist es eben so, daß dann das letzte Element der Liste gemeint ist.

Also nochmal: "@a[0] = (1, 2, 3)" nimmt soviel Elemente aus der Liste (1, 2, 3), bis der Indexbereich von 0 bis 0 des Arrays @a voll ist, also nur die 1. "$a[0] = (1, 2, 3)" setzt das 1. Element des (selben) Arrays @a hingegen auf einen Wert, also bei Angabe einer Liste auf den letzten und damit auf die 3. Und da haben wir es: Der Kontext (skalarer oder Listenkontext) wird nicht dadurch bestimmt, daß wir an ein Array zuweisen (es ist hier ja in beiden Fällen @a betroffen), sondern durch das lustige Zeichen, das wir verwenden! So also war es zu verstehen, was wir im schwarzen Text geschrieben hatten, daß der Kontext in Perl durch lustige Zeichen bestimmt wird.

Man sieht hier auch deutlich, daß Arrays und Listen völlig verschiedene Dinge sind:

$a[0] = @a; # @a in skalarem Kontext bedeutet die Anzahl der Elemente
print @a;   # gibt 26 aus, nicht 36

(In der Zuweisung ist @a hier skalar interpoliert: es hat zwei Elemente. "print" ist hingegen ist ein Listenoperator, also interpoliert @a als Liste.)

Mir wird hier immer noch zu sehr mit Vokabeln geworfen, insbesondere mischen wir hier plötzlich "skalar" und "interpoliert". Das ist aber völliger Humbug. Fakt ist: 1. Die Zuweisung an einen Skalar ist skalarer Kontext. 2. Ein Array in skalarem Kontext meint die Anzahl seiner Elemente. Punkt. Erinnern wir uns allerdings, daß wir das Wort "interpolieren" als das "Textuelle Rumersetzen im Programmcode" verstehen wollten (so hatten wir das oben im schrarzen Text ausführlich eingeführt), dann ist die Formulierung im letzten schwarzen Absatz auch gar nicht mehr unverständlich: Mal wird ein z.B. "(1, 3)" bezeichnendes "@a" eben durch "(1, 3)" ersetzt (dort, wo mehrere Werte möglich sind -- eben Listenkontext), und mal durch "2" (wo nur ein Wert möglich ist -- Anzahl der Elemente).

Wohlbemerkt: Ein Array in skalarem Kontext ist die Anzahl der Elemente. Eine Liste in skalarem Kontext hingegen ist das letzte Element. Das, und die Tatsache, daß es Listen in Wirklichkeit gar nicht gibt (sie sind nur Notation, keine Datenstruktur), sind zwei der "Unterschiede".

Skalarer Kontext gilt auch in Listen, wenn die Liste in skalarem Kontext ist:

$a = (@a, 1); # letztes Element ist 1
$b = (1, @a); # letztes Element ist 2, nicht 6
print $a, $b; # gibt wieder mal 12 aus

Hier ist das @a in der ersten Zuweisung durch 2 ersetzt worden, weil die Liste, in der es vorkam, in skalarem Kontext stand.

Man hüte sich also davor, diese beiden Zuweisungen so zusammenzufassen:

($a, $b) = ( (@a, 1), (1, @a) );
print $a, $b; # gibt wieder 26 aus, nicht 12 (kein skalarer Kontext)

Hier standen beide @a in der ersten Zuweisung in Listenkontext, weil an eine Liste zugewiesen wurde. Es stand dort also "($a, $b) = (2, 6, 1, 1, 2, 6)".

Referenzen auf Listen gelten als Listen von Referenzen. Referenzen auf Arrays werden mit [] erzeugt:

@c = ( \($a, $b), [$a, $b] ); # Liste von drei Referenzen
print ${ $c[2] }[1];          # gibt 6 aus (nämlich das hintere $b)

Hier ist also ein weiterer "Unterschied" zwischen Listen und Arrays. \(1, 2) sind zwei Skalare (eine Referenz auf 1 und eine auf 2), also das Gleiche wie (\1, \2). [1, 2] ist hingegen ein Wert, nämlich eine Referenz auf übrigens ein Array.

Syntaktisch kann einem lustigen Zeichen neben einem Bezeichner oder einem Skalar also auch ein Block folgen (aber kein geklammerter Ausdruck, denn lustige Zeichen sind keine Operatoren).

Anders ausgedrückt: Statt $$c hätte ganz oben damals auch ${$c} geschrieben werden dürfen. Ist das Innere ein indiziertes @c, ist dieser Block notwendig, weil $$c[2] ja was anderes bedeutet als das ${$c[2]}, das in diesem Beispiel erforderlich war.

Mit Referenzen kann man SCHEMEige (also verschachtelte) Listen so bilden:

$l = [1, [2, [3, []]]];
print $$l[0], ${ $$l[1] }[0]; # endlich wieder eine 12

Weil das unlesbar ist, gibt es den "->"-Operator, der eine $-Dereferenzierung impliziert:

print $l->[0], $l->[1]->[0]; # nochmal dieselbe 12

Zwischen Subskript-Operatoren sind "->"e optional (ggf. implizit). Dem SCHEMEigen "(list (car l) (cadr l) (caddr l))" entspricht also:

@a = ( $l->[0], $l->[1][0], $l->[1][1][0] );

Da [] eine Referenz auf ein Array erzeugt, gilt darin Listenkontext:

$a = [ (0, @a) ]; # @a interpoliert als Liste (erzeugte Liste hat 4 Elemente)
$b =   (0, @a);   # @a interpoliert skalar (erzeugte Liste hat 2 Elemente)

print $a->[1], $a->[2], $b; # gibt 123 aus, nicht 33

Wenn Dich das noch überrascht, lieber NPN, oder wenn Du das als "Feature" siehst, dann lies noch mal bis hier alles durch. Wie man eine verschachtelte Liste à la (1, [2, []], 3) in zwecks Ausgabe in einen String konvertiert, sehen wir übrigens weiter unten.

Runde Klammern werden bevorzugt als Parameterliste interpretiert. Dagegen hilft z.B. ein "+", das alleine noch keinen skalaren Kontext erzwingt:

print     (0, @a), @a; # gibt 0123 aus
print   + (0, @a), @a; # gibt 0123123 aus
print 0 + (0, @a), @a; # gibt 3123 aus

Dies ist eine der Stellen, wo Perl wirklich zum Ablachen ist. Aber logisch ist es! Schließlich muß ein '+', das irgendwo rumsteht, ja auch irgendeine Bedeutung zugewiesen bekommen. Und die Bedeutung, die es hier hat, ist eben, daß es keine hat -- außer daß so natürlich keine Klammern mehr am "print" sind. Und mit der '0' ist es ja wieder ein ganz anderes '+', nämlich die (skalare) Addition.

Arrays lassen sich auch wieder verkürzen. Negative Indizes zählen von hinten:

$#a--;
print $a[-1]; # gibt 2 aus (letztes Element)

Und natürlich verlängern (@a ist der Index des Elements hinter dem letzten):

$a[@a] = 9;
print @a;     # gibt 129 aus 

Aber Vorsicht. Hier ist das vordere lustige Zeichen mal wieder sehr wichtig:

@a[@a] = 9;
print @a;     # gibt 19 aus

(Denn bei Array-Slices gilt im Subskriptionsoperator natürlich Listenkontext, d.h. es werden hier $a[1] auf 9 und $a[2] auf undefiniert gesetzt.) Das Array hat jetzt 10 Elemente (denn auch $a[9] wird auf undefiniert gesetzt). $#a ist der Index des letzten:

print @a + $#a; # gibt auch 19 aus

Wie, Du bist schon wieder ausgestiegen? Das kann doch nicht sein. Was ist los mit Dir? Es stand da "@a[(1, 2, 9)] = (9, undefiniert, undefiniert)". Der höchste benutzte Index des Arrays ist nun 9, also hat es 10 Elemente! Daß unter den Indizes 2..9 gar nix zu finden ist, ist eine andere Geschichte. Es heißt nur, daß man diesen Bereich mit "$#a = 2;" wieder löschen kann, ohne irgendwas an nicht undefinierten Werten zu verlieren.

Die meisten Operatoren sind kontextüberladen: Z.B. ist 1..3 die Liste (1, 2, 3), aber $a[1..3] hat nicht die erwartete Bedeutung $a[3] (letztes Element der Liste wegen skalarem Kontext), sondern:

$a[1..3] = 7;
print @a; # gibt 79 aus

(Denn .. ist in skalarem Kontext ein bistabiles Flip-Flop, d.h. 1..3 ist 0.)

Dazu müssen zwei Dinge ergänzt werden: 1. '..' ist ein Flip-Flop wie in 'awk' und '...' eines wie in 'sed'. (Das war jetzt nur für UNIX-Leute interessant.) Hier wichtig ist aber 2.: Anders als $a[1..3] hat aber @a[1..3] tatsächlich die Newbie-verständliche Bedeutung: Es ist das Teilarray von Index 1 bis 3.

Ein weiteres Beispiel ist der x-Operator (Vervielfacher):

@a = (1) x 3; # die Liste (1, 1, 1)
@a =  @a x 3;
print @a;     # gibt 333 aus
print $#a;    # gibt 0 aus (@a hat also nur 1 Element!)

Man sieht hier, wie ein kontextüberladener Operator, auf eine Nicht-Liste angewendet (hier: ein Array), trotz äußerem Listenkontext (Zuweisung an @a) skalaren Kontext erzwingt, so daß @a als Elementanzahl 3 interpoliert (die dann, als String, verdreifacht wird).

Wir sind hier am Kern dieser ganzen Kontextgeschichte, daher die Frage: Du hast das doch verstanden, lieber NPN? Der Witz ist, daß die zweite Zeile aus (1, 1, 1) nicht etwa (3, 3, 3) macht. Dann würde die dritte Zeile zwar auch 333 ausgeben, die vierte jedoch 2 (Index des letzten Elements). Sie macht auch nicht (1, 1, 1, 1, 1, 1, 1, 1, 1), was wesentlich sinnvoller wäre (denn @a interpoliert in Listenkontext ja zur Liste), sondern es wird trotz äußerem Listenkontext (dirch die Zuweisung) vom x-Operator die Auswertung des @a in skalarem Kontext erzwungen (Anzahl der Elemente). Im Grunde ist dies auch am logischsten, denn schriebe man "@a x 3" als x(@a, 3), dann muß @a ja skalar interpolieren, schließlich verlangt x genau zwei Werte. Perl ist bei genauerer Betrachtung bis zum Abwinken logisch!

Es ist nicht ganz unwichtig, diese ganze Kontextgeschichte wirklich richtig zu verstehen: Der Begriff Array-Kontext meint lediglich den Namensraum des Bezeichners hinter dem lustigen Zeichen @. Steht ein $ vor dem Bezeichner, nennt man das skalaren Kontext. Der "Kontext" in Ausdrücken ist aber immer bloß die Unterscheidung von Einzahl oder Mehrzahl, wobei Einzahl auch "skalarer Kontext" genannt wird, und Mehrzahl "Listenkontext". Es gibt aber keinen Namensraum für Bezeichner im Listenkontext.

Zur besonderen Verwirrung heißt die Funktion, mit der man Listenkontext erkennt, "wantarray", nicht "wantlist":

sub t { wantarray ? 1 : 2 }
print &t, &t + 0; # gibt 12 aus

(Dies ist natürlich kein skurriler Humor der Perl-Designer, sondern ganz einfach Tradition. Arrays und Listen waren in Perl schließlich nicht schon immer grundsätzlich verschiedene Konzepte.)

Subroutinen sehen die Liste der Aktualparameter in "@_" (nicht deren Kopien), d.h. es handelt sich um Call-by-Reference. Bei Funktionsaufruf mit Parameterliste kann beim Aufruf das lustige Zeichen "&" weggelassen werden:

sub tt {
  my $m = 5;
  sub a { print $b, $m; $_[0] = 4; 6 };
  $m = a($b); # gibt 35 aus
  $m = a($b); # gibt 46 aus
}
tt(); # ($b ist von oben noch 3, aber hiernach 4)

Ergänzung: &a entspricht a(@_), nicht a(), denn @_ ist eine sog. local-Variable mit "dynamischer Semantik". (Wir erklären das weiter unten beim Beispiel mit dem Curryfizierer.)

my-Variablen sind mit lexikalischer Semantik lokal. Innere Subroutinen sind keine my-Variablen und nicht lokal:

sub ttt {
  my $m = 5;
  sub b { print $b, $m; $_[0] = 4; 6 };
}
ttt();
$m = b($b); # gibt 45 aus
$m = b($b); # gibt nochmal 45 aus (das my-$m verdeckt das globale)

Das innere &b ist also global verfügbar, greift aber trotzdem korrekt auf die my-Variablen des lexikalischen Umfelds seiner inneren Deklaration zu. (Dabei ist das @_ der umgebenden Funktion -- hier &ttt -- aber, wie schon gesagt, keine my-Variable, also nicht Bestandteil des lexikalischen Umfelds -- was sich in späteren Perl-Versionen aber natürlich mal ändern könnte!)

Anonyme Subroutinen sind Referenzen, also Skalare. (Hier wird das lustige Zeichen "&" zum Dereferenzieren benötigt, kann also nicht weggelassen werden.) Damit haben wir eine Entsprechung zum SCHEME-lambda:

# (define a (lambda (b) (write b) (if (> b 1) (a (- b 1)))))
$a = sub { my $b = $_[0]; print $b; if ($b > 1) { &$a($b - 1) } };

&$a(5); # gibt 54321 aus

Und damit auch zu "(let a ((b 5)) (write b) (if (> b 1) (a (- b 1))))":

&{ my $a = sub { my $b = $_[0]; if ($b > 0) { print $b; &$a($b - 1) } } } (5);
# gibt nochmal 54321 aus

Man beachte aber, daß my-Variablen erst nach Vollendung ihrer Deklaration existieren, sich das $a im sub-Ausdruck also auf das globale $a bezog! ( Richtig heißen müssen hätte es "my $a; $a =" statt "my $a =". )

Wenn Du, lieber NPN, an dieser Stelle mittlerweile überhaupt nichts mehr verstehst, dann macht das nichts. Am besten überspringst Du dann auch das folgende und liest direkt bei den Hashes weiter.

Bei noch genauerer Betrachtung ist die Semantik der my-Variablen gewöhnungsbedürftig:

sub befremdlich {
  my $x = 2;
  sub { my $y = 1; sub { print $x, $y; } }
}

sub sehr_befremdlich {
  my $x = 2;
  sub { my $y = 1; my $x = $x; sub { print $x, $y; } }
}#                 ^^^^^^^^^^^

&{ &{ &befremdlich } }; &{ &{ &sehr_befremdlich } }; # gibt 12 aus

Dazu ein Beispiel mit einem lambda im let: (define a (let a ((b 1)(c 2)) (write b) (write c) (lambda () (a b c))))

$a = &{ my $a; $a = sub { my ($b, $c) = @_;
        print $b, $c;
        &{+sub{my($a,$b,$c)=@_; # <---
          sub { &$a($b, $c) }
        }}($a,$b,$c)            # <---
     } } (1, 2); # gibt 12 aus

Die mit "<---" markierten Zeilen bringen die freien Variablen in die selbe lexikalische Ebene und umgehen so die befremdliche my-Semantik. (In den Beta-Versionen von Perl 5.004 ist dieser Bug behoben. Auch das hier bei Perl 5.002 erforderliche '+'-Zeichen im Workaround (ein weiterer Bug) ist dann nicht mehr notwendig.)

Das Pendant zum SCHEME-"(((a)))" lautet nun so:

&$a( &$a( &$a() ) ); # gibt 121212 aus

Man beachte, daß dies keineswegs esoterischer Schnickschnack ist! Sowas kommt z.B. in jeder zweiten Stream-Funktion vor (mit Häkchen markiert):

# ; Mische f-geordnete Streams ohne Wiederholung:
# (define (merge a b f)
#   (let ((c (f (head a) (head b))))
#     (cond ((< c 0) (cons-stream (head a) (merge (tail a) b f)))
#           ((> c 0) (cons-stream (head b) (merge a (tail b) f)))
#           (else (cons-stream (head a) (merge (tail a) (tail b) f))))))
sub merge {
  my ($a, $b, $f) = @_;
  my $c = &$f(head($a), head($b));
  if ($c < 0) { return cons(head($a), sub{merge(tail($a), $b, $f)}) }
  elsif ($c > 0) { return cons(head($b), sub{merge($a, tail($b), $f)}) }
  else { return cons(head($a), sub{merge(tail($a), tail($b), $f)}) }
}

# ; Ebne Stream von f-sortierten Streams ein:
# (define (flatten s f)
#   (cons-stream (head (head s))
#                (flatten (cons-stream (merge (tail (head s)) (head (tail s)) f)
#                                      (tail (tail s))) f)))
sub flatten {
  my ($s, $f) = @_;
  cons (head(head($s)),
        sub{flatten(cons(merge(tail(head($s)), head(tail($s)), $f),
                         &{+sub{my$s=$_[0]; sub{tail(tail($s))} }}($s) ), $f)});
}#                       ^^^^^^^^^^^^^^^^^^                     ^^^^^^

# ; Wende f auf alle Elemente des Streams an:
# (define (mapf f s) (cons-stream (f (head s)) (mapf f (tail s))))
sub mapf {
  my ($f, $s) = @_;
  cons(&$f(head($s)), sub{mapf($f, tail($s))})
}

# ; m-me Stream der n-Potenzen (bzgl. m) mit x:
# (define ((pow m n) x) (cons-stream x ((pow m n) (m n x))))
sub pow {
  my ($m, $n) = @_;
  sub { my $x = $_[0];
        cons($x, &{+sub{my($m,$n,$x)=@_;
#                ^^^^^^^^^^^^^^^^^^^^^^^   
                 sub{&{pow($m, $n)}(&$m($n, $x))} }}($m,$n,$x) );}
}#                                                ^^^^^^^^^^^^

Die hier benutzten Grundfunktionen der Streams kann man so ausdrücken:

sub head {$_[0][0]}    # head($a) entspricht SCHEME-"(head a)"
sub tail {&{$_[0][1]}} # tail($a) entspricht SCHEME-"(tail a)"
sub cons {[$_[0],&{+sub{my($x,$y)=$_[0];sub{if(defined$y){$y}else{$y=&$x}}}}
          ($_[1])]}    # cons($a, sub{$b}) entspricht "(cons-stream a b)".

Wie gesagt, in Perl 5.004 kann man die Häkchen-Geschichten weglassen. (Wie nützlich richtig funktionierende Closures sind, hat die Perl-Gemeinde glücklicherweise inzwischen gemerkt.)

Assoziative Arrays heißen Hash. War in skalarem Kontext eine Liste ihr letztes Element und ein Array die Anzahl seiner Elemente, so ist ein Hash ein Wert, der im Skalarkontext als Boolean genau dann wahr ist, wenn das Hash mindestens 1 Key/Value-Paar enthält. (Genauer ist der Wert, zu dem ein Hash im Skalarkontext interpoliert, nicht spezifiziert.) In Listenkontext ist ein Hash die Liste mit abwechselnd Key und Value. Entsprechend [] bei Arrays ist {} eine Referenz auf ein Hash, also ein Skalar. $a{$b} ist der Value zum Key $b im Hash %a. Als Keys sind beliebige Skalare möglich, ausgenommen Referenzen. Ein Bezeichner als Key gilt implizit als String, weswegen es für Listen einen Komma-Operator gibt, der den linken Wert, wenn es ein Bezeicher ist, als String quotiert:

@a = (a => "b");
$a = {@a => $a[1] => 12};
%a = ($a -> {$a -> {a}}, 12);
print $a{12}; # gibt 12 aus

Hier war unsere Formulierung "Ein Bezeichner als Key gilt implizit als String" nicht so glücklich, denn der "=>"-Operator wird ja gerade gebraucht, um explizit den links davon stehenden Bezeichner als String zu quotieren, um ihn als Key verwenden zu können. Die erste Zeile kann man also auch @a = ("a", "b") schreiben.

Zur weiteren Erklärung: Damit ist die zweite Zeile eine Zuweisung einer Hash-Referenz auf (a => "b", b => 12) an $a, entsprechend "%hilf = (a => "b", b => 12); $a = \%hilf" (und nicht etwa "$a = \(a => "b", b => 12)", denn das entspräche "(\'a', \'b', \'b', \'12')" -- siehe im Text weiter oben!). Wir erinnern uns nun, daß "$a->" vor einem Subskript-Operator das Gleiche wie "$$a" ist, und $$a{a} ist "b". $a->{"b"}, also $$a{b} ist aber 12. Damit ordnet das Hash dem Schlüssel "12" den Wert 12 zu. (Dieses Beispiel mischt somit Strings, Bezeichner, Listen, usw. derart, daß man sofort erkennen kann, ob man das eigentlich verstanden hat.)

Strings mit ' sind Literale. Strings mit " auch (d.h. Whitespace wird nicht ignoriert), aber in interpolativem Kontext. Listen interpolieren mit der Variable $" zwischen den Elementen (default ist " "):

$a = @a = (3, 4);
print '($a, @a)'; # gibt "($a, @a)" aus
print "($a, @a)"; # gibt "(2, 3 4)" aus
print  ($a, @a) ; # gibt "234" aus (jeweils ohne ")

Mit interpoliert in den String wird ein eventuelles Subskript:

%a = (a => 'a'); $a = \%a;

print  $a->{a} ; # gibt "a" aus
print "$a->{a}"; # gibt "a" aus

Für den Aufruf einer Objekt-Methode (Erklärungen siehe weiter unten) gilt das aber nicht:

package a;sub new{bless{}}sub a{return"a"}$a=new a;

print  $a->a ; # gibt "a" aus
print "$a->a"; # gibt "a=HASH(0x74b1cc)->a" aus

Auch hier verhindert ein '\' die Interpolation (wenn auch mit anderer Semantik als außerhalb von Strings):

print "\$a"; # gibt $a aus

Genau genommen spricht man hier, innerhalb von Strings, auch im \-Fall doch wieder von "Interpolation", nämlich von Backslash-Interpolation. (Es wurde \$ zu $ interpoliert.) Z.B. interpoliert '\n' in einem '"'-String in ein echtes Newline:

print "\n"; # Gibt einen Zeilenwechsel aus (welch' Überraschung!)

Als Beispiel zur Interpolation von Arrays in Strings sei eine Routine angeführt, die verschachtelte Listen in einen String konvertiert (das map wendet den Ausdruck im 1. Parameter auf die Liste der restlichen Parameter an, wobei $_ das jeweilige Element ist):

sub array {
  local $" = ", ";
  return "@{[map array($_), @_]}" if @_ > 1;
  return "$_[0]" unless UNIVERSAL::isa($_[0],"ARRAY");
  return '['.array(@{$_[0]}).']';
}

print array(1, [2, [], [3, 4]]); gibt "1, [2, [], [3, 4]]" aus

Zuweisungen sind der zugewiesene Wert. Listenzuweisungen in skalarem Kontext sind die Anzahl der zuzuweisenden Werte:

print  $a   =  $a  = (3, 1); # gibt 1 aus
print  $a   = ($a) = (3, 1); # gibt 2 aus, nicht 1

print @a[0] = ($a) = (3, 1); # gibt 3 aus
print $a[0] = (@a);          # gibt 2 aus

print  $a   =  %a  = (3, 1); # gibt 2 aus
print  $a   =  %a;           # gibt 1/8 aus

Zuweisungen sind auch lvalues:

$g = "Garten";
($z = $g . 'zwerg') =~ s/($g)z(.*)/Z\2widder/;
print $z; # gibt Zwergwidder aus (Schlappohrkarnickel)

Für Interpolation in Pattern gilt ebenfalls die Ausnahme bei "'"-Delimiter, d.h. das "Z\2widder" wird hier wie ein double-quoted String behandelt. (Jedenfalls fast, denn das '\2' steht hier für den gematchten Inhalt der zweiten Klammer im Pattern und nicht, wie sonst in '"'-Strings, für Control-B.)

Mit der Option 'e' hinter dem letzten Slash würde "Z\2widder" übrigens als Ausdruck interpretiert, d.h. '\2' stünde darin dann für eine Referenz auf 2. Man kann aber auch $2 nehmen, das ebenfalls zum Inhalt der zweiten Klammer des letztgematchten Patterns interpoliert. ('\2' könnte man allerdings auch schon im Pattern selbst verwenden.)

Ja ja, schlag mich nur, lieber NPN. Ich weiß, ich müßte den Pattern-Matching-Kram erst erklären. Was z.B. bedeutet dieses " =~ "? Nun, auf das, was links davon steht (hier der String "Gartenzwerg") wird das, was rechts davon steht, angewendet (hier die Substitution von "Gartenz..." durch "Z...widder". Hierzu noch ein Beispiel aus dem realen Einsatz auf einer Debian(Linux)-Installation:

$plistCommand = "dpkg -l";
$pdependsCommand = "dpkg -s"; 
$psizeCommand = "dpkg -L"; 

open PLIST, "$plistCommand|"; 
for (1..5) {<PLIST>}; 
while (<PLIST>) { 
  ($status, $package, $version) = split; 
  @size = `du -c -k \$(find \$($psizeCommand $package) -maxdepth 0 -type f)`; 
  ($size) = $size[$#size] =~ /^(\d+)/; 
  print "\n$package ($version) -- $size KB\n"; 
  print grep /Depends:|Recommends:|Suggests:|Provides:/, `$pdependsCommand $package`; 
} 
close PLIST or die "$plistCommand geht nicht";

Hier ist PLIST ein Filehandle der Pipe, in die die Ausgabe von "dpkg -l" flutscht. <PLIST> steht dann für die jeweils nächste Zeile dieser Pipe. Unsichtbar steht da eigentlich "while ($_ = <PLIST>)" und "split $_", d.h. die Eingabezeile wird am Whitespace aufgespalten. @size ist dann das Array mit allen Ausgabezeilen des du-Kommandos. (Hier wird auf ein UNIX-Kommando diesmal also nicht per Pipe, sondern per Backquote zurückgegriffen.) Die blaue Zeile möchte ich hier erklären, weil sie ein weiteres Beispiel für jenes " =~ " ist:

Da die Zuweisung an $size durch die Klammerung in Listenkontext gesetzt wurde, liefert der Match-Operator die Liste der Matches von den Klammerausdrücken im Pattern zurück, hier also die am Anfang der Zeile stehende Zahl. Die beiden Klammerpaare sind also quasi der Kernbestandteil dieser Code-Zeile. (Klammern sind sowieso das Wichtigste in Perl. Entweder haben sie keine Bedeutung, wo man eine erwartet, oder es ist genau umgekehrt.)

Wir kennen nun die lustigen Zeichen $, @, &, und %. Alles gleichzeitig denotiert man mit dem ebenfalls lustigen Zeichen * und nennt das Typeglob. Ein Typeglob *a ist ein spezielles Hash mit festen Keys wie "SCALAR", "ARRAY", "HASH", usw. derart, daß z.B. *a{SCALAR} identisch ist mit \$a:

$a = 12;
print ${*a{SCALAR}}; # gibt 12 aus

Damit ist die Symboltabelle von einem Package a das Hash %a::, das Bezeichner als Strings auf Typeglobs abbildet. Das Package-Statement schaltet blocklokal auf diese Symboltabelle um. Package-Namen sind immer global, d.h. ggf. voll qualifiziert:

$a::a = 1; $a = 2;
{ package a; $a++; { package a::a; $::a--; }} # "a::a", nicht bloß "a"
print $a, $a::a, $a::a::a; # gibt 12 aus

Strings gelten als symbolische Referenzen auf Package-Variablen diesen Namens. My-Variablen sind keine Package-Variablen:

%a = (a => 1);
my %a = (a => 2);
print "a" -> {a}, $a {a}; # gibt 12 aus

Man verwechsle diese symbolischen Referenzen jetzt bloß nicht mit den oben eingeführten normalen Referenzen. Eine symbolische Referenz ist z.B. ${"a"} für $a. Eine richtige Referenz \$a ist von $a aber verschieden!

Ein Bezeichner, der sonst nichts bedeutet (Bareword), gilt als String. Ein einzelner Bezeichner in einem Block hinter einem lustigen Zeichen gilt nicht als Ausdruck, also auch nicht als String:

${"a"} = zwerg;   # symbolische Referenz zur Laufzeit
${a} = Garten.$a; # aber hier Symboltabellensuche zur Übersetzungszeit

Der Operator scalar erzwingt skalaren Kontext (Listenelemente werden von links nach rechts ausgewertet), d.h. dies entspricht einer symbolischen Referenz:

print scalar(*hilf = $::{a}, ${*hilf{SCALAR}}); # gibt Gartenzwerg aus

Hier wird also im Hash %:: der Wert zum Schlüssel "a" gesucht und dann im erhaltenen Typeglob nach dem Schlüssel "SCALAR", der eine zu dereferenzierende (richtige!) Referenz ergibt.

$::{a} sucht den Typeglob zur Laufzeit. *{::a} sucht ihn zur Übersetzungszeit (identisch mit *::a, also *a in main). Typeglob-Zuweisungen aliasen Bezeichner. Bei Zuweisung einer (echten) Referenz an einen Typeglob wird darin die jeweils entsprechende ersetzt:

package a; # Zum Schutz des main-Packages vor dem Aliasing

*a = *b;
*c = \@b;
$b = 6;
$c = 7;
$b[0] = 8;
$c[0] = 9;
print $a, $a[0]; # gibt 69 aus

package main; # %:: ist Abkürzung für %main::

Typeglobs gibt es nur als Package-Variablen, also nicht my. Eine Alternative zu my-Variablen sind local-Variablen. Diese sind Package-Variablen mit dynamischer Semantik, d.h. stünde im folgenden Curryfizierer "local" statt "my", wären $f und $x undefiniert: ("shift" schiebt ein Element aus @_.)

sub concat { $_[0] . $a . $_[1] }
sub cur { my ($f, $x) = @_; sub { &$f($x, shift) } }

$York = cur(\&concat, "York");
$Karl = cur(\&concat, "Karl");

$a = " Heider ";
print &block, &$York("Werres\n");
sub block { local $a = "-Heinz "; &$Karl("Krause\n"); }

( Stünde im Block "my" statt "local", hieße jemand "Karl Heider Krause", ließe man im Block das "local" ganz weg, hieße jemand "York-Heinz Werres". )

(Ein Curryfizierer ist ein Funktion höherer Ordnung, die eine Funktion auf eine andere derart abbildet, daß sie einen Parameter fixiert. -- So wie es das &cur oben macht.)

Es kommen nun die objektorientierten Features von Perl, denn diese sind nicht ganz unwichtig, bilden sie schließlich u.a. das Interface zu Tk: Was referenziert werden kann, läßt sich segnen. Gesegnete Dinge wissen den Namen von ihrem Package:

package quark;
$a = {};
$b = bless $a;
print scalar ref {}, scalar ref $a, scalar ref $b; # gibt HASHquarkquark aus

Man kann ein referenziertes Ding in ein beliebiges Package segnen:

sub new { bless { member => $_[1] }, $_[0] }
sub add { $_[1] + $_[0] -> {member} }

package gemuese;
@ISA = quark;

package schale;

Also, was haben wir hier: Es gibt ein Array @gemuese::ISA und im Package quark die Routinen add() und new(). Letztere gibt eine Referenz auf ein Hash zurück, das dem Schlüssel "member" den 2. Parameter zuordnet. Die zurückgegebene Hash-Referenz wurde dabei zuvor mit dem ersten Parameter markiert. Dieser erste Parameter wird beim Aufruf von new() ein Package-Name sein, so daß wir sagen, daß das Hash "in das Package gesegnet" wurde. Das ist zugegebenermaßen bescheuert, aber ich erkläre hier ja nur, was im schwarzgedruckten Text stand.

add() wiederum sucht die Variable "member" im als ersten Parameter übergebenen Package (und addiert den zweiten Parameter zum Rückgabeergebnis), d.h. es soll eine Methode eines Packages darstellen, während new() ein Konstruktor sein soll.

Wenn man Strings, die Namen eines Packages sind, "Klasse" nennt, sowie Referenzen auf ein gesegnetes Etwas "Objekt", dann kann man:

$o = &{"${gemuese::ISA[0]}::new"}("gemuese", 1); # Erzeugen einer Gemüseinstanz
#                           ^^^    ^^^^^^^
$i = &{(eval "\$@{[ref $o]}::ISA[0]")."::add"}($o, 2); # Aufruf der add-Methode
#                      ^^                ^^^     
print $i; # gibt 3 aus

Erzähl mir nun nicht, lieber NPN, das sei kryptisch! Es wird hier beim "Erzeugen einer Gemüseinstanz" doch bloß zunächst die Basisklasse gesucht, also gemuese::ISA[0] genommen. Anschließend steht da also "${quark}::new" und zwar als String. Durch das lustige Zeichen & wird das zur symbolischen Referenz auf die Methode quark::new() und somit mit den Parametern ("gemuese", 1) aufgerufen. Das Ergebnis ist die gesegnete (richtige) Referenz.

Und genau diese greift add() nun auf: [ref $o] liefert eine Referenz auf ein Array, dessen 1. Element der String "gemuese" ist. @{...} dereferenziert das wieder. (Das @ ein lustiges Zeichen ist, wird das Ganze mitten im String ausgewertet. Du erinnerst Dich: Wir nennen das "interpoliert".) Da steht nun also $gemuese::ISA[0], was zu "quark" ausgewertet und dann mit "::add" concateniert wird. Und zum Schluß kommt dann wieder der Effekt der symbolischen Referenz. Das ist wirklich zu verstehen! (Und tatsächlich kryptisch. :-)

Für diese Ausdrücke gibt es abkürzende Notationen. C++ig:

    $o = new gemuese(1);
    $i = $o->add(2);

Oder so:

    $o = gemuese->new(1);
    $j = add $o 2;

print $i, $j; # gibt 33 aus

Hinter einer Methode in Kommandoform steht, genau wie bei lustigen Zeichen, ein Bezeichner, ein Skalar, oder ein Block, d.h. "methode $objekt->{member}" meint "(methode $objekt)->{member}", nicht "methode { $objekt->{member} }".

Und "methode ( $objekt->{member} )" wäre ganz was anderes:

package quark; 
sub new { bless {}, $_[0] }
sub methode2 { print $_[0] =~ /(.*)=/ }

package gemuese;
@ISA = quark;

package schale;
sub methode1 { print "schale"; }
$a = {a => new gemuese};
methode1 ($a->{a}); # gibt "schale" aus
methode2 {$a->{a}}; # gibt "gemuese" au

Nun könnten beide Methoden natürlich gleich heißen, z.B. "methode". Dann wäre die letzte Zeile allerdings ein Syntax-Error, weil dort ja dann eine ungerade Anzahl von Elementen in einer Hash-Liste stünde! Die Kommandoform des Objektaufrufs geht also nur, wenn im Aufruf-Package keine solche Subroutine vorkommt.

Anders als oben werden bei den abkürzenden Notationen die Methoden aber nicht fest aus der ersten Basisklasse geholt, sondern zunächst in der Klasse selbst gesucht und dann rekursiv, Tiefe zuerst, in allen Basisklassen. Damit kann man Methoden in abgeleiteten Klassen überschreiben, also alten Code dazu bringen, neuen Code zu benutzen ("SUPER" ist ein Pseudo-Package, das die Suche direkt in @ISA beginnen läßt):

package Klasse;
sub pm { $_[1] + 1 }
sub rds { my $this = shift; $this->{member} + shift }
sub ifr { my $this = shift; $this->pm($this->rds(shift)) }
sub new { bless {member => $_[1]}, $_[0] }

package Subklasse;
@ISA = Klasse;
sub rds { my $this = shift; $this->SUPER::rds(shift) + $this->{val}  }
sub new { my $this = Klasse->new(@_[1]); $this->{val} = -1; bless $this }

package main;
$ko = new Klasse(1);
$sko = new Subklasse(1);
print $ko->ifr(2), $sko->ifr(2); # gibt 43 aus

Das Ganze entspricht also dem folgenden Java-Programm:

=java
  class Klasse {
    private   int member;
    private   int pm(int i)  { return i+1; }
    protected int rds(int i) { return i+member; }
    public    int ifr(int i) { return pm(rds(i)); }
    public Klasse(int i) { member = i; }
  }

  class Subklasse extends Klasse {
    private   int val;
    protected int rds(int i) { return super.rds(i)+val; }
    public Subklasse(int i) { super(i); val = -1; }
  }

  class main {
    public static void main(String args[]) {
      Klasse ko = new Klasse(1);
      Klasse sko = new Subklasse(1);
      System.out.println("" + ko.ifr(2) + sko.ifr(2));
    }
  }
=cut

Der wesentliche Unterschied ist dabei nicht nur die nicht vorhandene Memberzugriffskontrolle, sondern vor allen Dingen die Tatsache, daß Methoden zur Laufzeit gesucht werden.

Jetzt erzähl mir nicht, lieber NPN, Du könnest kein Java lesen! Und SCHEME auch nicht? Dann ist es wohl besser, weiter unten mit den Filehandles fortzufahren. ("=irgendwas" bis "=cut" ist übrigens Kommentar in Perl.)

Man kann Variablen in eine Klasse ziehen. Nach "tie $p, 'P'" ist "(tied $p)" eine Instanz von P und "$p" steht fortan für "(tied $p)->FETCH" oder, als lvalue, für "STORE {tied $p} <Wert>". Je nach Typ der Variablen muß P noch TIESCALAR, TIEARRAY oder TIEHASH implementieren. Als Beispiel soll $p235 nach der Zuweisung $p235 = [$a, $b] für die Anzahl der Zahlen zwischen $a und $b stehen, die ein Produkt von 2, 3 und 5 sind:

tie $p235, 'p235';
$p235::fetch = sub {
  my ($von, $bis, $c) = (@{$_[0]}, 0);
  for $i ($von..$bis) {
    $i /= 2 until $i % 2;
    $i /= 3 until $i % 3;
    $i /= 5 until $i % 5;
    $c++ unless $i -1
  }
  $c
};
{ package p235;
  sub TIESCALAR { bless [] }
  sub STORE { @{$_[0]} = @{$_[1]}  }
  sub FETCH { &$fetch }
}

$p235 = [3, 20];
print $p235; # gibt 12 aus

Für große Intervalle, z.B. [6e15, 8e15] statt [3, 20] ist dieser Algorithmus so natürlich nicht brauchbar, weil die Rechenzeit zu stark wächst. Ein Verfahren, bei dem die Rechenzeit überhaupt nicht wächst, wäre (unter Verwendung der oben definierten Funktionen flatten, mapf und pow) z.B. dies:

$p235::fetch = sub {
  my ($von, $bis, $c) = (@{$_[0]}, 0);
  my ($d, $m, $p) = (sub{$_[0] - $_[1]}, sub{$_[0] * $_[1]});
  $p = flatten(mapf(pow($m, 5),
               flatten(mapf(pow($m, 3), &{pow($m, 2)}(1)), $d)), $d);
  $p = tail($p) while head($p) < $von;
  $p = tail($p), $c++ until head($p) > $bis;
  $c
};
print $p235; # gibt 12 aus

Bei diesem Verfahren wächst dafür der Speicherbedarf in übler Weise, d.h. für große Zahlen ist es noch weniger geeignet als das Verfahren mit dem Wachstum in der Rechenzeit. Als dritter Versuch empfiehlt sich eine rekursive Definition (unter Verwendung der oben definierten Funktionen merge, mapf und cur). Es muß gegenüber der Version zuvor nur eine einzige Zuweisung ersetzt werden:

  $p = cons(1, sub{merge(mapf(cur($m, 5), $p), merge(mapf(cur($m, 3), $p),
                         mapf(cur($m, 2), $p), $d), $d)});

Das Problem nun ist, daß diese Version zwar gemäßigten Speicherverbrauch und fast kein Rechenzeitwachstum aufweist, aber für die Differenzbildung in $d, anders als die vorherige Version, numerisch exakter Zahlendarstellung bedarf. Wir überprüfen, wieviele Elemente dieses Streams korrekt sind, indem wir denselben noch einmal mit BigInts statt Double-Floats definieren:

  use Math::BigInt;
  $pb = cons(Math::BigInt->new("1"), sub{merge(mapf(cur($m, 5), $pb),
             merge(mapf(cur($m, 3), $pb), mapf(cur($m, 2), $pb), $d), $d)});

BigInts überladen die numerischen Operatoren, d.h. wir können die obigen Differenzbildungs- und Multiplikationsfunktionen direkt übernehmen:

  ($d, $m) = (sub{$_[0] - $_[1]}, sub{$_[0] * $_[1]});

Die kanonische Form nichtnegativer BigInts hat ein führendes '+':

  $p = tail($p), $pb = tail($pb), $korrekt++ until head($p) != head($pb);
  print $korrekt, head($pb); # gibt 3122+408146688000 aus

Für das besagte Intervall [6e15, 8e15] müßten wir also die BigInt-Version nehmen. 6e12 und 8e12 haben übrigens selbst die Eigenschaft P235, d.h. sie sind nur durch 2, 3 und 5 teilbar (und durch Zahlen mit der Eigenschaft P235). Da BigInts aber doch reichlich lahm sind, versuchen wir stattdessen eine Primfaktordarstellung der Zahlen. Dann können wir vor dem Vergleich die Exponenten kürzen:

$p235::fetch = sub {
  my ($von, $bis, $c, $p) = (@{$_[0]}, 0);
  sub convert { my $a = shift; 2**$a->[0]*3**$a->[1]*5**$a->[2] }

  $p = cons([0,0,0], sub{merge(mapf(sub{my$p=shift;[$$p[0],$$p[1],$$p[2]+1]},
       $p), merge(mapf(sub{my$p=shift;[$$p[0],$$p[1]+1,$$p[2]]}, $p),
       mapf(sub{my$p=shift;[$$p[0]+1,$$p[1],$$p[2]]}, $p), \&db), \&db)});

  $p = tail($p) while convert(head($p)) < $von;
  $p = tail($p), $c++ until convert(head($p)) > $bis;
  $c
};
print $p235; # gibt 12 aus

Und noch die Vergleichsfunktion mit Exponentenkürzung:

sub db {
  my ($a, $b) = @_;
  my ($a2, $a3, $a5) = @$a;
  my ($b2, $b3, $b5) = @$b;
  my $k = ($a2 > $b2 ? $b2 : $a2); $a2 -= $k; $b2 -= $k;
  $k = ($a3 > $b3 ? $b3 : $a3); $a3 -= $k; $b3 -= $k;
  $k = ($a5 > $b5 ? $b5 : $a5); $a5 -= $k; $b5 -= $k;
  ($a, $b) = (2**$a2 * 3**$a3 * 5**$a5, 2**$b2 * 3**$b3 * 5**$b5);
  if ($a > 1e12 || $b > 1e12) { return Math::BigInt->new("$a") - $b }
  else { return $a - $b }
}

Machen wir nun die Probe auf's Exempel:

$p235 = [6e15, 8e15];
print $p235; # gibt "Out of memory!" aus

Was ist nun an dieser Version schon wieder falsch? Antwort: Nichts mehr. Sie ist schnell, speicherschonend, numerisch korrekt und fast ohne BigInts. Ein Linux mit 64 MB Speicher (32/32 RAM/Swap) hat nur leider ein ganz paar Bytes zu wenig für das Erfolgserlebnis "172". Um den endgültigen Beweis aber doch noch zu erbringen, stellen wir die Zahlen statt als Array von drei Floats als Vektor von drei Bytes dar. Bei der Gelegenheit geben wir die 172 Zahlen auch gleich mitsamt Primfaktorzerlegung aus und messen die Anzahl der BigInt-Vergleiche. Hier noch einmal eine Komplettversion:

=komplett
#!/usr/bin/perl
use Math::BigInt;

sub head {$_[0][0]}
sub tail {&{$_[0][1]}}
sub cons {[$_[0],&{+sub{my($x,$y)=$_[0];sub{if(defined$y){$y}else{$y=&$x}}}}
          ($_[1])]}
sub mapf { my ($f, $s) = @_; cons(&$f(head($s)), sub{mapf($f, tail($s))}) }

sub merge {
  my ($a, $b, $f) = @_;
  my $c = &$f(head($a), head($b));
  if ($c < 0) { return cons(head($a), sub{merge(tail($a), $b, $f)}) }
  elsif ($c > 0) { return cons(head($b), sub{merge($a, tail($b), $f)}) }
  else { return cons(head($a), sub{merge(tail($a), tail($b), $f)}) }
}

sub m2 { my$p=shift; v( a($p,0)+1, a($p,1)  , a($p,2)   ) }
sub m3 { my$p=shift; v( a($p,0)  , a($p,1)+1, a($p,2)   ) }
sub m5 { my$p=shift; v( a($p,0)  , a($p,1)  , a($p,2)+1 ) }

sub a { my ($v, $i) = @_; vec ($v, $i, 8) }
sub v {
  my ($a, $b, $c, $v) = @_;
  vec ($v, 0, 8) = $a;
  vec ($v, 1, 8) = $b;
  vec ($v, 2, 8) = $c;
  $v
}

$p = cons( v(0,0,0), sub{merge(mapf(\&m5, $p), merge(mapf(\&m3, $p),
                               mapf(\&m2, $p), \&db), \&db)});

sub convert { my $v = shift; 2**a($v,0) * 3**a($v,1) * 5**a($v,2); }

sub druck {
   my $v = shift;
   my ($a, $b, $c) = ( a($v,0), a($v,1), a($v,2) );
   my $z = 2**$a * 3**$b * 5**$c;
   print "$n-($a $b $c)-$z\n";
   $z
}

($von, $bis, $c)  = (6e15, 8e15, 0);
$n++, $p = tail($p) while convert(head($p)) < $von;
$n++, druck(head($p)), $p = tail($p), $c++ until convert(head($p)) > $bis;

print "Es wurden $c Zahlen im Intervall [$von, $bis] ausgegeben.\n";
print "Und es waren nur ", $bigint_vergleiche," der ",
      $bigint_vergleiche + $normal_vergleiche,
      " Vergleichsoperationen BigInt-ig.\n";

sub db {
  my ($a, $b) = @_;
  my ($a2, $a3, $a5) = (a($a,0), a($a,1), a($a,2));
  my ($b2, $b3, $b5) = (a($b,0), a($b,1), a($b,2));
  my $k = ($a2 > $b2 ? $b2 : $a2); $a2 -= $k; $b2 -= $k;
  $k = ($a3 > $b3 ? $b3 : $a3); $a3 -= $k; $b3 -= $k;
  $k = ($a5 > $b5 ? $b5 : $a5); $a5 -= $k; $b5 -= $k;
  ($a, $b) = (2**$a2 * 3**$a3 * 5**$a5, 2**$b2 * 3**$b3 * 5**$b5);
  if ($a > 1e12 || $b > 1e12)
  { $bigint_vergleiche++; return Math::BigInt->new("$a") - $b }
  else
  { $normal_vergleiche++; return $a - $b }
}
=cut

Das Ergebnis ist:

#   7474-(16 1 15)-6.e+15
#   ...
#   7645-(18 0 15)-8.e+15
#   Es wurden 172 Zahlen im Intervall [6.e+15, 8.e+15] ausgegeben.
#   Und es waren nur 66 der 15268 Vergleichsoperationen BigInt-ig.

Nun könnten wir noch versuchen, diese Anzahl von 66 auf 0 zu senken. Viel mit Perl zu tun, hätte das aber nicht mehr. Perl selbst sollte man sich allerdings auch gar nicht genauer anschauen, weil dann doch reichlich wundersame Dinge zutage treten, z.B: dies:

tie $q, 'Q';
$q = [1, 2];

print $q->[0]; # gibt 1 aus
print $q->[1]; # gibt 3 aus

print $q->[0], # gibt nichts aus
      $q->[1]; # gibt 3 aus

print "$q->[0]$q->[1]";  # gibt 3 aus
print "$q->[0] $q->[1]"; # gibt 1 3 aus

package Q;
sub TIESCALAR { my $t; bless \$t; }
sub STORE { my $t=shift; $$t = shift; }
sub FETCH { my $t=shift; [$$t->[0], $$t->[1] + 1] }

Beispiele dieser Art gibt es wie Sand am Meer. 40% der Programmierzeit stellt man die Proramme um, bis sie plötzlich ohne Grund so funktionieren, wie sie sollen. Weitere 40% beschäftigt man sich mit "lustigen Zeichen" (die im übrigen eine Fehlübersetzung von "funny character" sind, was eigentlich "bekloppte Zeichen" bedeutet). Die restlichen 20% hat man das Programm, das man erstellen wollte, zunächst in einer anderen Sprache formuliert und nur noch übersetzt. Wer direkt in Perl entwickelt, braucht entweder hauptsächlich Pattern-Matching, oder ist verrückt.

Ganz anders sieht das natürlich mit Perl/Tk aus, denn das ist ein wahrer Segen im Vergleich zu Tcl/Tk! Wir werden unten ein Beispiel dazu sehen.

Trotzdem benutzen wir nun Perl, um diesen Schnellkurs (eine HTML-Datei) in ein ausführbares Perlskript umzuwandeln. Dazu noch folgende Ergänzungen:

Wie schon erwähnt, ist $::{a} ein Typeglob, kein Skalar (nämlich der Symboltabelleneintrag zum Bezeichner "a"), jedoch z.B. *::a{CODE} ein Skalar, kein Typeglob (nämlich die Referenz \&::a aus dem Typeglob *::a). Keine Entsprechung gibt es hingegen zu *::a{FILEHANDLE}, denn Filehandle haben kein lustiges Zeichen. Man identifiziert sie über den Kontext oder über ihren Typeglob und betrachtet sie als Skalar:

$file = *STDOUT;
print $file "Die Keks", "dose"; # $file ist ein Filehandle

print STDOUT " ist ", "leer.";  # man beachte: kein Komma nach dem Filehandle

Identisch mit *STDOUT wäre hier \*STDOUT, denn ein Typeglob betrachtet sich bei Zuweisung an einen Skalar immer als Referenz auf sich:

$a = *a; # identisch mit: $a = \*a;
@a = 4;
*b = *$a; # hier dereferenziert '*' einen "Pointer" (Referenz auf Typeglob)
print @b; # gibt 4 aus

Vielleicht haben wir das aber auch falsch verstanden:

print $b; # gibt den String "*main::a" aus.

Die 4 können wir auch so ausgeben:

print @{*$a};        # gibt 4 aus (ohne {} wäre syntax error!)
print @{*$a{ARRAY}}; # <-- oder gar so (gibt auch 4 aus)

Aber jetzt nicht mehr:

$b = 5;
print @{*$a};        # gibt nichts aus
print @{*$a{ARRAY}}; # gibt auch nichts aus

Wir wollen das lieber nicht näher analysieren. All das ist auch nicht relevant, wenngleich eben sehr typisch. Das ist Perl!

Es muß aber noch erwähnt werden, daß sowas wie "print $file $string" nicht verwechselt werden sollte mit z.B. "methode $objekt $string":

banane krumm gelb; # gibt "krumm und gelb" aus
{ package krumm; sub banane {$"=' und '; print "@_"} }

Hätten wir die Klassenmethode nicht "banane", sondern "print" genannt, wäre nichts ausgegeben worden, weil der Filehandle banane undefiniert ist.

Nein, weil der Filehandle "krumm" undefiniert ist. (Na sowas, da haben wir doch glatt einen Fehler in diesem Schnellkurs gefunden, den 1 Jahr lang niemand entdeckt hat. Aber bei all dem Ärger, den die Kommandoform des Objektaufrufs machen kann -- siehe oben -- sollte man sich auch wirklich nicht damit befassen.)

Bezeichner ohne lustiges Zeichen sind eben ganz und gar nicht lustig! Übrigens haben auch Formats kein lustiges Zeichen. Sie haben noch nicht einmal einen Eintrag in Typeglobs. Wir werden hier zum Thema Formats aber weder das Camel Book noch die Manpages nachplappern. Aus dem Camel Book stammt aber dieser Vergleich äquivalenter Anweisungen (man beachte das wegfallende Komma, wenn der erste Parameter ein Block ist):

@result = map {glob($_)}  "*.c", "*.c,v";
@result = map   <${_}>  , "*.c", "*.c,v";

Dieser <>-Operator ist nicht zu verwechseln mit dem Input-Operator für Filehandles, der eine Zeile einliest (skalarer Kontext) oder alle. Der Default ist :

Ja, was ist denn der Default? Hier stand eigentlich "<STDIN>", was vom HTML-Browser unterdrückt wurde und auch noch falsch ist. Richtig ist folgendes: <> bedeutet nicht <STDIN>, sondern <ARGV>. Und ARGV ist ein magischer Filehandle, der für die virtuelle Datei steht, die sich ergäbe, wenn man alle in der Parameterliste des Programmaufrufs (@ARGV) erwähnten Dateien aneinanderhängen würde. (Dabei wird @ARGV sogar ganz unvirtuell ge"shift"et und der jeweils erhaltene Dateiname in $ARGV abgelegt.) Ist @ARGV beim ersten Ausführen von <> aber leer, so wird ein "-" als Ersatzdateiname erfunden. Und das bedeutet in Perl (UNIX-üblich) wiederum STDIN. Anders ausgedrückt: <> und <STDIN> sind nur identisch, wenn das Perl-Programm ohne Parameter aufgerufen wurde. (Übrigens funktioniert auch <stdin>. Wer weiß, was das wieder für ein geheimnisvolles magisches Feature ist.)

print <>;       # kopiert die Standardeingabe
print while <>; # dasselbe zeilenweise (skalarer Kontext)

(In while-Schleifen (aber nur dort) wird das Ergebnis von <> in der Bedingung an $_ zugewiesen (das wiederum Default-Parameter für print ist.)) Noch zur Erklärung der beiden Zeilen:

  print <>;       # entspricht  @input = <>; print @input;
  print while <>; # entspricht  print $input while $input = <>;

Dieser Unterschied war hier mit "skalarer Kontext" und "zeilenweise" gemeint. (Das sollte inzwischen aber auch klar geworden sein.)

Wir müssen an dieser Stelle wissen, daß Perl auf Systemen, die den Zeilenwechsel durch "\r\n" darstellen, diesen bereits beim Einlesen von Dateien durch "\n" ersetzt und bei der Ausgabe wieder in "\r\n" umwandelt. Da es ja nicht unüblich ist, mit Windows erstellte Textfiles per binmode-FTP nach UNIX zu kopieren, hat das den Effekt, daß ein unter UNIX laufendes Perlscript von solchen Dateien verwirrt werden kann. (Obwohl UNIX-Tools normalerweise damit zurechtkommen: Editiert man z.B. von Linux aus eine Textdatei in der Windows-Partition, so stören die \r weder, noch gehen sie verloren. Auch wenn man die Datei in eine Linux-Partition kopiert, bleiben die \r natürlich erhalten -- und werden dort nur von Perl nicht erwartet.)

Statt

while (<>) { print if /Keks$/ }

heißt es zum Kopieren der mit "Keks" endenden Zeilen korrekt demnach:

while (<>) { print if /Keks\r?$/ }

Oder, wenn man nicht in jedem Pattern für '$' ein '\r?$' schreiben möchte:

while (<>) { $hilf=$_; s/\r//; print $hilf if /Keks$/ }

(In der Regel wird man diese Problematik in Perl-Skripten aber einfach ignorieren. Die meisten Perl-Skripte überstehen ohnehin kaum mehr als ihr mitgeliefertes Beispiel. Auch das ist Perl!)

Der (hier auf den Default Parameter $_ angewendete) Substitutionsoperator (s///) liefert in skalarem Kontext übrigens die Anzahl der Ersetzungen zurück. Bei 0 Ersetzungen allerdings undefiniert, <Zynismus>denn 0 ist ja kein Wert und eignet sich nicht zum Zählen. (Das ist sehr intuitiv, weil ja z.B. auch unser Kalender mit dem "Jahr undefiniert" beginnt, gefolgt von "Jahr 1".</Zynismus> (<Werbung>Siehe hierzu auch in http://www.uni-bremen.de/~werres/oldjahr1000wechsel.html .)</Werbung>)

Wir müssen nun

while (<>) {
  s/&lt/</g; s/&gt/>/g; s/&amp/&/g;
  print, next if s|^<pre>|| .. s|^</pre>||;
  s/<[^ ].*?>//g unless /pre>/;
  print("# ") unless /^$/; print
}

Mit "perl diesskript < perlschnellkurs.html > perlschnellkurs.pl" kriegt man nun das gewünschte Skript. (Nein, kriegt man nicht. Jedenfalls nicht mehr von dieser Version mit Anmerkungen ausgehend. Dazu ist das Beispiel viel zu rudimentär. Für ordentliches HTML-Parsing siehe man bei CPAN nach.)

Wir wollen stattdessen mit einem kompletten Beispiel abschließen, in dem wir das Toolkit Tk mit Perl benutzen. Wer ungefähr ahnt, wie Tk funktioniert (es ist das Tk aus Tcl/Tk), kann susie.pl auf Basis unserer bisherigen Erkenntnisse ohne weitere Kommentare lesen.


27.04.97 (Anmerkungen: 28.03.98) -- York Werres (werres at nexgo.de).