Corinna: Ein modernes und reifes Objektsystem für Perl 5

Die Programmiersprache Perl hat mit Corinna seit Version 5.38 ein ausgereifteres Objektsystem ohne die bisherige Schwachstelle der fehlenden Schlüsselworte.

In Pocket speichern vorlesen Druckansicht 18 Kommentare lesen

(Bild: Miriam Doerr, Martin Frommherz/Shutterstock.com)

Lesezeit: 14 Min.
Von
  • Herbert Breunung
Inhaltsverzeichnis

Mit der Version 5.38 stehen neue Schlüsselworte für die Objektorientierung (kurz OO) im Sprachkern von Perl zur Verfügung. Dies ist eine der tiefgreifendsten Erweiterungen seit Perl 5.0 (1994) und verdient daher eine genaue Analyse, die nebenbei etwas über die langfristige Entwicklung der Sprache aussagt. Aber auch für Nicht-Perl-Programmierer ist die Umsetzung der Neuerung durchaus erkenntnisreich, da das Perl-Entwicklungsteam hier ein grundlegendes Problem umsichtig und nachhaltig gelöst hat.

Definitionen der OO sind selten präzise und oft widerspricht eine Definition der anderen, wie auch Damian Conway – der maßgeblich zur objektorientierten Programmierung in Perl beigetragen hat – immer wieder betont. Der Ansatz von Alan Kay ist hilfreich, aber die zentrale Herausforderung der meisten Softwareprojekte bleibt: eine fast alles erstickende Komplexität. Objekte verstecken etwas davon hinter der Fassade ihrer öffentlichen Schnittstelle und begrenzen so die für den Programmierer relevante Informationsmenge. Nur zu diesem Zweck besitzen Objekte auch einen Zustand (Attribute).

In Perl 5.0, das die Objektorientierung in die Sprache einführte, gelang es nur halbherzig, dieses Verstecken umzusetzen. Denn Perl-5-Objekte sind, abgesehen von ihrem Referenz-Typ, normale Datenstrukturen. Nur die Selbstbeschränkung der Nutzer verhindert hier den Zugriff auf private Attribute.

Eine zweite Schwäche waren fehlende Schlüsselworte. Erfahrene Programmierer sehen sofort an einem bless oder use base, welche Pakete (Namensräume) Klassen sind und am my $self = shift;, welche sub als Methode fungiert. Das Perl-Entwicklungsteam hätte es aber auch Neulingen einfacher machen können, anstatt deren Einwand, Perl hätte gar keine richtige OO, nur immer wieder mit dem Hinweis auf die überlegene Mächtigkeit der Perl-OO abzubügeln.

Sachlich ist es zwar richtig, dass sich mit dem nachträglich in die Sprache eingefügten (Meta-)System jede Art von OO bauen lässt. Es verlangt jedoch einen deutlich erhöhten Schreibaufwand – auch für alltägliche Arbeiten, die in anderen, sonst wesentlich geschwätzigeren Sprachen, direkter gelöst sind. Zusammen mit dem damaligen Fehlen von Signaturen hinterließ dieser Umstand bei nicht Wenigen einen archaischen, ungelenken Eindruck.

Perl 6 sollte all das beheben, denn bereits 35 der 361 initialen Requests for Comments (RFC) adressierten Teilprobleme. Während eines überlangen Entscheidungsprozesses erwuchs aus den Antworten der RFC ein mächtiges, sehr ausgereiftes OO-System. Doch Perl 6 entwickelte sich zu einer eigenen Sprache und wurde später in Raku umbenannt. Als feststand, dass Perl 5 nicht abgelöst wird, erschuf Stevan Little mit Moose ein Objektsystem, das die Raku-OO so gut es ging nach Perl 5 portierte und durch Fähigkeiten des Common Lisp Object System (CLOS) und anderer Sprachen erweiterte. Es folgte eine dem Goldrausch vergleichbare Phase, in der unzählige Moose-Plug-ins und etliche Moose-Alternativen entstanden. Während dieser Zeit kristallisierte sich heraus, welche Funktionalitäten wichtig sind und welche Schreibweisen sich eignen.

Als Curtis "Ovid" Poe antrat, um die neue OO unter dem Projektnamen Cor (später Corinna genannt) zu entwerfen, konnte er auf die gewonnenen Erfahrungen zurückgreifen und sich auch mit entscheidenden Köpfen hinter dieser Entwicklung beraten – darunter etwa Damian Conway und Stevan Little. Letztlich wählte er einen vorsichtigen und minimalistischen Weg, mit einer Syntax, die bewusst Verwechslungen mit Raku und Moose meidet, und mit einer Semantik, die sich nahtlos in Perl 5 einfügt.

