[linux-l] Ocml vs. Java

Volker Grabsch vog at notjusthosting.com
So Okt 2 00:55:27 CEST 2005


On Mon, Sep 26, 2005 at 04:38:28AM +0200, Oliver Bandel wrote:
> [...]
> > Nur mal des Interesses halber: Wie würdest du folgendes Problem in Ocaml
> > angehen? Du willst eine Funktion "list_file". Sie bekommt als Parameter
> > eine Datei (genauer: irgendeinen Stream, den man zeilenweise durchlaufen
> > kann), und liefert einen Iterator zurück. Diesen Iterator soll man
> > "aufheben" können, d.h. der Iterator soll zwischendurch auch wirklich
> > als einzelnes Objekt (bzw. Funktion) dastehen. Wenn man ihn später
> > durchläuft, iteriert er alle Zeilen der Datei, entfernt das Newline und
> > "trailing spaces", und setzt ein "XXX: " davor. Einzige Bedingung: Keine
> > Listen! Das heißt, die Datei wird wirklich erst dann iteriert, wenn die
> > zurückgegebene Iterator aufgerufen wird.
> 
> Was spricht denn gegen Listen?
> Allein die Dateiegröße?

Ja, aber nicht nur. Ist die Datei ne Pipe, dann will ich, dass das
Programm sofort nach jeder Zeile reagiert, und nicht erst, nachdem die
Pipe ein EOF sendet.

Außerdem: Schon prinzipiell packe ich nichts in eine Liste, von dem
ich 1. im Voraus die Größe nicht abschätzen kann und 2. nur
sequentiellen Zugriff brauche. Das ist für mich eine Grundregel des
gesunden Programmierer-Verstands.

> Bei so etwas würde ich möglicherweise klassisch imperativ heran gehen.
> while-loop ;-)
> Kann man natürlich auch mit ner rekursiven Funktion lösen.
> Bei extrem großen Datenmengen muß die aber tail-recursive sein, sonst
> haut's einem mit einer exception irgendwann weg.
> Aussrdem kann's evtl. arsch-langsam werden, wenn rekursiv, aber nicht tail-rec,
> und der Speicherbedarf wäre auch extrem.
> 
> Was spricht in dem Falle gegen klassisch imperatives vorgehen?

Dass ich nen Iterator brauche. Außerdem wollte ich nen "funktionaleren"
Ansatz von dir.

