Zurück | Übersicht | Weiter

Perl 6 Tutorial - Teil 5 : Captures und Subroutinen

Willkommen zum fünften Teil dieses ausführlichen Perl 6-Tutorials, der sich nur mit dem kleinen sub - Befehl beschäftigen wird und einige Dingen die dazu gehören. Subroutinen oder Funktionen, die in Perl 6 immernoch mit sub deklariert werden, gehören zu den wichtigsten und grundlegendsten Techniken der Programmierkunst. Es sind Teilprogramme die mit einem möglichst aussagekräftigen Namen aufgerufen werden. Meist werden ihnen Werte übergeben und oft liefern sie auch ein Ergebnis zurück. Jeder der schon etwas Perl kennt, hat bereits Ähnliches geschrieben wie folgende Funktion, welche die Länge der Hypotenuse berechnet.

   sub hypotenuse {
      my ($a, $b) = @_;
      sqrt( $a**2 + $b**2 );
   }

Und die gute Nachricht für Lernfaule lautet: Dies ist vollständig gültiges Perl 6. Menschen die jedoch gerade von C oder Java zu Perl wechseln, hätten die Subroutine wohl so geschrieben:

   sub hypotenuse ($a, $b) {
      return sqrt( $a*$a + $b*$b );
   }

Und die gute Nachricht für diese Neulinge lautet: Dies ist ebenfalls vollständig gültiges Perl 6. Die zweite Lösung liegt näher an dem was die meisten anderen Sprachen da draußen betreiben (entspricht häufig den Lesegewohnheiten) und sie spart mühevolle Fingerbewegungen (ebenfalls ein Indikator ob etwas perlish ist). Aber was genau wurde geändert? Folgen dem sub - Namen runde Klammern, definiert das eine Signatur (Liste der Parameter) und keine Prototypen mehr wie in Perl 5. Diese Parameter (auch @_) sind keine lokalen Variablen und (wenn vorher nicht anders deklariert) schreibgeschützt. Folgt keine Signatur, landen Parameter in @_, wie aus Perl 5 gewohnt. TIMTOWTDI.

Geforderte Parameter

Signaturen helfen jedoch nicht nur Neulingen, sondern nehmen Jedem Arbeit ab. Zum Beispiel kann sub hypotenuse nur dann sinnvolle Ergebnisse erzielen, wenn sie 2 Parameter bekommt. Im alten System der Parameterübergabe wäre es aufwendig dies einzufordern. In Perl 6 braucht der Programmierer dafür gar nichts zu tun, denn würde man die zweite Routine mit hypotenuse(5) oder hypotenuse(2,3,4) aufrufen, gäb es eine dicke Fehlermeldung zur Kompilierungszeit. Wäre die sub wie im ersten Beispiel implementiert, würde der Kompiler lautlos schnurren wie ein Kätzchen, auch wenn die Ergebnisse der Funktion nicht immer brauchbar wären. Doch wenn schon die Parameter prüfen, dann auch auf den Datentyp. Num ist ein nativer Datentypen für Skalare und entspricht dem Zahlenbereich rationaler Zahlen.

   sub hypotenuse (Num $a, Num $b) {
      return sqrt( $a*$a + $b*$b );
   }

Der Vorteil einer solchen Prüfung: die Fehlermeldung kommt, wenn Unbeabsichtigtes passiert (hier bei nichtnumerischen Längenangaben), nicht erst Zeilen später, wenn ein Folgebefehl versagt. Somit sind Ursachen wesentlich leichter auffindbar. Und um konsequent zu sein sollte die sub hypotenuse nur Zahlen größer als 0 zulassen, da sie mit physischen Längen rechnet. Dazu definieren wir den eigenen Datentyp Num+ (analog zu Q+) als Untermenge von Num wie folgend:

   subset Num+ of Num where { $_ > 0 };

Perfekt wäre es, wenn auch der Typ des Rückgabewertes festgelegt werden könnte. Das wirkt in diesem Beispiel vielleicht etwas übertrieben, aber Programme werden so zuverlässiger. Diese Art der Programmierung, bei der der Compiler die Anzahl, und Typen der Ein- und Ausgabe einer Routine prüft, wird Design by Contract genannt und wurde zuerst durch die Sprache Eiffel bekannt. Da Perl 6 Motto heißt "All your Paradigm's belong to us" heißt, ist das beschriebene auch hier möglich:

   Num+ sub hypotenuse (Num+ $a, Num+ $b) { ... }
   sub hypotenuse (Num+ $a, Num+ $b --> Num+) { ... }