Corinna führt auch keine neuen syntaktischen Wendungen ein und verzichtet konsequent auf Ausnahmeregelungen, wie etwa bestimmten Symbolen in Bezeichnern (große Anfangsbuchstaben oder '__') besondere Bedeutung zuzuschreiben. Die Semantik ist rein deklarativ und die Syntax hält sich an einfache, einheitliche Regeln – beides eher von Raku als von Perl 5 beeinflusst.

Die schwerwiegendste Kritik an Corinna lässt sich als Frage an die Perl-Porter (p5p) formulieren: Warum war diese Funktionalität nicht bereits zehn Jahre früher möglich, als es absehbar war, dass Perl 6 Perl 5 nicht ersetzen wird? Die größte technische Schwäche von Corinna hingegen ist der nicht vermeidbare Umstand, dass per bless entstandene Klassen und Corinna-Klassen nicht voneinander erben können und auch getrennte Basisklassen besitzen. Für eine Zusammenarbeit bleibt Entwicklerinnen und Entwicklern nur die Möglichkeit, OO per Komposition zu verwenden und Objekte der anderen Art als Attribute zu benutzen.

Corinna umfasst vier neue Schlüsselworte: class, role, method und field, wobei role wie auch etwa 70 Prozent der spezifizierten Funktionalität noch nicht mit Version 5.38 ausgeliefert werden. Die folgende Darstellung behandelt trotzdem alle wesentlichen Features und benennt, was bereits verwendbar ist. Nutzer älterer Perlversionen können Corinna (auf dem Stand von 5.38) als Modul Compat::Class:Feature einbinden.

Attribute sind Variablen, deren Geltungsbereich mit field, anstatt my, our oder state deklariert wird, was ihre Nutzung auf alle Methoden der aktuellen Klasse oder Rolle beschränkt. Logischerweise ist das nur innerhalb einer Klasse (class) oder Rolle (role) möglich. Und wie zu erwarten ist ihr Inhalt bei jedem Objekt ein anderer. Während ihrer Initialisierung lässt sich ihnen ein Wert mithilfe eines beliebigen Ausdrucks oder Anweisung zuweisen. Diese wird ausgeführt, wenn das Objekt erzeugt wird (wenn der Nutzer Klassen::Name->new(...) aufruft).

Wenn eine Anweisung nicht reicht, lässt sich auch ein ganzer Block einschieben. Dieser wird so ausgewertet, als ob ein do vor dem Block stünde.

Zwischen Variablenname und Zuweisung können beliebig viele Attribute stehen. Sie sind syntaktisch von Raku abgeschaut, aber bereits mit Perl 5.10 (2008) eingeführt. Bisher fanden sie jedoch kaum Beachtung, da sie lediglich Subroutinen zu selten verwendetem Verhalten verhelfen. In Corinna gilt für alle Schlüsselworte die einheitliche Reihenfolge: Schlüsselwort, Name, Attribute, Code. Die letzten beiden Elemente sind optional.

Das einzige bisher implementierte Feld-Attribut ist :param. Es gibt an, dass der Konstruktor ein gleichnamiges Argument akzeptiert, dessen Inhalt automatisch der Feldvariable zugewiesen wird. Weichen die Namen von Konstruktorargument und Feldvariable voneinander ab, bekommt das Attribut :param den anderen Namen als Argument.

field $timestamp :param(created) ||= time;

Class::Name->new( created => 0 );

Wäre der Feldvariable $timestamp bei Initialisierung kein Wert zugewiesen, so würde der Konstruktor mit Fehler abbrechen, wenn er keinen Wert unter dem Namen created erhält (und auch kein <code> ||= time</code> im Beispiel stünde). Im Beispiel bekam er eine Null, die allerdings nicht nach $timestamp übertragen wird, da der selbstzuweisende Operator ||= greift und dem logisch wahren Ergebnis von time den Vorrang gibt.

Geplant sind die Attribute :reader und :writer, die das Erzeugen von Methoden für lesenden und schreibenden Zugriff (Accessors) veranlassen. Auch hier sind wieder abweichende Namen als Attribut-Argument zulässig und sei es nur, um eine kombinierte Lese- und Schreibmethode zu erhalten. Denn :writer ohne Argument hängt ein set_ vor den Variablennamen. Zu beachten ist auch, dass Feldvariablen in abgeleiteten Klassen nicht sichtbar sind. Bei exotischeren Namenskonflikten kann eine Feldvariable mit :name(...) bei Initialisierung einen anderen Namen tragen als bei der späteren Benutzung in den Methoden. Auch :reader, :writer und :param beziehen sich dann auf den als ... angedeuteten Namen.