> Wie soll gelesen werden?
> Byte-per-Byte, oder Blockweise (wegen Unix-read(2) und Blocksize usw.)?
> Ist das geschwindigkeitskritisch?
> 
> Können lexer-Libs genutzt werden?
> (denke da an lex/ocamllex, denn das Überlsen von Sachen wie Space
> und \n kann man da ganz simpel mit erledigen.
> Kann man aber auch nen eigenen lexer implementieren, ist ja auch
> recht einfach.
> Dann würde ich es in OCaml vermutlich mit zwei sich gegenseitig aufrufenden
> rekursiven Funktionen machen. Die eine überliest space und \n und die
> andere liest so lange ein, wie diese nicht auftauchen.
> Die beiden rufen sich dann immer gegensitig auf und packen die Daten in eine Liste,
> sofern gewollt, oder machen halt was anderes damit, wenn keine Liste aufgebaut werden soll.

Och, nun komm. Du packst das Problem an der falschen Ecke an. Diese
Details sind mir völlig unwichtig. Von mir aus gehe davon aus, dass
du nen Iterator gegeben hast, der dir die Eingabedatei zeilenweise
liefert.

> > Wir können nämlich auch sehr
> > große Dateien haben, die auf keinen Fall zwischendurch komplett in den
> > RAM geladen werden dürfen.
> 
> Ist das bei Dir auf Arbeit wichtig?
> Oder wer sind "Wir"?

Angewohnheit. Mit wir meinte ich dich und mich, also die beiden, die an
dem Problem sitzen. Plus eventuell andere Leute, die sich dafür
interessieren. Wenn ich Howtos schreibe, benutze ich gern "wir". Ich,
als Autor, der es vormacht, und der Leser, der es nachmacht. "Wir" beide
tun die jeweiligen Schritte. Nicht nur im übertragenen Sinne, denn
schließlich befolge ich meine eigenen Howtos, gehe sie durch, und schaue,
ob das alles auch so stimmt.

> > Das heißt, wir nehmen eine Datei "test.txt":
> > 
> > Zeile1      
> > Zeile2      
> > Letzte Zeile
> > 
> > Und folgenden Python-Code:
> > 
> > 	f = open("test.txt")
> > 	iter = list_file(f)
> > 
> > 	for x in iter: print x
> > 
> > 	close(f)
> > 
> > 
> > Dies sollte zurückliefern:
> > 
> > XXX: Zeile1
> > XXX: Zeile2
> > XXX: Letzte Zeile
> > 
> > In Python würde list_file so aussehen:
> > 
> > 	def list_file(f):
> > 	    return ("XXX: "+line.rstrip() for line in f)
> > 
> > 
> > andere Variante:
> > 
> > 	def list_file(f):
> > 
> > 	    for line in f:
> > 	        yield "XXX: "+line.rstrip()
> 
> Das ist wirklich recht wenig Code.
> Wie sieht es denn da mit Abfragen von
> Problemfällen aus?

Da gibt's nur den Fall, dass die Datei sich nicht öffnen lässt.

> Wenn ich den Code schreiben würde, wäre er sicherlich
> ausgestatet mit Abfragen, die Exceptions (File nicht vorhanden
> oder nicht zu lesen, etc.) auswerten würden.

Ja, von mir aus. Die eine Exception macht für das geg. Problem
gar nichts aus. Was würde ich schon in den Handler schreiben?
Fehlermeldung ausgeben und Programm abbrechen, richtig? Aber das
macht der Default-Handler für ungefangene Exceptions doch sowieso.

> Wenn Du wirklich ein Beispiel haben willst, sach noch mal bescheid.
> heute ist's schon wieder zu spät geworden. :(

Ja, klar will ich. Wenn du in Ocaml keinen Iterator hast, der dir
einfach eine Datei zeilenweise einliest, dann nimm halt nen
Listen-Iterator. Ist mir wurscht. Es geht nur um eine Funktion,
die nen Iterator nimmt, und einen neuen Iterator zurückliefert.
Der neue Iterator soll wie der alte funktionieren, nur dass eben
jedes Element besagte Transformation erfährt.

> Es gibt da viele Wege, die man nutzen könnte.
> Byteweise lesen, oder Zweilenweise.
> Byty-per-Byte => selber scannen und space und \n weg schmeissen usw.
> dann braucht man hinterher keine regexps oder sowas.
> Zeilenweise einlesen, dann entweder regexüs, oder einfach den String
> selbst secannen (wenn es echt nur space am anfang ist, das weg soll,
> ist ja regexp schon overhead).

Nein! Da liegt nicht das Problem. Es geht mir um das Design-Problem.
Nimm doch bitte die Aufgabenstellung einfach. Das gegebene Teilproblem
ist durchaus sinnvoll.

> > > Btw: Die Sache mit den Verschachtelungen sehe ich noch ein bischen
> > > anders. Man kann sich auch Funktionen als composites erstellen.
> > > In Haskell gibt es dafür sogar einen eigenen Operator (".").
> > > Den gibt's in OCaml leider nicht. Aber dann baut man sich eben eine
> > > entsprechende Kompositions-Funktion zusammen.
> > > Muß man nur einmal machen und hat dann seine Ruhe. ;-)
> > 
> > Das erinnert mich an etwas: In Python gibt es sehr viele Sprache-Features,
> > die gar nicht im Compiler gelöst werden, sondern direkt in Python, dank
> > einiger mächtiger Meta-Features. 
> 
> Wieso unterscheidest Du Python und den Compiler?

Mit Python meinte ich die Sprache. Mit Compiler meinte ich den Teil
des CPython-Interpreters, der den Code compiliert.

