Re: Groovy (war Re: [PROST]Re: CDC Plugin fuer Eclipse)



Jochen Theodorou schrieb:

ok, kannst du nochmal kurz den Unterschied zwischen tail-calls, Endrekursionen, continuations mit besonderer Rücksicht des Stack erklären? Warum müsste man continuations erlauben Stackvariablen in den Heap zu verschieben?

Tail-call und Endrekursion habe ich austauschbar benutzt und meine damit das selbe: Für den Aufruf der Funktion kann der existierende "activation record" wiederverwendet werden, weil nach diesem Aufruf kein Zugriff auf diesen record mehr nötig ist. Ein activation record speichert die Parameter und lokalen Variablen der Funktion und eine Information, wie man zu der aufrufenden Funktion zurückkommt.


Da diese activation records klassischerweise auf einem Stack verwaltet werden, kann man hier Stackspace sparen - Endrekursion ist damit genauso effizient wie eine klassische Iteration mit einem Sprung.

Eine Closure (und damit auch Continuations) hält ihre äußere Umgebung fest und durchbricht damit das LIFO-Prinzip von activation records. Entweder speichert man sie gar nicht auf dem Stack (was es einfach macht, aber meist nicht so effizient, da ein Zugriff auf eine Speicherstelle im Heap teurer ist als auf eine Speicherzelle im Stack bzw. weil derartige Variablen auch in Prozessorregister gehalten werden können und man gar keinen Stackspeicher braucht) oder aber man trennt zwischen freien und gebundenen Variablen auf und sorgt dafür, dass die freien Variablen (über die die Closure abgeschlossen wird) (genau wie die Closure selbst) im Heap landen. Da aber statisch nicht entscheidbar ist, ob eine Closure überhaupt je gebildet wird, ist das u.U. vorauseilender Gehorsam und man möchte diese Aufteilung erst beim Erzeugen der Closure vornehmen - muss dann also stack-allozierte Variablen schnell mal in den Heap verschieben.

Klassiches Beispiel für Closures sind Objekte: Diese lassen sich leicht simulieren:

 def person(n: String) =
  fn(s: Atom, a...) =
   case s
    'get-name => n
    'set-name => n := a(0)
   else raise(MessageNotUnderstood)
   end