:common markiert Klassenvariablen, die einen Datenaustausch zwischen Instanzen einer Klasse erlauben und :handles(...) ermöglicht Delegation.

field $datetime :handles(time:hms, now) = 'DateTime';

In diesem Beispiel speichert das Feld $datetime ein DateTime-Objekt.

Aufrufe wie $objekt->time werden dann automatisch an das Attribut von $objekt als $datetime->hms weitergeleitet und $objekt->now wird zu $datetime->now.

Corinna-Methoden beginnen mit method, gefolgt von einem Namen, der dem Namen einer Subroutine gleichen darf, denn für beides gibt es getrennte Namensräume. Da use feature 'class'; immer use feature 'signatures'; beinhaltet, besitzen Corinna-Methoden nach den optionalen Attributen immer Signaturen – mindestens leere, runde Klammern. Diese kennen nur positionale Argumente, die dann optional sind, wenn ihnen ein Default-Wert zugewiesen ist. Wer sich daran erinnert, dass Konstruktoren sehr wohl benannte Argumente verstanden, dem sei gesagt, dass der Konstruktor im OO-System Corinna automatisch generiert wird und keine vom Nutzer geschriebene Methode ist. Um dennoch mit den Konstruktor-Argumenten Berechnungen anzustellen, bevor das Objekt erzeugt wird, können Entwicklerinnen und Entwickler den Phaser ADJUST{...} verwenden. Phaser sind Codeblöcke, die zu bestimmten Zeiten oder Bedingungen ausgeführt werden (wie etwa BEGIN{...} oder LAST{...}).

Innerhalb von ADJUST{...} lassen sich alle Feldvariablen auslesen und verändern. Später soll noch DESTRUCT{...} hinzukommen, mit dem Programmierer noch aufräumen, bevor das Objekt abgebaut wird.

Im Gegensatz zu Perl-5.0-Objekten, darf man $self nicht in der Signatur angeben. Diese Spezialvariable beinhaltet die aktuelle Instanz, und mit ihr lassen sich alle privaten und öffentlichen Methoden aufrufen. Sie ist automatisch in jeder Methode bekannt, solange es keine Klassenmethode ist. In letzteren kennt man $class, die auch nur Klassenmethoden rufen kann.

Methoden sind von Haus aus öffentlich. Mit dem Attribut :private dagegen nur für die aktuelle Klasse oder Rolle sichtbar, genau wie Feldvariablen. Und wie bei letzteren entstehen Klassenmethoden mit dem Zusatz :common, innerhalb derer keine Instanzmethoden mehr zugänglich sind und auch keine gewöhnlichen Feldvariablen. Methoden mit :common, aber ohne Signatur und Block sind abstrakt, was im Folgenden näher erläutert wird. Das Attribut :overrides unterdrückt lediglich die Warnung, die anzeigt, dass hier eine Methode überschrieben wurde. Da Klassen- und Instanzmethoden eigene Namensräume besitzen, lässt sich nur innerhalb einer Methodenart überschreiben.

Es sind auch noch sogenannte Methoden-Modifikatoren wie :before, :after und :around angedacht. Mit ihnen entstehen Codeblöcke, die sich vor, nach oder vor und nach der gleichnamigen Methode ausführen lassen. Diese Funktionalität ist aber eher von experimenteller Natur und angesichts der Tatsache, dass bisher kein Methoden-Attribut implementiert ist, erscheint ihre Zukunft eher ungewiss.

Rollen beginnen mit role und sind Namensräume für Feldvariablen und Methoden. Von einer Rolle (manchmal Trait genannt) lässt sich keine Instanz bilden, aber Klassen und andere Rollen können sie konsumieren. Durch diesen Vorgang wird die öffentliche Schnittstelle der Rolle zum Teil der privaten API des Konsumenten. Der besitzt jedoch keinen Zugriff auf die private API der Rolle, deswegen können auch keine Namenskonflikte zwischen Feldvariablen einer Rolle und der konsumierenden Klasse entstehen. Namenskonflikte zwischen Methoden der Rolle und ihres Konsumenten sind Fehler, welche bereits zur Kompilierungszeit zum Abbruch führen.

Der ADJUST{...}-Phaser einer Rolle wird nach dem ADJUST des Konsumenten ausgeführt und DESTRUCT{...} vor dem Konsumenten.

Rollen besitzen zwei wesentliche Anwendungsfälle, die sich bei Corinna kombinieren lassen. Zum einen, um Hilfsmethoden bereitzustellen, die nirgendwo elegant in die Vererbungshierarchie passen. Zum anderen müssen Methoden einer Rolle, die keine Signatur oder Code besitzen, also abstrakt sind, vom Konsumenten überschrieben werden. Das entspricht der Funktionalität einer Schnittstelle (interface).