> > Zum Beispiel kann man mit "Metaclasses"
> > in Python ne Menge anstellen.
> 
> Hmhhh, nun schaute ich eben mal in der OCaml-Doku zum OO-zeugs nach,
> um zu schauen, was es da in OCaml dazu viell. gibt.
> 
> Naja, dann stolpere ich über Polymorphic Methods und daß man die
> nutzen kan, um Iteratoren zu definieren...
> 
> ...hmhhhh... wat'n Zufall. ;-)
> 
> ...aber danach hatte ich ja garnicht gesucht. ;-)

Klingt gut. Spricht ja auch prinzipiell nichts dagegen, das was ich da
oben in Python demonstriert habe, auch typsicher zu machen. Genau
deswegen wollte ich ja auch von dir nen Beispiel-Code.

> > > Naja...  OCaml ist eben keine purely functional language.
> > > Sie bietet eben mehrer Möglichkeiten an; und genau deswegen
> > > hat sie bei mir bessere Karten als Haskell.
> > > 
> > > Haskell ist zwar edler, aber eben auch dogmatisch.
> > 
> > Mag sein, aber dort gibt's Listcomprehensions, da sind sie von den
> > Dogmen etwas abgegangen zugunsten von "Usability". Finde ich gut, sowas.
> 
> Wieso Dogmen?
> Funktionale Programmierung heisst nicht, daß man eine bestimmte Syntax
> nimmt.
> Bei der funktionalen programmierung geht es doch darum, daß
> 
> - Funktionen wie normale Values behandelt werden
> - keine Nebeneffekte genutzt werden
> - und weiteres
> 
> Aber die Syntax alleine macht eine Sprache noch nicht zu einer funktionalen
> Sprache.
> 
> Also macht eine Syntax mit Listcomprehensions aus einer funkt. Sprache nicht eine
> nicht-funktionale nur wegen der Syntax.
> 
> Also ist da auch kein Dogma gebraochen, wenn eine FPL Listcomprehensions hat.
> Welches Dogma meinst Du denn?

Ich sah das bisher als Dogma, aber wenn du meinst. Okay, dann werde ich
das in Zukunft nicht mehr so bezeichnen. Soll mich auch recht sein. Ist
ja nur ne Begrifflichkeit.

> > > Zumindest in Haskell kann man aber Funktionskomposition
> > > mit dem "."-Oprator machen.
> > > Dann notiert man das wie eine Pipe(line).
> > > Von rückwärts allerdings, aber immerhin ohne Klamerungs-Arien.
> > 
> > Cool. Nette Sache.
> 
> Eben.
> 
> Aber: Ich dachte Du hast mit Haskell schon mal herum gewerkelt...
> Den Operator kanntest Du nicht?!

Nee, hab ich nicht gebraucht. Und was Großes hab ich in Haskell noch
gar nicht gemacht.

> > Wenn ich z.B. für's Wikidok-Projekt etwas
> > schreibe, werden wohl viel weniger Klassen drin vorkommen.  :-)
> 
> Wikidok-Projekt klingt für mich irgdnwie nach FPLs... ;-)

Ich glaube, das ist das unwichtigste Detail. Die Architektur des
gesamten Systems ist das Innovative. Die konkreten Parser könnten
theoretisch auch jeder in einer anderen Programmiersprache unter
anderen Paradigmen geschrieben sein. :-)

> > Dennoch liefert ersteres einen Iterator zurück, während zweiteres
> > Speicher verschwendet und eine Liste aufbaut. Aber egal, in Python
> > gibt's sicher auch einen "filter", der einen Iterator und keine Liste
> > erzeugt.
> 
> Was hast Du eigentlich für ein problem, eine Liste aufzubauen?

Wenn ich in meinem Code, mal blöd gesagt, die eckigen Klammern gegen
runde ersetze (Listcomp. -> Generator-Expr.) , und schon mit Iteratoren
statt Listen arbeite, dann verwende ich lieber die Iteratoren, ist doch
klar, oder?  :-)

