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