Bla Bla Bla

Die "..." in den letzten Beispielen waren nicht nur Dekoration die andeutet, daß an dieser Stelle der eigentliche Code später eingefügt wird. Auch der Perl 6 versteht es in dem Sinne. Der Interpreter geht davon aus, daß der Programmierer nicht ohne Grund beginnt eine Subroutine zu schreiben und möchte ihm mit einem freundlichen Kompilererror daran erinnern, daß im Falle:

   sub hypotenuse;    # oder
   sub hypotenuse { }

etwas fehlt. Der geschickte Softwarearchitekt kann aber mit dem Operator namens yadayadayada (zu deutsch bla bla bla) dem Interpreter mitteilen, daß er später die Routine schreiben wird. Je nachdem ob er möchte das die jetzige Routine ein fail, eine Warnung ( warn), oder einen Error ( die) liefert, kann er die Schreibweisen ..., ??? oder !!! verwenden. Doch kehren wir zurück zum ersten Beispiel.

Optionale Parameter

Manchmal braucht es aber Routinen, die je Situation unterschiedlich viele Parameter bekommen. Wie deklarier ich das in der Signatur, ohne daß sich der Interpreter beschwert? In dem ich den Paramtern ein Fragezeichen anhängt und sie damit als optional markiert.

   sub hypotenuse (Num+ $a, Num+ $b, Str $txt? --> Num+) {
      my $h = sqrt( $a*$a + $b*$b );
      say "$txt $h." if $txt;
      return $h;
   }

Man muß nur darauf achten, in der Signatur die optionalen Parameter nach den Notwendigen zu positionieren, da alle Parameter in der Reihenfolge befüllt werden, in der sie der Routine übergeben werden. Deshalb sollte auch die Reihenfolge der optionalen Parameter sorgfältig überdacht werden, um sie von links nach recht von mehr zu weniger wichtig zu sortieren. Der Befehl if $txt wird auf keinen Fall Probleme bereiten. Denn auch wenn kein Antwortsatz gewünscht ist und $txt leer bleibt, wird die Variable auf jeden Fall mit undef initialisiert. Um andere default-Werte festzulegen könnte man schreiben:

   Num+ sub hypotenuse (Num+ $a, Num+ $b, Str $txt?) {
      my $h = sqrt( $a*$a + $b*$b );
      $txt //= 'Länge:
      say "$txt $h.";
      return $h;
   }
oder
   Num+ sub hypotenuse (Num+ $a, Num+ $b, Str $txt = 'Länge: ') {
      my $h = sqrt( $a*$a + $b*$b );
      say "$txt $h.";
      return $h;
   }

Da optionale Parameter auch an der Zuweisung erkannt werden, darf das Fragezeichen im letzten Beispiel weggelassen werden. Das Gegenteil des Fragezeichens ist das Ausrufezeichen (siehe dem Bedingunsoperator C) Deshalb werden notwendige Parameter mit einem angehängten C<$var!> deklariert, was aber bei positionalen Parametern nicht notwendig, da default ist.

Schlürfende Parameter

Manchmal ist es aber auch praktisch eine unbekannte Anzahl von Parametern in einem Array zusammenzufassen. Dies erreicht man durch einen Stern als Präfix:
   sub summe (*@a) { [+] @a }
Aber auch Hashes und Skalare aller Art dürfen als "slurpy" deklariert werden. Im folgenden Beispiel implementieren wir Perl's map-Funktionen. Mit dem Unterschied, daß bei unserem map der anonyme Block an beliebiger Stelle stehen darf und der der Interpreter anhand der Signatur die Parameter passend zuordnet.
   sub map (*&code, *@werte) {
      return gather for @werte -> $wert {
         take $code($wert);
      }
   }
Würde die Sigantur (*@werte, *&code) lauten, müsste der Block immer an letzter Stelle übergeben werden.

Benannte Parameter

Wie angedeutet waren alle bisherigen Parameter positional, richten sich also nach der Reihenfolge im Funktionsaufruf.
   sub hypotenuse ($a, $b) {
      sqrt( $a*$a + $b*$b );
   }