> Ich mache das ganz gerne, daß ich mir immer erst mal alles in den RAM
> lese, Datenstrukturen aufbaue und dann damit arbeite.
> Das hat nämlich den Vorteil, daß man die Datenstrukturen auch mehrfach nutzen
> kann und nicht immer wieder die Datei neu einlesen.

Wenn ich nen Algorithmus habe, der den Datenbestand sequentiell
durchlaufen kann und dabei effizient arbeitet und nur wenig Speicher
verbraucht, dann implementiere ich *diesen* und keinen anderen. Das
ist ja wohl der Mindestanspruch an mich selbst, dass ich mich bemühe,
wenigstens den grundlegenden Algorithmus effizient zu gestalten.

> Und bei Programmänderunegn ist man auch flexibler.

Brauch ich später ne Liste, kippe ich den Iterator in eine Liste ab. :-)

> Und wenn man die Daten übers netz liest, dann kann man cuh nicht
> noch mal neu einlesen, so wie bei einer Datei.
> 
> Grundsätzlich ist es IMHO das sinnvollste, immer erst Datenstrukturen
> aufzubauen und dann damit zu arbeiten.
> 
> Natürlich gibt es Ausnahmen, aber das muß man sich dann gesondert anschauen.

Diese "Ausnahmen" sind aber sehr verbreitet. In meinen bisherigen
Programmen sind die Daten entweder von der Sorte, dass sie in ne
Datenbank gehören (von mir aus auch in ne Datenstruktur im RAM), oder
von der Sorte, dass ich sie sequentiell in eine Datei oder einen Filter
kippe.

Wenn meine Applikation z.B. einen Druckauftrag generiert, dann müssen
mehrere Objekte in zusammenabrbeit eine Datenstruktur aufbauen. z.B.
geht ein Rechnungsobjekt die Rechnungs-Positionen durch, jede generiert
ihre Zeile in der Tabelle. Weiterhin lässt das Rechnungsobjekt die
Adresse vom Kunden-Objekt generieren, und bastelt das alles schön
zusammen. Natürlich noch Drucker-Unabhängig, einfach nur als allgemeine
Datenstruktur, die am Ende z.B. in ein XML-File gekippt werden, aus der
alles weitere generiert wird.

Ich baue also eine Struktur auf, die am Ende nur serialisiert werden
braucht. Muss ich sie deshalb im RAM behalten? Okay, ne Rechnung ist
klein, aber es gibt auch komplexere Sachen. Nur, weil die Datenstruktur
von unterschiedlichen Funktionen zusammengebaut wird, heißt das nicht,
dass zwischendurch Listen aufgebaut werden müssen. Wenn stattdessen z.B.
Iteratoren eingebaut und "zusammengesteckt" werden, ist das effizienter,
und wenn mich das (im Vergleich zur Listen-Variante) null extra Aufwandt
kostet, dann mach ich das doch gleich mit Iteratoren!

So, jetzt hast du nen Anwendungsfall. :-)

> > Aber sobald du Polymorphie brauchst, hast du sowieso einen gewissen
> > Overhead,
> 
> Nö, bei FPLs nicht.
> Da ist die Polymorphie schon gleich mit als Bonus dabei.
> Ist halt ne andere Art von Polymorphie; da geht es um Typen,
> nicht um Methoden-Aufrufe.

Ich hab in Ocaml nachgelesen: Diese Art von Polymorphie (Funktionen-
Polymorphie) meinte ich überhaupt nicht. Ich meinte die Polymorphie
von Objekten.

> > den du in diesem Fall aber gerne in Kauf nimmst, weil er
> > mikrig ist im Vergleich zur Alternative. Ein kleines dummes Beispiel:
> 
> Naja, das beispiel ist immer so ein typisches.
> Ob man in dem Falle OO anwenden sollte... hmhhh.
> Kann man schon machen, bzw. wird oft gemacht.
> Aber FP-Ansatz mag ggf. auch passen.
> 
> Könnte man sich echt mal nen Kopp zu machen... ;-)

