Roles und Signaturen mit Moose: Moderne Erweiterungen für Perl 5
Mit Moose begann eine neue Zeitrechnung für klassenbasierte Objekte in Perl. Doch dank der Spracherweiterung fanden auch Signaturen und Roles (Traits) eine weite Verwendung in Perl-Programmen und sind auch deshalb auf ihrem Weg in den Sprachkern.
- Herbert Breunung
Mit Moose begann eine neue Zeitrechnung für klassenbasierte Objekte in Perl. Ein früherer Artikel berichtete, wie es dazu kam, und beschrieb in galaktischen Beispielen die grundlegenden Schreibweisen für Attribute, Methoden, Variablentypen und Vererbung. Doch dank Moose fanden auch Signaturen und Roles (Traits) eine weite Verwendung in Perl-Programmen und sind auch deshalb auf ihrem Weg in den Sprachkern.
Zu Beginn wurde der Elch (engl. Moose) eine Spracherweiterung genannt. Dessen Geweih sollte das Aussehen des guten alten Kamels deutlich ändern. Aber Moose ist mittlerweile vorderster Ausdruck einer Reformbewegung. Das Modul führt die Beliebtheitswertung des MetaCPAN knapp vor dem angesagten Web-Framework Mojolicious an. Auch mit der Anzahl seiner Erweiterungen können sich nur vier von über 25.000 Distributionen messen. Es löst nunmal das drängendste Problem, das Perl 5 hatte. Das ist auch in den 361 Vorschlägen zur Gestaltung von Perl 6 (aus dem Jahr 2000) nachzulesen, von denen sich 35 mit objektorientierter Programmierung befassen, wie etwa die RFC 126.
Doch da die neue Sprachgeneration allerhand Aufgabenstellungen zu lösen trachtet, gab es die Rückportierung der schönen neuen Objekte nur im Verbund mit einigen, bis dahin ungewohnten Konzepten. Dazu gehören merkwürdigerweise Signaturen.
Steinzeit am Apparat
Wenn auswärtige Programmierer zum ersten Mal Perl-Code lesen und dabei beobachten, wie die Argumente jeder Routine einzeln oder komplett aus der Spezialvariable @_ gefischt werden, kann das einen mittelschweren Kulturschock auslösen. Immerhin wurden Signaturen bereits 1958 mit Fortran II eingeführt, waren also selbst 1987 nicht so neumodisch wie diese speicherhungrigen Objekte. Die Perl-Größe Paul Fenwick sagte dazu 2013 in einem Vortrag: "Die Steinzeit rief an und wollte ihre Methoden-Signaturen zurück."
Das Grundproblem ist dem der Perl-5-Objekte ähnlich. Funktional lässt sich alles erreichen – nur mit mehr Wissen und Schreibarbeit als nötig. Referenzen aller Art lassen sich weiterreichen und benannte Parameter durch Hashes simulieren.
sub beschleunigen {
# erster Parameter ist eine Hashref
my $paramter = shift;
say $paramter->{'geschwindigkeit'};
...
Eine Art Signatur steht meist in der zweiten Zeile der Routine.
my ($laenge, $breite, $hoehe) = @_
Und mit dem seit Perl 5.10 (2007) bekannten "defined or"-Operator (//) werden Default-Werte übersichtlich im Routinenkopf angeordnet.
my $msg = shift // 'alternativlos';
my $pos = shift // {20 , 10, 406};
Doch die Tücken beginnen, wenn der nächste Schrägstrich vergessen wird:
beschleunigen(\%parameter);
Dann enthält @_ paarweise alle Schlüssel und Werte des Hash, ununterscheidbar von etwaigen weiteren Parametern. Aber um fair zu bleiben – mit Prototypen lässt sich das verhindern. (Wenn der Aufruf nicht vor der Definition der Routinen erfolgt, was sogar unbemerkt durchgeht, folgt man nicht der besten Praktik, "use warnings;" einzusetzen.)
sub beschleunigen (\%) {
...
Diese runden Klammern umgeben keine Signatur, sondern den sogenannten Prototyp. Er bestimmt, wie viele Argumente beschleunigen erwartet und welchen Perl-Typ sie besitzen müssen (Skalar, Array, Hash, Coderef, beliebige Referenz). Im Beispiel funktioniert dann einzig:
beschleunigen( %parameter );
Zusätzliche oder andere Argumente als eine Hash-Variable lösen einen Compiler-Fehler aus. Innerhalb der Routine enthält @_ eine Hash-Referenz.
Das war eine Masse an Informationen, mit denen sich kein Anfänger belasten mag, der lediglich zwei benannte Parameter braucht. Will er zudem sicherstellen, dass sie eine Zeichenkette und eine natürliche Zahl enthalten, muss er die Überprüfungen selbst schreiben oder wenigstens organisieren, was zweifelsfrei nicht die angepriesene, "perlige" Bequemlichkeit darstellt.
Deswegen schuf Michael G. Schwern (bekannt als Vater wichtiger Testmodule) Method::Signatures, das alle Signaturen-Funktionen von Moose bereitstellt, jedoch wesentlich schneller startet und auch für Programme ohne Objekte nützlich ist. Routinen mit Signaturen nannte Schwern func, damit bestehender Code nicht beeinträchtigt wird. Fast überflüssig zu erwähnen, dass Method::Signatures ebenfalls zu den 100 beliebtesten Modulen gehört.
Peter Martini bereitet derzeit sogar den Interpreter darauf vor, "echte" Signaturen zu unterstützen. Zusammen mit der von Stevan Little wiederaufgenommenen Arbeit an einem Metaobjektprotokoll für Perl 5 ist das nach Einschätzung des Autors die derzeit bedeutendste Entwicklung. Sie hat das Potenzial, weit mehr zu verändern als die letzten drei oder vier Perl-Versionen zusammen. Mit einem MOP im Kern ließen sich die wichtigsten Moose-Funktionen per Pragma (wie strict) mit use zuschalten, und Moose selbst ließe sich spürbar entschlacken und beschleunigen.
Dann fehlt nur noch ein Sprachstandard für eine moderne Ausnahmebehandlung, die sich derzeit mit einem von circa zehn Modulen (zum Beispiel Try::Tiny) nachrüsten lässt. Damit hätte Perl 5 einen Stand erreicht, den sich viele als Perl 6 vorgestellt haben.
Rollen
Rollenverteilung
Doch Larry Wall hat noch weit größere Ambitionen. Zur diesjährigen YAPC::EU in Kiew stellte der Perl-Erfinder in seiner Keynote erneut klar, dass sich Perl 6 nur nennen dürfe, was möglichst alle grundlegenden Aufgaben der Programmierer besser löst als Perl 5, inklusive der Organisation von Abstraktion. In den 80er-Jahren kamen dafür Objekte in Mode. Sie wurden teils übertrieben als das Mittel zur Produktivitätssteigerung beworben. Denn wie die gefürchteten Polsterjacketts dieser Ära ist der Umgang mit Klassen, und vor allen ausufernden Klassenhierarchien, klobiger als erhofft. Der Aufstieg (teil-)funktionaler Sprachen wie Scala, F# oder Haskell und die Einführung funktionaler Mittel in Sprachen wie C# und Java (Lambdas) ist ein Symptom dafür. Andere Ansätze sind Aspekte und Traits. Besonders letztere finden gerade Einzug in zunehmend mehr Sprachen, da sie ein fundamentales Problem von Klassen angehen.
Da Klassen in eine strikte Vererbungshierarchie eingebunden sind, ist die Wiederverwendung ihrer Attribute und Methoden nur eingeschränkt möglich. Bisher stand man vor der Wahl, sich entweder mit einer Einfachvererbung schmerzlich einzuschränken und Codeduplikation quasi zu provozieren. Oder man lockerte die Schlinge etwas und fing sich Konflikte mit kollidierenden Namen ein.
Traits helfen hierbei aus, weil sie außerhalb der Klassenhierarchie stehen. Eine Klasse kann beliebig viele Traits konsumieren (ihre Methoden aufnehmen) und Konflikte durch Ablehnen oder Umbenennen auflösen. Das lässt sich mit Einfach- und Mehrfachvererbung kombinieren.
Die diesem Konzept ähnlichen Mixins können den Widerstreit zweier gleichnamiger Methoden nicht schlichten. Das war wohl der Hauptgrund für die mit Ruby 2.0 eingeführten Refinements, die das stille Überschreiben von Methoden lexikalisch begrenzen. Die Begriffe Mixin und Trait werden jedoch auch abweichend verwendet (wie bei D), da es keine offizielle Konvention dafür gibt.
Perl 6 (und Moose) vermied es, zu dieser Sprachverwirrung beizutragen und nannte seine Traits "Roles" (Rollen), da sie im Gegensatz zu bisherigen Entwürfen auch Attribute enthalten oder einfordern dürfen. Das macht Sinn, weil funktionell verwandte Methoden normalerweise über gemeinsam verwendete Attribute kommunizieren, die sonst in jeder, den Trait verwendenden Klasse neu zu schreiben wären. Konflikte zwischen gleichnamigen Attributen verschiedener Rollen lassen sich derzeit nur in Perl 6, nicht aber mit Moose lösen. Sie werden jedoch wie Methodenkonflikte zur Kompilierzeit erkannt und erzeugen Fehler.
Rollen sind (möglichst kleine) Einheiten der Wiederverwendung, Klassen (möglichst vollständige) Einheiten der Komposition und Instanziierung. Folglich lässt sich aus einer Rolle nicht direkt ein Objekt bilden, wohl vermögen Objekte zur Laufzeit Rollen zu konsumieren. Eine andere Konsequenz dieser Arbeitsteilung ist: Abstrakte Klassen werden kaum noch benötigt und Vererbungshierarchien wesentlich flacher oder entfallen ganz. Da Rollen Methoden anfordern können, die sie selbst nicht implementieren, erfüllen sie somit auch die Aufgabe eines Interfaces. Einzig wenn die Implementierung an eine abgeleitete Klasse zu delegieren ist, braucht es abstrakte Klassen. Denn wenn eine Rolle "Handbremse" die Methode "stop" fordert, dann muss sie von der Klasse oder einer ihrer Rollen zum Zeitpunkt des Einfügens implementiert sein, um den Compiler-Fehler zu vermeiden.
Weil Rollen durch die vorgestellten Methoden-Modifikatoren befähigt sind, Methoden zu erweitern, können sie auch als Aspekte fungieren.
In der Rolle Captain Gideons
Das letzte Beispiel des ersten Teils erwähnt die Klasse "Excalibur", die von "Raumschiff" und "White Star" erben müsste. Eine weitere Besonderheit gegenüber gewöhnlichen Raumschiffen ist ihre auf Vorlonentechnologie basierende Hauptkanone.
package VorlonenKanone;
use Moose::Role;
requires 'energielevel';
has 'ladezustand' => (is => 'rw', isa => 'Num');
sub feuer { ...
Äußerlich sieht es fast wie eine Moose-Klasse aus. Es ist kein base oder parent zu bemühen, da die Ableitung von Moose::Role hinter dem Vorhang geschieht, wie bei use Moose; in Klassen auch. Durch requires wird das Attribut energielevel verlangt, (das ja auch "nur" eine Methode ist), da es von der Methode feuer verwendet wird.
Da die Serie leider vorzeitig abgesetzt wurde, blieb die Excalibur ein Einzelstück, weswegen die Rolle nur einem einzelnen Raumschiff zugewiesen werden braucht.
use Moose::Util qw( apply_all_roles );
my $excalibur = WhiteStar->new;
apply_all_roles( $excalibur, 'VorlonenKanone' );
Doch in einem besseren Paralleluniversum gibt es sicherlich eine Klasse neuartiger Ranger-Schiffe, in die die Rolle mit with eingefügt wird:
package Excalibur;
use Moose;
extends 'Raumschiff', 'White Star';
with 'VorlonenKanone', 'Gravitationsgenerator';
...
Der von den Centauri bereitgestellte Gravitationsgenerator sollte im gleichen Arbeitsgang eingebaut werden, da sich sonst Namenskonflikte nicht erkennen lassen. Das liegt an der derzeit noch begrenzten Fähigkeit von Perl 5, das Ende des BEGIN {...}-Phasers zu erkennen und gegebenenfalls zu diesem Zeitpunkt nach Konflikten zu suchen. Durch das Einfügen der Rollen in mehreren Schritten kann man der Kontrolle bewusst aus dem Weg gehen und selbst ungewöhnliche Konstrukte erzeugen. Dann gilt nur die Methode, die zuerst eingefügt wurde. Im Regelfall werden Konflikte jedoch mit einer der beiden folgenden Techniken behoben: Besitzen beide Rollen zum Abschalten die Methode aus, wäre ein deklaratives Umbenennen sinnvoll.
with 'VorlonenKanone' => {
-alias => { aus => 'kanone_aus' },
-excludes => 'aus'
},
'Gravitationsgenerator'=> {
-alias => { aus => 'gravitation_aus'},
-excludes => 'aus'
};
Da -alias nur einen Alias erzeugt, ist der Import des alten Namens zusätzlich zu unterdrücken. Der andere Lösungsweg wäre eine Methode aus in der Klasse Excalibur, die Zugriff auf VorlonenKanone::aus($self); und Gravitationsgenerator::aus($self); hätte, selbst wenn ihr Import unterdrückt würde. $self muss überreicht werden, da die Methoden sonst keinen Zugriff auf die Attribute hätten.
Mit MooseX::Declare bekommen die Beispiele den futuristischen Perl-6-Anstrich:
use MooseX::Declare;
role VorlonenKanone {
requires 'energielevel';
has 'ladezustand' => (is => 'rw', isa => 'Num');
method feuer { ... }
}
class Excalibur extends Raumschiff extends White Star
with VorlonenKanone with Gravitationsgenerator {
...
}
Hier dürfen die Namen der Klassen und Rollen nicht in Anführungszeichen gesetzt werden, bei einem nachträglichen extends oder with im Klassenblock müssen sie es schon. Da MooseX::Declare Devel::Declare verwendet, um die zusätzlichen Schlüsselworte einzufügen, hat es bis zum nächsten Semikolon volle Gewalt über den Parser und kann auf die leicht störenden Zeichen verzichten. Das gilt nicht für die normalen, aus Moose importierten Subroutinen wie extends, with oder has. Diese werden, wie erwähnt, zu Beginn der Laufzeit ausgeführt, weshalb deren Reihenfolge wundervolle Streiche spielen kann. Steht statt des entscheidenden with nur eine Zeile oberhalb der Methode, die von der Rolle verlangt wird, ist der Compiler unerbittlich. Deshalb ist ein with gut am Boden einer Klasse aufgehoben, noch besser, weil narrensicher, ist jedoch die eben gezeigte Form mit MooseX::Declare, die ebenfalls with am Ende aufruft und zudem deklarativer ist. Die wesentlichen Informationen zu einer Klasse sollten am Anfang stehen.
Diese gehen auch nicht verloren. Jederzeit lässt sich das Raumschiff zu seinen Bauteilen befragen:
# von dieser Klasse abgeleitet ?
$excalibur->isa('Raumschiff');
# enthält diese Rolle ?
$excalibur->does('VorlonenKanone');
# enthät diese Methode ?
$excalibur->can('feuer');
Die Antwort wäre jedes Mal "1", und auch die offizielle, mit Perl 5.10 in UNIVERSAL eingeführte Methode, um nach einer enthaltenen Rolle zu fragen, wird unterstützt.
$excalibur->DOES('VorlonenKanone');
does ist das Schlüsselwort in Perl 6 für diese Überprüfung und dient der Injektion von Rollen in eine Klasse oder ein Objekt.
Da wie gezeigt jede Klasse der Typ eines Attributs sein kann, lässt sich der Compiler auch hier für Fragen nach konsumierten Rollen einspannen.
class AntiDrakFlotte {
has 'flaggschiff' => {
is => 'rw',
isa => 'Excalibur',
does => 'VorlonenKanone',
}
...
Das ist nicht unwichtig, da besagte Rolle vielleicht über eine Elternklassen oder eine andere Rolle aufgenommen wurde.
So etwas wie die hohe Schule sind parametrisierte Rollen, die Perl 6 von sich aus kennt und Moose mit dem in MooseX::Declare enthaltenen MooseX::Role::Parameterized. Eigentlich sind es nur etwas flexiblere Rollen, die auf anfänglich überreichte Parameter reagieren. Verständlicherweise sind die Parameter vor der eigentlichen Rolle zu definieren.
package Antrieb;
use MooseX::Role::Parameterized;
parameter typ => (
isa => (enum ['Quanten', 'Antimaterie', 'Plasma']),
default => 'Plasma',
);
role {
my $p = shift;
if ($p->typ eq 'Quanten') {
method energie => \&QuantenStrahl::an;
method stop => \&QuantenStrahl::aus;
}
elsif ($p->typ eq 'Antimaterie') {
method energie => \&AMTriebwerk::fusion;
method stop => \&AMTriebwerk::trennen;
}
...
}
Ansonsten enthält das Beispiel wenig Neues, da parameter sich die Syntax mit has teilt. Dass benannte Parameter in einer Hashref ankommen, die mit shift aus @_ gezogen wird, ist ebenfalls tägliches Perl-5-Geschäft. Einzig bemerkenswert ist die Schreibweise zum Umleiten der Methodenaufrufe. In der Klasse wird der Antriebstyp wie bekannt weitergereicht.
package Excalibur;
...
with Antrieb => { typ => 'Antimaterie' };
...
Miniatisierung
Miniaturisierungswettstreit
Doch für viele ist das zu schwere Technik. Moose braucht seine halbe Startsekunde auf moderner Hardware und 5 MByte RAM ohne Zusatzmodule wie MooseX::Declare, das etwa gleichstark zulangt. Kein Vergleich mit den weit unter 12 kB von Role::Tiny, dessen Abhängigkeiten sich auf eine Handvoll winziger Kernmodule und die 4 kB vom erwähnten Try::Tiny erstreckt. Aber aus diesem winzigen Paket bekommt der Anwender requires, with, does, die Methodenmodifikatoren und apply_roles_to_object zum Anreichern der Objekte zur Laufzeit. Da diese Methoden sub geschrieben werden, lassen sie sich mit apply_roles_to_package in beliebige Namensräume exportieren. Lediglich in der Klasse wird es nur wenig umständlicher:
package Excalibur;
use Role::Tiny::With;
with 'VorlonenKanone', 'Gravitationsgenerator';
Um aus diesem Namensraum Excalibur eine Klasse zu machen, darf man zwischen der alten Schule und einem beliebigen Objektsystem wie Class::Tiny wählen, dass sich syntaktisch ebenfalls dicht an Moose hält.
Mouse tritt gerade in seiner Bedeutung etwas zurück, sodass es beinah kompatible Objektsysteme grob in drei Gewichtsklassen gibt: winzig (Class::Tiny, Role::Tiny), klein (Moo) und ausgewachsen (Moose) Interoperabilität und Migrationsfähigkeit dieser Module wurde in den letzten Monaten spürbar verbessert (siehe Moo::late ). Der Tradition Acme-Humors folgend, (die 2004 mit dem Objektsystem Spiffy seinen Anfang nahm), gibt es noch "Mo" (was in vier Tweets passt) und M, das lediglich diesen skurrilen Wettbewerb gewinnt. Aber von den stillen Freuden der eingefleischten Perl-Hacker abgesehen, bildete sich hier eine Infrastruktur mit großem praktischen Nutzen, die es erlaubt, die Kosten und Fähigkeiten der Objekterzeugung frei zu wählen. Gleichzeitig renoviert sie die wichtigsten Baustellen von Perl 5 und erleichtert es, Perl 6 zu erlernen, die sich anbahnende Sprachgeneration mit gleichem kulturellen Hintergrund. Neueste Detailoptimierungen umfassen MooseX::LvalueAttribute, das statt des leicht umständlichen
$excalibur->geschwindigkeit(2475);
$excalibur->geschwindigkeit = 2475;
gestattet, jedoch nur, wenn das Attribut als "lvalue" (zuweisungsfähig) gekennzeichnet wurde:
...
has geschwindigkeit => (
traits => ['Lvalue'], ...
Eine erfreuliche Entwicklung ist auch Moops, das mit Class::Tiny bis Moose zusammen- arbeiten kann und im Wesentlichen das Gleiche bietet wie MooseX::Declare. Es greift für die Einführung der Schlüsselworte (with, extends usw.) jedoch auf eine neuere und bessere API zurück und funktioniert deshalb nur mit Perl 5.14 oder neuer. Es ist wesentlich schlanker, trotz einiger zusätzlicher Zuckerl wie Try::Tiny, Type::Tiny sowie dem gefühlten besten neueren Entwicklungen aus Moose und Moo. Bevor dieser Artikel jedoch zu sehr in die Breite geht, schließen sich besser die Hangartore mit dem Hinweis auf mögliche, kommende Neuigkeiten, die sich noch vor ihrer Veröffentlichung hier einsehen lassen.
Herbert Breunung
schreibt regelmäßig Artikel über Perl 5, Perl 6 und GUI-Programmierung und spricht darüber auch auf Konferenzen im In- und Ausland. Außerdem führt er ein freies Softwareprojekt und den Aufbau einer hypertextfähigen Perl-6-Dokumentation.
(ane)