Selbst bei einem Aufruf wie hypotenuse($b, $a) würde der Inhalt der Variable $b in den Parameter $a kopiert werden und analog Variable $a in $b. Es gibt jedoch sehr gute Gründe Parameter manchmal direkt beim Namen anzusprechen. Der Quellcode wird nachvollziehbarer und nicht jeder kann sich die Reihenfolge von 12 Parametern für jede Routine oder Methode merken. Oft ist auch nicht die Eingabe von jedem Parameter notwendig, aber wie sag ich's dem Computer, daß ich diesmal gerne die Parameter 3, 5 und 12 übergebe. Selbst die bereits vorgestellten optionalen Parameter waren positional. Aus der Überlegung erschließt sich auch warum in Perl 6 benannte Parameter per default optional sind. Wie bekannt kann das angehängte ! sie erzwingen. Benannte Parameter erkennt man am Präfix :. Und sehr zu beachten: sie folgen den positionalen Parametern in der Signatur.
   sub new(:$parent, :$ID, :$value :@size, :@pos, :@item, $:style)
Es gab Moment, da wünschte ich mir beim WxPerl-Programmieren Perl 6 wäre schon da und z.B. die new=-Methode einer Combobox hätte eine solche Signatur. Weil beim erzeugen des Widget sind bei mir nur die Eltern (Platz in der Objekthierarchie) und der Sytle (Aussehen und Verhalten) wichtig. Die ID lass ich autogenerieren, Größe und Position bestimmen die Sizer und Textwert und die Item werden bei Bedarf gesetzt.
   my $cb = Wx::Combobox.new( parent => $win, :style($style) );
So würde mir das gefallen. Zu Demonstrationszwecken enthält das letzte Beispiel beide Paar-Schreibweisen. Sie wurden bereits in der vorigen Folge erläutert. Nur ließe sich hier =:style($style)
auch zu :$style zusammenfassen.

Hat eine Routine keine Signatur, erhält sie ihre mt Namen zugewiesenen Parameter aus %_, so wie @_ nur die positionalen Parameter enthält. Verwendet man Platzhalter-Variablen wie $^a in Routinen ohne Signatur (obwohl die nur für einfache Blöcke gedacht sind), erscheinen die Werte dieser Variablen nicht mehr in %_ oder @_.

Möchte man ein Paar als positionalen Parameter angeben, muß er in runde Klammern gesetzt werden. Wie nachfolgend zu sehen, kann man die umschließenden Klammern einer Signatur meist weggelassen werden.
   # Aufruf mit einem positionalem Argument
   Wx::Combobox.new (:parent<$win>), ;
Ich weiß auch: Tk und viele andere Module kennen heute bereits benannte Parameter. Die Übergabe eines anonymen Hashes hat zumindest optische Ähnlichkeiten, Typen und Anzahl der Geforderten Parameter werden dabei nicht geprüft. In Perl 6 könnte man aber auch eine Routine mit einem Hash aufrufen. Damit Perl die Paare des Hashs als Name und Wert benannter Parameter wertet muss der mit einem senkrechten Strich dereferenziert werden.
   Wx::Combobox.new( |%default );
| ist keine Sigil für einen Datentyp, so wie ein & für Codereferenzen steht, es dient lediglich zum Interpolieren in den Capture-Kontext, vergleichbar mit @@, das für den bereits behandelten slice- oder auch multislice-Kontext steht.

Was zum $@%& sind Capture?

Von allen Neuheiten in Perl 6 fordern Capture wohl am stärksten die Fähigkeit sich neue Nervenverbindungen wachsen zu lassen, denn soweit mir bekannt, gibt es nichts Vergleichbares in anderen Sprachen. Ein Capture ist ein Datentyp, der einem Skalar zugewiesen wird und alle Parameter eines Routinenaufrufs speichern kann. Da Signaturen sowohl positionale als auch benannte Parameter haben können, erscheinen Capture anfangs als seltsame Hybride aus Array und Hash. Nur anders als die Letztgenannten kann eine Capture nicht nachträglich verändert werden. Sie ist immutable wie eine Liste. Weil es jetzt keine Referenzen mehr gibt bekamen Capture den \ vererbt, der von nun an capture composer heißt.
   my (@a, $b, %c) = [1 .. 5], 6, {'sonnen' => 'schein'};
   $capture = \(@a, $b, %c);
