Bitte weitere Fragen und Vorschläge für Antworten an wahlen@tu-harburg.de schicken!
Wenn man nicht explizit mittels "metaclass ..." eine Metaklasse
angibt, wird der Namen der Klasse mit einem angehaengten "Class"
benutzt. Die Metaklasse zu Reader heisst demnach ReaderClass
etc.
Fuer einfache Faelle gibt es SimpleConcreteClass, das ein generisches
new ohne Argumente zur Verfuegung stellt.
Sollen bei der Erzeugung einige Slots schon mit sinnvollen Werten
initialisiert werden, definiert man in der Metaklasse eine new-Methode
mit spezifischen Argumenten. Diese new-Methode legt mittels "_new"
(geerbt von ConcreteClass) eine Instanz an, weist einige Slots zu und
gibt die erzeugte Instanz an den Klienten weiter.
Nicht etwa: Set(Int).new
Das liegt daran, dass der Name der Klasse eine Bezeichner auf der Wertebene
ist. "Set" ist implizit eine Methode der Klasse Object, die einen Typ-
Parameter nimmt. Und wie immer bei Methodenaufrufen muss vor Typ-Parametern
ein Doppelpunkt stehen.
Wird "Set" auf der Typebene benutzt, z.B.: "intset :Set(Int)",
dann
fungiert es als Typoperator. Dessen Args werden nicht gedoppelpunktet.
Wie erlaube ich nur lese-Zugriff auf Slots?
Antwort: Den Slot private unter einem anderen Namen (z.B. mit
underscore '_') definieren und eine Zugriffsmethode schreiben:
public methods
value :Int
{ _value }
private
_value :Int
Oder alternativ (weniger fein): Den Slot privat deklarieren, und die
Zugriffsmethode mit gleichem Namen public. Die Zugriffsmethode
kann dann
mittels super.<slotname> auf den Slot zugreifen:
public methods
value :Int
{ super.value }
private
value :Int
Vorsicht Falle: Bei Methoden wird der Ergebnistyp im Gegensatz zu
anonymen Funkionen nicht inferiert, sondern als Void angenommen.
Insbesondere in new-Methoden von Klassen vergisst man leicht, den
Ergebnistyp hinzuschreiben, was zu Fehlern fuehrt, wenn man dann
tatsaechlich so ein Objekt erzeugt und benutzen will.
Steckt ein Trick hinter der komischen if-Syntax?
Ja. Die Syntax
<test> ? <ifTrue>
bzw.
<test> ? <ifTrue>
: <ifFalse>
wird intern uebersetzt ("normalisiert") in einen Methodenaufruf:
<test>."?"(<ifTrue>)
bzw.
<test>."?:"(<ifTrue>,
<ifFalse>)
<ifTrue> und <ifFalse> sind dabei Funktionen ohne Argumente (aka
Bloecke, Fun0s). Je nachdem, ob <test> zur Klasse True oder False
gehoert (d.h. true oder false ist), wird einer der beiden aufgerufen.
<ifTrue> und <ifFalse> muessen Bloecke und keine einfachen Ausdruecke
sein, da sonst immer beide Zweige ausgewertet wuerden (man simuliert
lazy evaluation).
Aehnlich funktioniert die Mechanik hinter "||" und "&&", genaueres
verraet der Quellcode (Klassen Bool, True, False).
Wie stelle ich "Variante Records" dar?
Auf jeden Fall werden alle Varianten als Subklassen eines gemeinsamen
Vorfahren implementiert. Nehmen wir folgenden Modula-Record:
TYPE A =
RECORD
i: INTEGER;
CASE whichOne:
[1..3] OF
1: c :CHAR;
| 2: b
:BOOLEAN;
| 3: s
:ARRAY OF CHAR;
END;
In Klassen saehe das so aus:
class A public i :Int;
class A1 super A public c :Char;
class A2 super A public b :Boolean;
class A3 super A public s :String;
Die Frage ist nun, wie man auf die verschiedenen Varianten zugreift,
d.h. wie man herausfindet, mir welcher Variante man es konkret zu tun
hat.
Es gibt vier verschiedene Ansaetze: den rein objektorientierten,
Implementation mittels case-Methode, mittels Visitor, und mittels
dynamischem Klassen-Test.
- rein objektorientierter Ansatz
Bei konsequent objektorientierter Implementation wird das gesamte
Verhalten ins Objekt gesteckt, d.h. fuer jede moegliche Verwendung
des
Objekts gibt es eine eigene Methode. Diese Methode wird in der
Vorfahrenklasse "deferred" oder mit einem default-Verhalten
deklariert, und die Subklassen koennen sie dann redefinieren.
Die
Typ-Unterscheidung wird also durch Message-Dispatch ausgefuehrt.
Der Nachteil dieser Methode ist, dass das Verhalten sehr stark
verteilt wird: in jeder Klasse steht ein bisschen Code. Es gibt
Faelle, in denen der Zusammenhalt des Algorithmus wichtiger ist als
der Zusammenhalt des Objekts.
In solchen Faellen kann man eine explizite case-Methode benutzen.
- case
Erstmal eine ganz abstrakte Erklaerung: Die case-Methode bekommt einen
Block fuer jede moegliche Variante uebergeben. Es wird dann der
zum
Typ des Empfaengers passende Block aufgerufen. Um auf spezifische
Felder der Subklassen zugreifen zu koennen, muss man den genauen Typ
kennen; zu diesem Zweck uebergibt der Empfaenger sich selbst mit
bekanntem Typ an den jeweiligen Block.
An unserem Beispiel:
class A
public
i :Int
methods
case(T <: Void,
a1 :Fun1(A1,T),
a2 :Fun1(A2,T),
a3 :Fun1(A3,T)) :T
deferred
;
class A1
super A
public
c :Char
methods
case(T <: Void,
a1 :Fun1(A1,T),
a2 :Fun1(A2,T),
a3 :Fun1(A3,T)) :T
{
a1[self]
}
;
Und so weiter fuer die anderen Klassen. Benutzt wird das z.B. so:
m(a :A)
{
a.case(
fun(a1 :A1) {
"Character " + a1.c.printString
},
fun(a2 :A2) {
"Boolean " + (a2.b ? {"Ja"} : {"Nein"})
},
fun(a3 :A3) {
"String " + a3.s
}
).printOn(tycoon.stdout)
}
Siehe auch Klassen Bool, TWL/Compile/Parse/LALRAction,
TWL/Compile/Parse/Symbol, TWL/Compile/TWL/Ide.
Wie man schon sieht, legt man sich mit diesem Muster auf eine feste
Anzahl
Varianten fest, was beim OO-Ansatz nicht der Fall ist. Will man
nachtraeglich
eine Variante hinzufuegen oder aendern, muessen die abstrakte Oberklasse
und
alle Varianten angefasst werden.
- visitor
Wird die Anzahl der zu unterscheidenden Faelle zu gross, greift man
zum Visitor. Der Visitor ist ein Objekt, das alle oben einzeln
uebergebenen Bloecke als Methoden enthaelt und zusammenfasst.
Fuer
jeden moeglichen "Gastgeber" gibt es also eine eigene Methode, die
konventionell durch "visit<Klassenname>" gebildet wird. Beispiel:
class A
public
i :Int
methods
visit(T <: Void, visitor: AVisitor(T)) :T
deferred
;
class A1
super A
public
c :Char
methods
visit(T <: Void, visitor: AVisitor(T)) :T
{
visitor.visitA1(self)
}
;
class AVisitor(T <: Void)
public methods
visitA1(a1 :A1) :T
deferred
visitA2(a2 :A2) :T
deferred
visitA3(a3 :A3) :T
deferred
;
Um ein konkretes Verhalten zu implementieren, muss man eine Subklasse
von
AVisitor erzeugen, z.B.:
class DummyAVisitor
super AVisitor(String)
metaclass SimpleConcreteClass(DummyAVisitor)
public methods
visitA1(a1 :A1) :String
{ "Character " + a1.c.printString
}
visitA2(a2 :A2) :String
{ "Boolean " + (a2.b ? {"Ja"} : {"Nein"})
}
visitA3(a3 :A3) :String
{ "String " + a3.s
}
;
...und benutzt wird das so:
m(a :A)
{
a.visit(DummyAVisitor.new).printOn(tycoon.stdout)
}
Dieses Muster ist u.a. dann vorteilhaft, wenn der Visitor
weitergereicht werden soll (z.B. in Baeumen), da ein einzelnes Objekt
natuerlich griffiger ist als ein halbes Dutzend Codebloecke.
Ausserdem praktisch ist, dass der End-Benutzer (Methode m im obigen
Beispiel) nicht wissen muss, welche Varianten es gibt.
- dynamischer Klassen-Test
In Tycoon-2 ist es zur Zeit schon (in wird in Zukunft noch viel mehr)
moeglich, reflexiv zur Laufzeit auf Klassen und Metaklassen
zuzugreifen. Mittels x."class" erhaelt man die Klasse, zu der
x
gehoert, als Laufzeitobjekt. Dieses Objekt kann man nach seinem
Namen
fragen (x."class".name) oder es mit anderen Objekten vergleichen
(x."class" == Set). Folgender Hack waere also denkbar:
m(a :A)
{
let clazz = a."class",
clazz == A1 ? {
let a1 = _typeCast(a, :A1),
"Character " + a1.c.printString
} : {
clazz == A2 ? {
let a2 = _typeCast(a, :A2),
"Boolean " + (a2.b ? {"Ja"} : {"Nein"})
} : {
clazz == A3 ? {
let a3 = _typeCast(a, :A3),
"String " + a3.s
} : {
SomeError.new.raise
}
}
}.printOn(tycoon.stdout)
}
Dies ist KEIN EMPFOHLENER PROGRAMMIERSTIL. Ein wichtiger
Nachteil
ist, dass fuer Subklassen von z.B. A1 nicht mehr "class == A1" gilt;
der Code ist also noch staerker als beim case/visitor-Ansatz vom
aktuellen Klassen-Schema abhaengig.
class Array(E <: Object)
(*...*)
public methods
"[]"(i :Int) :E
;
Wenn ich jetzt einen bestimmten Arraytyp meine, muss ich den aktuellen
Wert fuer E angeben. Das sieht so aus:
x :Array(Char)
Dabei fungiert "Array" als Tyoperator, dem ich "Char" uebergebe.
Daraufhin werden alle Es in Array durch Char ersetzt, und ich erhalte
einen Typ, der u.a. die Methode
"[]"(i :Int) :Char
enthaelt. Der Typ Array heisst "offen", weil der Parameter E offen
bleibt; Array(Int) heisst dementsprechend "geschlossen". Nur
fuer
geschlossene Typen kann man Werte angeben.
Die Fehlermeldung bedeutet, dass eine Methode inkompatibel redefiniert
wird. Inkompatibel heisst wohl "die Signatur der Methode in der
Subklasse ist keine Subsignatur der Signatur in der Superklasse".
Das kann zum Beispiel daran liegen, dass man einen Typoperator statt
eines geschlossenen Typs als Argument an eine Vorfahrenklasse
uebergeben hat:
class A(T <: Void) methods m(x :T) ...;
class B(E <: Void) ... ;
class C super A(B) ... ### Krach
let und Let sind am toplevel leider nicht moeglich.Fuer Typen muss man
eine eigene Klasse erzeugen,
fuer Werte benutzt man define:
define x :T;
Das definiert eine Pool-Variable mit Namen x und Typ T, die mit nil
initialisiert wird. Pool-Variablen werden konzeptionell als Slots
der
Klasse Object eingehaengt. Dadurch kann man in jeder Klasse auf
x wie
auf einen eigenen Slot zugreifen: Effektiv ist x eine globale
Variable.
Ausdruecke am Toplevel werden im Kontext der Klasse Nil ausgewertet.
Da auch Nil Subklasse von Object ist, stehen einem die Pool-Variablen
also auch am Toplevel zur Verfuegung.
Ueber den Pool-Mechanismus wird auch der Zugriff auf Klassen
als
Objekte geregelt: Wenn ich am Toplevel "List.new" schreibe, wird das
uebersetzt ist "self.List.new". Da es in der Klasse nil keinen
Slot
"List" gibt, wird im Pool nachgeschaut, und siehe da, wir finden die
gesuchte Klasse. Mal ausprobieren:
"tycoon.tl.pool.poolMethods.keys.do(fun(s :Symbol){tycoon.stdout << s, tycoon.stdout.nl});"
Siehe auch Abschnitt "Set.new".
Pool-Variablen haben zwei Nachteile: 1. Man wird sie nicht wieder
los (bzw. nur durch den reflektiven Aufruf tycoon.tl.pool.undefinePoolVariable("x")), und
2. sie koennen zu Konflikten mit existierenden Methoden oder Slots
fuehren. Als Ausweg empfiehlt es sich, fuer adhoc-Variablen
moeglichst kryptische Namen zu waehlen - sowas kommt in unseren
super-designten Libraries naemlich nicht vor ;-)
Um eine veraltete Klasse X loszuwerden, kann man sie mittels
"class X metaclass AbstractClass;" leer ueberschreiben.
Per Konvention heisst die Pool-Variable genauso wie ihre Klasse,
nur
wird sie klein geschrieben (Klasse Tycoon, Variable tycoon).
| f.matthes, nov-1997 |