Alexander Kornrumpf hat geschrieben: ↑18.08.2022, 23:14
Ich weiß nicht wie relevant das für eine research language ohne Nutzer ist (*scnr*) und ob wir mit lambda dasselbe meinen (ich meine eine anonyme Funktion, die ihren äußeren scope capturen kann), aber das will man in einer Sprache eigentlich schon haben heutzutage.
Also die Idee ist schon, etwas auszuprobieren, dass mir etwas über realitätsnahe Programmierung beibringt. D.h. die Sprache sollte so sein, dass man sie verwenden würde, wenn irgendwer eine Runtime, Standardbibliothek und Werkzeuge stellen würde. Alleine kann man das nicht, selbst mit den Mitteln von heute.
Die Diskussion würde ich nicht weiterführen wollen, nicht zuletzt weil ich sagen würde, dass Java und vor allem auch C einfach mehr genutzt wird als Scheme :)
Der Punkt für mich sind folgende Beabachtungen:
Erstmal ist es so, dass eine Sprache Paradigmen in Form von Typisierung und Syntax unterstützt.
Dass OOP FP simulieren kann oder andersrum ist eine verbreitete Illusion, weil man durch die Simulation den Toolsupport kaputt macht bzw. nicht im entferntesten an das rankommt, was man hätte, wenn man gleich das gewünschte eingebaut hätte.
Auf OOP-Seite ist das primär Effektbeschränkung, also z.B. bei bestimmten Subtypen dann die konkreten Implementierungen doch zu kennen.
Auf FP-Seite ist das imho Inlining bzw. Rewriting, was man mit OOP so nicht machen würde.
Manueller Aufwand sind beide Simulationen immer.
Es ist aber leider auch so, dass man nicht einfach in eine Sprache OOP und FP reinbauen kann und glauben, dass das funktioniert.
In aller Regel muss man dann beides irgendwie einschränken, um nicht die Vorteile zu verlieren und erhält dann Fragmente, die nur bedingt kompatibel sind.
Vorabbemerkt muss ich sagen, dass ich großer Fan mancher Muster und Konzepte bin, die die FP-Welt hervorgebracht hat; ich würde generics und deren collection-Implementierungen dazu zählen.
Das Problem mit FP sind vor allem Leute, die FP gut finden.
Die interessieren sich selten dafür, wie Code oder sonst irgendwas tatsächlich repräsentiert ist.
Die glauben auch, das Haskell irgendwie deklarativer ist, als C.
Ich habe auch persönlich noch keinen effektiven Weg gefunden, mit so Leuten zu argumentieren.
Anonyme Funktionen benötigen neben Syntax im Prinzip Funktionstypen, Namen bzw. Repräsentation anonymer Funktionen, dynamische Hüllen für statische Zugriffe und einen Zeigertyp für Funktionszeiger mit Hülle.
Funktionstypen sind kein Problem, die brauche ich schon für die Overloadresolution und die Typinferenz, sind also da.
Namen und Repräsentation bekommt man hin; zur not zählt man die einfach irgendwie durch und präfixt das mit dem Namen der statisch umhüllenden Funktion. Das ist eigentlich dasselbe wie der Zugriff auf ein implizites this. Die Repräsentation ist eigentlich dasselbe wie von einem flachen Typ mit einem def apply.
Die Hülle ist so eine Sache; als Außenstehender denkt man immer erstmal, die Hülle zu identifizieren wäre das Problem. Das stimmt aber nicht. Der Compiler kennt die Definitionen alle und muss sie auch kennen, weil er ja irgendwas machen muss um die Zugriffe zu realisieren. An den Definitionen noch einen Marker unterzubringen, dass man jetzt in einer dynamischen Hülle geteilt existiert ist auch nicht schwer.
Das Problem hier, ist die Bedeutung dieses Markers.
Wenn es eine reine Copy-In-Hülle wäre, wie das z.B. in Java gemacht ist, dann könnte ich das vermutlich auch über ein komplett freies Wochenende implementieren.
Wenn die Hülle By-Reference-Semantik haben soll, dann hat man ein Problem, weil dann für die statische gebundene Variable die Lebenszeit plötzlich nicht mehr durch den Stackframe der statisch umhüllenden Funktion beschränkt wäre, was bedeutet, dass sie nicht auf dem Stack realisiert werden kann, was wiederum bedeutet, dass man Autoboxing braucht, was neben viel Aufwand vor allem ein massives Performanceproblem ist. Meine Erfahrung mit Scala war je nachdem was man so gemacht hat, ein Faktor 10 bis 100. Da ist bei mir Praxisrelevanz zuende.
Dazu kommt dann noch Ressourcenverwaltung, da es plötzlich nicht mehr einfach möglich ist, zu entscheiden, wann man irgendwas freigeben darf.
Mir ist ehrlich gesagt unklar, wie man realitätsnah Autoboxing ohne GC implementieren kann.
Keinen GC zu haben war für mich aber eine bewusste Entscheidung die nicht revidiert wird.
Ein Funktionszeiger ist einfach ein Zeiger.
Bei mir sind alle Zeiger auch tatsächlich Hardwarezeiger, d.h. ihre Repräsentation ist die Adresse des Ziels.
Wenn man gute Performance haben will, wäre mir auch nicht klar, wie man das anders erreichen soll.
Ein Zeiger auf eine anonyme Funktion kann, wenn diese eine nichtleere Hülle hat, aber ganz offensichtlich kein Funktionszeiger sein.
Das kann ich auf zwei Wegen beheben: Entweder ich führe zwei Arten von Funktionszeigern ein oder ich mache die bestehenden Funktionszeiger kaputt.
Beides ist Implementierungsmäßig erstmal kein Problem, den Aufwand wird man aber vermutlich unterschätzen :)
Mein größtes Problem mit FP-Jüngern ist eigentlich, dass viele von denen glauben, dass λ x . 1 denselben Typ haben sollte, wie die innere Funktion in λ y. λ x. y. Ist ja beides eine Funktion von int nach int. Das ist vollkommen absurd, weil sich die Repräsentation fundamental unterscheidet und Computer nunmal nicht Funktionsdefinitionen als zugrundeliegende Berechnungseinheit haben. Zumindest kaufe ich meinen Arbeitsspeicher nicht in GigaFunctions und meine CPU wird nicht in GigaFunctionApplications/s ausgewiesen.
Jetzt ist meine Erfahrung, dass die praktische Verwendung von λ-Ausdrücken eigentlich auf Callbacks und FP-Collection-APIs beschränkt ist.
Nicht zuletzt, weil sowas wie das Scala-ParserCombinators-API einfach unfassbar viel Sprachsupport hat, damit man die Schachtelung der Ergebnisse implizit rückgängig machen kann. (da gibt es sowas das grob ~[x, ~[y, z]] mit case x ~ y ~ z flach klopft).
Was wiederum dazu führt, dass es kaum Menschen verstehen und in vielen Sprachen mit FP-Erweiterungen auch nicht mit der Einfachheit von Scala verwendet werden kann, btw. in nicht-GC-Sprachen hat man auch hier ein massives Ressourcenverwaltungsproblem bei einer Lösung, die fachlich etwas supersimples macht.
Der Punkt für mich jetzt ist, dass ich sowieso schon einen "Calloperator" apply habe. Bei vielen UI-Callbacks sollte man die Funktionen ohnehin irgendwie gruppieren und hätte dann eine spezifische Listenerklasse.
Bei den Collections habe ich eine komplett andere Lösung, die eigentlich meine Reaktion auf die desaströse Performance der Scalacollections ist: Man darf in Tyr Blöcke an Funktionen weitergeben, wenn man die aufgerufene Funktion direkt inlined.
Die Einschränkung heilt das Problem mit der dynamischen Lebenszeit und dem Stackframe.
Das Vorgehen erlaubt dir aber die Abstraktion der Collection zu implementieren, d.h. du musst nicht mehr wissen, wie man am besten alle Elemente aus einem HashSet oder sowas rausnimmt.
Damit das funktioniert, erlaube ich zudem lokale Variablen der aufgerufenen Funktion in den übergebenen Blöcken zu binden.
Das hat interessanterweise als Konsequenz, dass man Sachen machen kann, die mit λs nicht gehen, weil sozusagen zwei anonyme Funktionen *denselben* Parameter binden.
In der theoretischen Betrachtung ist das also wieder ein ganz anderes Konzept.
Die Ressourcenverwaltung löse ich, indem manche Funktionen das aufgerufene Objekt freigeben (mem.consumed).
Ein Beispiel wäre
hier etwa forall.
Du sagst, dass du die lokale Variable c nennen möchtest.
Du übergibst einen Block
Und es kann dir scheißegal sein, ob da jetzt ein Iterator oder ein Index zum Iterieren verwendet wird. Genauso wie du nicht darüber nachdenkst, wie man abbricht, wenn man die Antwort kennt oder in welcher Reihenfolge man die Elemente auswählt. Das sollte dann in der Beschreibung der konkreten Implementierung von forall stehen.
Wenn man an der Stelle sicher ist, dass man einen ArrayBuffer in der Hand hat, im Beispiel ist das so, dann bekommt man die effiziente
Implementierung.
Wenn nicht, dann gibte es halt die
allgemeine.
Nebenbei bemerkt ist das statische Neubinden von inline-Funktionen bei präziserem Zielobjektstyp auch sowas, das man nicht erreichen kann, wenn man OOP mit FP simuliert. Da kassiert man neben dem Overhead auch die Wahl suboptimaler Algorithmen.
Meine zugrundeliegende Wette bei λ vs generalized Binder (Block+Blockparameter) ist natürlich, dass das double-inlining insgesamt günstiger ist, als der Overhead durch die Closures.
Meine Annahme hier ist, dass einerseits sowieso meistens der Code von double-inline nach Optimierung nicht mehr ist, als das was man mit Closures erzeugen würde.
Ebenso nehme ich an, dass Ausführungszeit der Anwendung und Lebenszeit der Entwickler die wesentlichen Optimierungsziele sind.
Ich glaube aber, wenn Sprachen wie C++ oder Java jetzt auch noch generalized Binder abwärtskompatibel nachrüsten würden, dann würde das mehr Chaos als Ordnung stiften.
(der Beitrag ist Urlaub, keine Mittagspause;) )