Diese Capture enthält keine Referenzen auf die Variablen sondern nur die Inhalte.

Anstatt zu referenzieren kann man in Perl 6 einen Alias auf eine Variable in der Symboltabelle erstellen. Dies geht mit einem sehr einfachen Syntax und gänzlich ohne Typeglobs.

   $alias := $kathete;
   $kathete = 5;
   say $alias;         # ist 5
   # bindet während Kompilierung
   $alias ::= $kathete;

Multi Sub's

Erinnern wir uns des ersten Beispiels. Wollte man eine Routine schreiben die die Länge einer beliebigen Seite des rechtwinkligen Dreiecks berechnet, würden die bisher vorgestellten Mittel nicht ausreichen. Mit benannten Parametern wäre die richtige Zuordnung der Seiten gesichert, aber nicht die Forderung, daß 2 von 3 gegeben sein müssen. Eine Lösung bestünde darin das Problem aufzuteilen, was Perl völlig neue Möglichkeiten eröffnet:

   multi sub pythagoras (:$kathete!, :$kathete!) {
      sqrt(@kathete[0]**2 + @kathete[1]**2);
   }

   multi sub pythagoras (:$kathete!, :$hypotenuse!) {
      sqrt($hypotenuse**2 - $kathete**2);
   }

Das Schlüsselwort multi kündigt an, daß es mehrere Routinen gleichen Namens gibt. Mit einem only könnte ausschließen, das nachträglich noch eine multi zu einem Namen deklariert wird. Das ist aber meist nicht notwendig, da normale sub per default only sind.

Wird die Routine mit pythagoras( :kathete<3>, :hypotenuse<5> ); aufgerufen, prüft der Interpreter welche Signatur zu den Parametern passt. Manch einer ahnt es schon. Auch dafür wird intern wie bei given / when der smartmatch benutzt. Es kann auch sehr praktisch sein selbst zu überprüfen ob ein Satz von Parametern bei einer Routine Erfolg gehabt hätte.

   $capture ~~ &routine.signature;

Parameter Traits

Alle bisherigen Paramter konnten in der Routine nicht verändert werden, was meist sinnvoll ist, aber zuweilen unpraktisch. In diesen Fällen können einzelne Parameter als veränderbar (rw steht für read/write) gekennzeichnet werden, was einer <-> Zuweisung in Pointy-Blocks entspricht.

   sub incr (*@vars is rw) { $_++ for @vars }

Der Befehl is definiert Traits (Charakteristiken) von Variablen. Das sind neben dem Inhalt zusätzliche Werte oder Eigenschaften die zur Kompilierungszeit Variablen gegeben werden können. Im Gegensatz dazu werden mit but Properties (zusätzliche Laufzeiteigenschaften) bestimmt. Somit wird der alte Perl 5-Witz "0 but True" lauffähiger Code.

Eine andere Möglichkeit veränderbare Parameter zu erhalten ist der Trait copy. Wie der Name aussagt sind solche Parameter veränderbare Kopien der übermittelten Variablen.

Eingewickelte Routinen

Es gibt sogar Situationen da muß man eine Signatur rückwirkend anpassen. Unsere großartige hypotenuse - sub könnte Teil eine Matematik-Bibliothek sein die wir benutzen wollen. Sie kann sogar die Hypotenuse berechnen, wenn ein Winkel und die gegenüberliegende Seitenlänge gegeben ist. Nur leider rechnet sie mit gon (Neugrad) und unser Programm mit Grad (Altgrad). Die Bibliothek zu verändern kommt nicht in Frage, da die Patches in jede neue fehlerreduzierte Version der Bibliothek eingepflegt werden müssten. Zum Glück gibt es auch dafür in Perl 6 eine elegante Lösung.

   sub hypotenuse($l, $winkel) {...}
   $handle = &hypotenuse.wrap( { callwith( $^l, $^winkel/360*400 ) } );
   # funktioniert einwandfrei
   hypotenuse(2,20);
   &hypotenuse.unwrap($handle);