Dies definiert eine Funktion "person", die eine Funktion zurückliefert, die einen Selektor erwartet und optional weitere Argumente. Eine solche Funktion würde mal Konstruktor nennen. Die anonyme Funktion selbst würde man das Objekt nennen, ein Exemplar der Klasse person, denn alle diese Exemplare verhalten sich gleich - schließlich wird ihr Verhalten durch die selbe Funktion definiert. Das "case" implemeniert die Methodensuche (in diesem trivialen Beispiel gibt es keine Vererbung) und man kann jetzt

 val p = person("Stefan")
 p('get-name)
 p('set-name, "Matthias")
 p('get-name)

benutzen, um ein neues Objekt anzulegen, den Namen abzufragen und neu zu setzen. Der Quote soll die Auswertung des Namens verhindern.

Sieht natürlich nicht so schick aus: Man könnte noch zwei Hilfsfunktionen (a.k.a. generische Funktionen) definieren:

 def get-name(o: Object) = o('get-name)
 def set-name(o: Object, n: String) = o('set-name, n)

Oder man definiert, dass diese Punkt-Notation p.get-name() syntaktischer Zucker für p('get-name) ist.

Die anonyme Funktion ist eine Closure, in der "n" eine freie Variable ist. "n" kann nicht auf dem Stack liegen, denn diese Variable lebt länger als die Funktion "person".

Wenn man verhindern möchte, dass eine Person namens "Foo" existieren kann, dann könnte das so aussiehen:

 def person(n: String) =
  if n != "Foo" then ...
  else raise(NotInstanceable)

und "n" ist nur in einem Fall eine non-lifo-Variable.

Aber sofern du nciht nur und ausschliesslich mit Rückgabeweerten arbeitest hast du immer mögliche Seiteneffekte. Und wenn dein Rückgabewert ein "Behältnis" sein darf, das mutiertwerden kann und erneut zurückgegeben wird hast du doch auch Seiteneffekte, oder?

Korrekt. Dies dadruch ein bisschen expliziter zu machen indem es nicht der Default ist finde ich aber durchaus überlegenswert.


Weil ich mich extrem an die VM erinnert fühle... du kennst nicht zufällig einen Debugger, der mir den Stack und Bytecode anzeigen kann und die Bytecodebefehle Schroittweise ausführt? Sowas wäre echt schön, denn ansonsten ist es relativ grausam grössere Dinge in Bytecode zu schreiben.

Nicht für Java - ich meine VS.NET kann sowas für deren Bytecode.

Nicht das ist die "Macht" von Joy oder Forth und der stack-basierten Sichtweise, sondern eben das Zusammenstellen von neuen Worten aus existierenden Worten.

Das weiss ich wohl, nur das Problem mit dem Wissen über den Stack bleibt doch, oder? Es sei denn ich arbeiten mit regelrechten Explosionswolken an neuen Worten.

Ja, man muss wissen, wie die Worte mit den Daten auf dem Stack herumwirbeln - ist aber alles eine Frage der Gewöhnung denke ich. In der Homecomputer-Ära hatte ich mal ein bisschen mit Forth gespielt, da das damals eine Hochsprache war, die auch mit wenigen K Hauptspeicher auskam und trotzdem eine komplette interaktive Entwicklungsumgebung hatte und zudem so effizient wie Assembler war - im Gegensatz zum eingebauten Basic. Inzwischen habe ich das aber alles wieder vergessen :)


[nullable]

dort nicht, aber wenn du kompatibel zu den normalen Typen bleiben willst/musst, dann muss man entweder irgendwo Umwandlungen.

Ich würde explizite Umwandlungen fordern.

Bei einem statischen Typsystem würde ich gerne sehen, dass Methodenaufrufe nur für Variablen funktionieren, die garantiert nicht "null" enthalten und alles andere einen expliziten Test benötigt.

Funktionale Sprachen haben ja meist einen Option-Typ

 type option 'x = Some 'x | None

der dann sowas erlaubt:

 x: option int = get-a-value-or-not()
 match x
  Some x' => mach-was-mit(x')
  None => kein-wert-da()

was man entweder genauso übernehmen kann oder man verpackt das Prinzip hinter anderer Syntax:

 var x: int|none = get-a-value-or-not()
 if x != none then x.gefahrloser-aufruf()
 else kein-wert-da()

Das "int|none" ist ein Vereinigungstyp. Könnte man natürlich auch als "int#" oder "?int" oder wie auch immer schreiben. Vereinigungstypen sind aber allgemein praktisch, etwa für sowas wie "string|int".

Dann kann man allgemein ein typecase in einer Sprache einführen:

 typecase x
  int => a(x)
  string => b(x)

was wiederum durch lokal-definierte Methoden in einer OO-Sprache implementiert werden könnte:

 method t(x: int) = a(x)
 method t(x: string) = b(x)
 t(x)

oder in anderer Syntax

 method int.t() = self.a()
 method string.t() = self.b()
 x.t()

Dieser Ansatz bietet sich IMHO auch für dynamisch getypte Sprachen an: Erforderlich ist aber dafür, dass ich auch Methoden für "null" bzw. "none" wie ich es genannt hatte definieren kann. Das wäre aber nur konsequent und würde eine unnötige Ausnahme bei Java beseitigen.

Die Sprache Nice bekommt es auch hin, die NullPointerExceptions aus der Sprache zu verbannen, ohne die VM ändern zu müssen.

Wie kompatibel ist nice mit der JavaApi?

Sehr kompatibel. Aus Nice heraus kannst du alle Java-Methoden aufrufen. Aus Java heraus sehen Nice-Klassen wie Java-Klassen aus und man kann genau einen automatisch generierten Konstruktor aufrufen und normale Methoden aufrufen. Ich glaube, für Multimethoden muss man mehr tun.


Also es geht, wenn man will. Es macht leider Konstruktoren so wie wir sie bei Java kennen unsicher bzw. man muss ein Hölle-kompliziertes Typsystem definieren. Es gibt da für Spec# ein Paper, was erklärt, wie man statisch prüfen kann, ob Variablen auch in einem Konstruktor initialisiert werden bevor man das erste Mal auf sie zugreift.

klingt nicht sehr passend für eine eher dynamisch typisierte Sprache, oder?

Nein, ich war bei statischen Typsystemen.

foreach hätte es für mich nciht sein müssen, aber ein "in" wäre doch schön gewesen

Das sie ein "in" nicht als neues Schlüsselwort haben wollten, kann ich mir vorstellen. Das ist ein typischer InputStream :) Foreach habe ich aber glaube ich noch niemals als Variablenname benutzt.


interface = 0 { ... }

nööö ;)

Hey, das lässt sich sogar ausbauen, man könnte ja auch noch "= 1", "= 2" usw. benutzen...


Hier ist das Beispiel, wo man dieses Überlagern evtl. haben will:

 (defconst a 12)
 (defun (m1) a)

 (defconst a 32)
 (defun (m2) a)

Was liefern jetzt (m1) und (m2)? Tatsächlich hat man jetzt 2 "globale" Konstanten "a", die eine gilt von ihrer Definition bis zu ihrer Redefinition, die andere danach bis zum Ende der Compilation Unit.

sowas finde ich schlecht, aber wahrscheinlich scheint da meine Pascal-Vergangenheit durch.


Man würde spontan sagen, das man hier lieber einen Fehler erzeugt bekommen wollen würde, aber in einem interaktiven System, das Redefinitionen erlauben muss, wird die Sache etwas schwerer.

korrekt... aber warum dann eine Kosntante definieren?

Warum nicht. Wenn's Konstant ist, soll es auch eine Konstante sein. Zwischen den ersten und den zweiten beiden Zeilen könnte doch ein Jahr liegen, in dem das obige Programm prima und korrekt lief. Ich erwarte, dass ein Programm nicht neu gestartet werden muss, nur weil ich mal etwas ändern will...


Hier ist jetzt die Frage, ob das Redefinieren von "a" die erste Funktionsdefinition beeinflussen sollte. Ich denke nein und schon ist das Problem da.

for (int i=0; i<maxX; i++) { for (int i=0; i<maxY; i++) { offset = i*maxX+i println field[offset] } }

die Variable i ist zwei mal definiert. Und es ist hier ganz klar ein Fehler, denn eine der beiden sollte anders heissen. Wenn ich die Sichtbarkeit der Varibalen nicht überschreiben darf, dann ist klar dass das da oben einen Fehler während des compilierens geben sollte.

Ich stimme dir zu, dass es zwar semantisch sauber definiert ist, wie sich Variablen gleichen Namens überlagern können, aber man will das eigentlich nie haben. Daher sollte das System das Neudefinieren von lokalen Variablen nicht erlauben.


Am besten, man hat gar keine Schleifen-Indizes, dann passiert sowas auch nicht:

 field.each(println)

Will sagen, mit dem Verbot der Redefinition kommt man vielleicht weiter als ohne das Verbot.

Denke ich auch.

eine Liste, aber eine Liste keine Sequenz. Dies sei eine Liste

 [1, 2, 3]

und dies eine Sequenz:

a(); b()


ah, ok. dennoch kann ich mir einfach eine Funktion definieren die mir zu einer Liste das Ergebnis der letzten "Funktion" darin zurück gibt und schon habe ich eine Sequenz.

Ja. In Pico ist z.B. { a(); b() } nur syntaktischer Zucker für begin(a(), b()) und diese Funktion ist so definiert, dass sie eine variable Anzahl von Argumenten schluckt und den letzten Wert zurückgibt. Außerdem ist natürlich definiert, das Argumente von links nach rechts ausgewertet werden. Konsequenterweise kann man da dann auch


 if ({ a(); b() }) ...

schreiben...

> Ausserdem kann ich alle Zwischenergebnisse
einer Sequenz speichern und diese dann als Liste zurückgeben

Nein, das geht nach meiner Definition gerade nicht. Eine Sequenz ist ein Stück Programm und eine Liste ein Stück Datum. Wenn du nicht Programm==Daten in deiner Sprache hast (was Groovy nicht hat AFAIK), dann kommst du weder an die Sequenz heran, noch kannst sie irgendwie umwandeln.


Was macht den Unterschied? Das der NodeBuilder schummelt.

Wieso schummelt er?

Na gut, sage ich: Er simuliert Methodenaufrufe.


Wie gesagt, wir wollen die statischen Dinge noch einbauen. Man handelt sich halt einen Haufen Probleme ein, die vorher nicht da waren.

Und warum wollt' ihr es dann einbauen? Beim Sprachdesign ist doch die hohe Kunst die des Weglassens, nicht die des Zusammenmanschens von Features :)


ich Sprach davon das Vererbung eine die Distanz um 2 erhöt, Autoboxing tut dies um 1 erhöhen. Es ist also eingeordnet.

Verstehe.

Dadurch kann es bei dem selben Quelltext zu unterschiedlichen Ergebnissen kommen und das versteht doch nun niemand.

Das verstehe ich jetzt wiederum nicht... Beispiel?

Sagen wir einmal, man könnte Groovy in zwei Modi ausführen: Einmal achtet er auf Typdeklarationen, einmal nicht. Gegeben sei dieses Program:


 def m(Object a) ...1...
 def m(String a) ...2...
 Object a = "Hallo";
 m(a);

Mit dynamischer Auswertung der Typen wird "2" aufgerufen. Wird jedoch der Typ der Variablen a zur Übersetzungszeit herangezogen, wird "1" aufgerufen. Nun stelle man sich vor, das da steht in mehreren Dateien.

Ich finde NPEs in Java furchtbar, weil sie das statische Typsystem ad-absurdum (ich übertreibe zur Verdeutlichung) führen.

Nicht die NPE ist der Kern der Kritik, sondern null und das null keinen Typ hat.

Ja, das ist besser formuliert.

Ein cast (weil du die CCE angesprochen hast) hebelt ja auch das Typsystem aus. Da hst du recht. Das heisst nicht das es nicht nützlich ist.

Eigentlich kann ein Cast das statische Typsystem sinnvoll ergänzen. Aber bei Java muss man leider sehr häufig casten (auch noch mit generics) und außerdem wird das Konzept auch noch für Typumwandlungen von primitiven Typen misbraucht.


Völlig statisch wäre ein zu starres Konzept, aber wenn man wenigstens die Stellen, wo es krachen kann, auf wenige Stellen (und nicht *jeden* Methodenaufruf!) beschränken könnte, wäre schon viel gewonnen. So wie es die Sprache Alice macht scheint mir ein gangbarer Weg zu sein.

wie macht es Alice den?

Es werden quasi Sollbruchstellen definiert, d.h. es wird ganz klar ausgewiesen, an welchen Stellen im Programm - und es sind wenige - ein Laufzeitfehler auftreten kann.


Der Cast findet auf Modul-Ebene statt: Wenn Module dynamisch geladen werden, wird an dieser Stelle einmal komplett die Schnittstelle geprüft und wenn man über diesen Punkt gekommen ist, ist man wieder in der heilen Welt der statischen Typsicherheit a la ML angekommen.

--
Stefan Matthias Aust // Lassen Sie uns durch, wir sind Arzt!
.



Relevant Pages

  • Re: Mutable reference to a structure field
    ... > the environment that the closure captures stored on the stack in some ... They would more likely be stored in the heap. ...
    (comp.lang.lisp)
  • Re: How does managed code work?
    ... Does it work the same way as the native stack with a frame pointer that is the head of a linked list of stack frames where each time we enter a function we create a new stack frame in which new variables are pushed and each time we exit a function the entire stack frame is popped? ... Can someone point me to a discussion of the managed heap? ... How does it prevent memory leaks that occur in COM when two objects reference each other and keep the others reference count nonzero? ... Because objects don't go out of scope, ...
    (microsoft.public.dotnet.languages.csharp)
  • Re: Is MSDN wrong? or I made a mistake? about static member function
    ... the heap" or some such reference. ... shouldn't we have a uniform notation? ... You can tell a heap object from a stack object by ... the result of trying to build a compiler on a tiny computer by someone who wasn't a very ...
    (microsoft.public.vc.mfc)
  • Re: Stack, Heap, Mfc
    ... >> is put on the heap. ... >> decendant does this not mean that all memory will be on the heap because ... > stack or the heap. ... You first try to limit the recursion to an acceptable ...
    (microsoft.public.vc.mfc)
  • Re: Please Explain where will the struct be stored if it is declared inside the Class
    ... forget about structs for a second. ... can be stored either on the stack, or on the heap. ... First, think about the stack. ... A struct would act exactly the same as any of these decimals and ints. ...
    (microsoft.public.dotnet.languages.csharp)