Ein Beispiel:

role Counter {

field $counter :reader = 0;

method increment () { $counter++ }
method display_counter;
}

class Rocket 1.0 :does(Counter) {

method display_counter () {
# eigentliche implementation
}
}

class eröffnet eine Klasse und lässt sich wie package als Anweisung schreiben oder einem Block aus geschweiften Klammern voranstellen. Wie bei package, darf nach Schlüsselwort und Namen die optionale Versionsnummer stehen.

Das wichtigste und einzige bisher implementierte Attribut ist :isa(..), das bestimmt, von welcher Klasse geerbt wird, da die UNIVERSAL-Methode ->isa seit Perl 5.0 solche Abhängigkeiten abfragt. Darüber hinaus prüft :isa(..) auch die Versionsnummer der Elternklasse und wirft bei negativem Ergebnis einen Fehler aus:

class Example::Subclass 1.0 :isa(Example::Base 2.345) { ... }

Bisher ist nur Einfachvererbung vorhergesehen, was sich bei starker Nachfrage seitens der Perl-Programmierer noch ändern kann. Solange feature experimentell sind, dürfen sie sich ändern. Immerhin ist geplant, dass mit :does(..) beliebig viele Rollen konsumiert werden dürfen. Ist eine Klasse als :abstract markiert, darf sie abstrakte Methoden ohne Signatur und Block enthalten, lässt sich aber dafür nicht instanziieren. Abstrakte Methoden müssen wie bei Rollen überschrieben werden oder die Instanziierung scheitert an einem Fehler.

Corinna-Objekte werden wie auch Objekte alter Schule vom builtin blessed, als auch von der gleichnamigen Scalar::Util-Routine erkannt. Der aus beiden Quellen beziehbare Befehl reftype kann sie hingegen unterscheiden. Objekte der alten Art liefern hier HASH, ARRAY oder was auch immer mit bless zum Objekt erhoben wurde. Bei Corinna-Objekten antwortet reftype mit OBJECT.

Corinna bietet alles von einer modernen OO Erwartbare – und noch etwas mehr. Das Perl-Team liefert Corinna behutsam über mehrere Releases verteilt aus. Mindestens so lange – wahrscheinlich noch mehrere Jahre – behält es den Status experimentell. Die derzeit in den Perl-Kern eingepflegte Funktionalität ist das Meta-Objekt-Protokoll (MOP), das es Nutzern erlaubt, eigene Erweiterungen für Corinna zu schreiben, die sogar in die Privatsphäre der Feldvariablen eindringen könnten.

Zusammen mit den in den letzten zehn Jahren hinzugekommenen Signaturen, der Ausnahmebehandlung per try, catch, finally, der Postfix-Dereferenzierung, verketteter Vergleichsausdrücke und vielem mehr hat sich die Sprache merklich weiterentwickelt. Gleichzeitig hat das Perl-Team Altlasten entsorgt, wie etwa die Spezialvariablen: $, $* und $#, Methodenaufrufe per "'" oder die Behelfsfunktion, um in Perl 4 verschachtelte Datenstrukturen zu simulieren. Um diesen Fortschritt ausreichend deutlich zu machen, wollte das Perl-Entwicklungsteam die anders verbrauchte Versionsnummer 6 überspringen und stattdessen Perl 7 herausgeben. Der Versuch scheiterte aber vorerst an Meinungsverschiedenheiten im Team.

Andererseits arbeitet das erst wenige Jahre existierende Perl Steering Committee (PSC) routiniert an den nächsten Weichenstellungen, und Curtis Poe befasst sich mit Projekt Oshun, das Signaturen optional mit echter Typisierung ausstattet, während Perl 5.38 bereits einen bool-Typen brachte. Berücksichtigt man, dass Corinna für Perl-Verhältnisse ungewöhnlich viel zur Kompilierungszeit überprüft, lässt sich ein Trend zu stärkerer Striktheit ablesen. Entwicklerinnen und Entwickler können der Striktheit – mit etwas Aufwand – aber auch in Zukunft aus dem Weg gehen, ebenso wie auch niemand die Sigillen ($) für Variablen abschaffen möchte. Beides gehört zum Grundverständnis von dem, was Perl ausmacht. Aber in seiner Standard-Konfiguration erhält Perl zunehmend ein Gesicht, das es für neue Projekte attraktiver macht.

Herbert Breunung
schreibt seit 2007 über Perl und Raku, hält Vorträge, ist Autor des Raku-Moduls Math::Matrix und forscht über Compiler zur elektronischen Klangsynthese.

(who)