Der letzte Befehl hebt die Umhüllung auf und es können selbstverständlich beliebig viele Umhüllungen stattfinden. Sind irgendwelche andere Vor- und Nachbereitende Tätigkeiten auszuführen und die Parameter sollen unverändert an die ursprüngliche Routine weitergereicht werden, kann man statt callwith auch callsame nehmen. Beide Befehle liefern die Ergebnisse der originalen Routine, die dann noch nach Wunsch nachbereitet werden können.

Rückgabekontext

Nachbearbeitungen werden aber oft vermieden, wenn die Routine auf den Kontext eingeht in dem sie gerufen wird. Das ist meist eine Signatur über alle Variablen, denen das Ergebnis der Routine zugewiesen wird. Diese Signatur erhält man mit dem Befel caller.want und man könnte ohne Damian Conways Modul Contextual::Return schreiben:

   given caller.want {
   when :($)       {...}   # Skalarkontext
   when :(*@)      {...}   # Arraykontext
   when :($ is rw) {...}   # Ein lvalue wird erwartet
   when :($,$)     {...}   # 2 Werte werden erwartet
   ...
}

Für all das gibt es auch noch eine andere Schreibweise.

   if    want.item     {...}  
   elsif want.list     {...}  
   elsif want.void     {...}  
   elsif want.rw       {...}  

Dieser Kontexte gibt es noch vieler mehr. Auch ist .want bei weitem nicht die einzigste Methode zur Introspektion, aber ich will hier kein Handbuch schreiben, sondern nur einige Möglichkeiten andeuten. Eine vollständige Auflistung aller Details soll das Tutorial in der Wiki unter http://wiki.perl-community.de/bin/view/Wissensbasis/PerlTafel werden. In diesem Beispiel wäre ein want anstatt caller.want ausreichend gewesen, aber würde der return - Befehl innerhalb eines Blocks stehen, wären caller.want und context.want verschieden.

return verlässt immer die innerste umgebende Routine. Wird lediglich gewünscht den Block zu verlassen, empfiehlt sich leave zu nehmen. Logischerweise wird nur der Rückgabewert von return gegen die Signatur der innersten Routine gematcht.

Module und Scope

Was wäre Perl ohne Module. Deshalb bietet Perl 6 auch für Modulauthoren etliche Verbesserungen zur Vorgängerversion. Da aber in diesem Bereich vieles noch nicht in trockenen Tüchern ist, jetzt nur einige Grundzüge. Mit package werden weiterhin Namensräume definiert, für Namensräume mit zusätzlichen Eigenschaften gibt es jetzt module. Der Befehl module Name; besagt, daß für den Rest der Datei der Namensraum Name gilt. Für mehrere Module in einer Datei schreibe man module Name{ ... }. So können Namensräume auch verschachtelt werden und mit my module Name { ... } Module sogar als lexikalisch lokal bestimmt werden. Analog dazu dürfen auch Subroutinen jetzt mit my lokal sein. Standartmäßig entspricht aber weiterhin ein sub routine {...} einem our sub routine {...}.

Eine der Hauptfähigkeiten von Modulen ist das Exportieren von Routinen. Dazu benötigt man kein use Exporter; mehr, sondern markiert die entsprechenden Routinen mit einer Trait als is export. sub - Traits haben ihre Position nach der Signatur. Module werden wie bekannt mit use oder require geladen, jedoch wurden auch diese Befehle wesentlich mächtiger um einige Probleme zu lösen die ein wachsendes CPAN mit sich bringt.

   use Dog:<1.2.1>;

So fordert man z.B. eine spezielle Version an. Noch genauer wäre:

   # bitte keine Version 1.2.7
   use Dog:ver(1.2.1..^1.2.7);

Auch ein optionaler Mechanismus zur Authentifizieren von Autoren ist im Syntax vorgesehen, jedoch noch nicht voll ausgereift.

Namensräume mit weitaus mehr Eigenschaften werden mit class erzeugt. Die OOP wird aber Stoff der nächsten Folge sein.


Zurück | Übersicht | Weiter

-- HerbertBreunung - 02 Jun 2009
Topic revision: r4 - 2009-12-02 - 20:23:36 - HerbertBreunung
 
Bitte die NutzungsBedingungen beachten.
Bei Vorschlägen, Anfragen oder Problemen mit dem PerlCommunityWiki bitten wir um WebBottomBarExample">Rückmeldung.