Ja, aber erfinde das Rad dabei nicht neu. :-)

Ich kenne ja deinen Kritikpunkt, und habe ihn übrigens schon vor langer
Zeit in meinen Überleggungen berücksichtigt, ohne zu wissen, dass man
das "funktionale Herangehensweise" nennt. Auch ich möchte auf den
Datenstrukturen möglichst wenig Overhead, und insbesondere nicht um
jedes einzelne Objekt herum einen weiteren Wrapper. Aber egal, das führt
hier zu weit. Wenn ich mein großes Projekt irgendwann mal vorstelle,
werde ich mich bemühen, die funktionalen Design-Aspekte ebenfalls als
solche herauszuarbeiten und zu benennen, genau wie die OO Aspekte.

> > 	for obj in liste:
> > 	    obj.draw()
> > 
> > 
> > Es kommt heraus:
> > 
> > 	Kreis mit Radius 1
> > 	Rechteck der Form 2 x 3
> > 	Kreis mit Radius 4
> > 
> > 
> > Von mir aus gib Rechteck und Kreis noch eine gemeinsame Basisklasse,
> > welche die Methode "draw()" vorschreibt. In Python ist das nicht
> > notwendig, hauptsache die Objekte haben die Methode. Klassenhierarchien
> > und Vererbung gibt's in Python natürlich trotzdem.
> > 
> > Der "OO-Overhead" ist minimal im Vergleich zur Alternative, nämlich
> > eine Monster-Draw-Funktion zu haben.
> 
> Warum Monster-draw-Funktion?
> 
> Man kann das bestimmt auch ohne OO hin bekommen und ohne
> Monster-draw-Funktionen.
> Wieso Monster?

Mit Monster meine ich eine Methode, die Rechtecke, Kreise, und so
ziemlich sonst alle gemetreischen Figuren zeichnen können soll.
Sowas zu schreiben, oder an diese Stelle einen Dispatcher hinzupacken,
wäre totaler Schwachsinn. Dann doch lieber die paar Bytes OO-Overhead
für die VMT.

> > > OO: Laufzeit-Overhead.
> > 
> > Nicht unbedingt. Wenn man es missbraucht, dann schon. Aber wenn man es
> > wirklich mit Klassen zu tun hat, sollte man sie in der Programmiersprache
> > auch als Klassen programmieren können.
> 
> Was meinst Du damit?
> Du meinst, wenn man die Sachlage, die man im Rechner abbildet auch
> in der Real-World OO-like gut beschrieben wrden können?
> Wenn also OO eine brauchbare Hrangehensweise darstellen?

Wie ein anderer Poster bereits bemerkte: Wenn ich tatsächlich mehrere
Funktionen habe, die an gewisse Daten gebunden sind, dann sollte man
sie nach OO-Manier zu einer Klasse zusammenfassen.

Wenn ich jedoch Reine-Daten-Klassen oder Reine-Funktionen-Klassen habe,
dann sollte ich dafür native Datenstrukturen bzw. Module nehmen.

Diese Grundregel, OO nur dann zu nutzen wenn es auch sinnvoll ist, wird
in Python genauso empfohlen wie in jeder anderen vernünftigen Sprache
auch. Java z.B. stellt sich da leider quer, und will, dass man alles in
Klassen packt.

> [...]
> > Hat man natürlich nur-Daten-Objekte
> > oder nur-Funktionen-Objekte, sieht das etwas anders aus. Deiner
> > Pauschalisierung muss ich also entschieden widersprechen.
> 
> Das war keine Pauschalisierung.
> Das war  gemeint in dem Sinne, daß OO dann unnötig Overhead einbringt, wenn
> es das falsche Modell ist.
> 
> (Oft wird das auch so gemacht, weil OO ja meist auch dogmatisch eingesetzt wird...)

ACK.


Viele Grüße,

	Volker

-- 
Volker Grabsch
---<<(())>>---
Administrator
NotJustHosting GbR



Mehr Informationen über die Mailingliste linux-l