Archiv verlassen und diese Seite im Standarddesign anzeigen : Designfrage: Funktional vs OOP
Hallo,
ich wollte mal ganz Allgemein fragen in welche Richtung ihr eher programmiert.
eher OOP: fixe Operationsmenge und variable Anzahl Daten/Eigenschaften
eher Funktional: fixe Daten/Eigenschaften und variable Operationsmenge
konkretes Beispiel:
Ich habe ein Modul für neuronale Netzwerke geschrieben
Dieses Netzwerk besteht aus Schichten (hier Klassen) die verschiedene Operatoren auf Memberdaten anwenden. OOP eignet sich hier super.
Baselayer {
DoCalc()
DataType Data;
}
Superlayer extends Baselayer {
DoCalc()
DataType MoreData;
}
Brain {
array: {BaseLayer, Superlayer...}
}
Jetzt sollen bestimmte Operationen auf alle Schichten angewendet werden (z.b. Optimierung, Normierung).
Die Operationen sind immer gleich (die Funktionen sind statisch!) nur die Daten sind andere.
Variante 1: Jede Schicht erhält zusätzliche Membervariablen die die Operationen pro Schicht ausführen.
IMO umständlich und auch unübersichtlich, da diese Operationen ersteinmal nichts mit einer Schicht zu tun haben und auch in einem anderen Kontext verwendet werden können - nicht müssen.
Baselayer {
DoCalc()
DoOptimize()
DataType Data;
}
Superlayer extends Baselayer {
DoCalc()
DoOptimize()
DataType MoreData;
}
Brain {
array: {BaseLayer, Superlayer...}
Optimize() {
for every layer: DoOptimize()
}
}
Noch blöder wird es, wenn die Funktionen einen globalen Zustand haben sollen, auf Funktionen der Basisklasse zugreifen muss oder mehrere (Laufzeit-)Varianten existieren
dann also eher über einen Funktionsparameter:
Variante 2:
Baselayer {
DoCalc()
DoOptimize(OptimizerClass)
DataType Data;
}
Superlayer extends Baselayer {
DoCalc()
DoOptimize(OptimizerClass)
DataType MoreData;
}
Brain {
array: {BaseLayer, Superlayer...}
Optimize() {
for every layer: DoOptimize(OptimizerClass)
}
OptimizerClass
}
Was ich irgendwie schöner finde ist ein eher funktionaler Ansatz bei dem die Schichten nix vom Optimierer wissen und ihre Daten rausrücken, also z.b. so:
Variante 3:
Baselayer {
DoCalc()
DataType Data;
}
Superlayer extends Baselayer {
DoCalc()
DataType MoreData;
}
Brain {
array: {BaseLayer, Superlayer...}
Optimize() {
for every layer:
OptimizerClass(get data from layer)
}
OptimizerClass
}
Was bevorzugt ihr? :)
Unfug
2018-06-20, 20:24:59
Um ganz ehrlich zu sein....
Ich habe deine Sachen nicht verstanden :D
Dein Code sieht überall nach OOP aus. Da ist nichts funktionales dran meiner Meinung nach.
Um deine Frage aber zu beantworten.
Ich benutze einen Hybriden Ansatz.
for, foreach etc aus OOP.
Funktionen höherer Ordnung, keine Side Effects, Trennung zwischen BusinessLogik und Logik aus FP.
Gerade am Anfang fand ich FP echt kompliziert zu denken. Inzwischen hat es mir so oft vieles einfacher gemacht. Allein die Sache mit Funktion höherer Ordnung...Ein Traum
EPIC_FAIL
2018-06-20, 20:44:20
Ich glaube da brauchen wir noch ein paar Infos:
Modifiziert der Optimierungsschritt interne Member (Data, MoreData). Ist der Baselayer eine Art abstrakte Klasse für n-viele weitere abgeleitete Klassen? Kann es mehr als einen Optimierer geben (unterschiedliche, nicht mehrere Durchläufe eines Optimierers).
Funktion höherer Ordnung (englisch higher-order function) ist in der Informatik eine Funktion, die Funktionen als Argumente erhält
Wäre Variante 2. Klar nehme ich "std::transform", Lambdas und so nen Kram.
die Optimizerfunktion wird über ein Lambda abgebildet. Damit ist "for_each" auch eine funktion höhere Ordnung.
Mir geht es eher um die Frage:
Klassisch OOP: maximale Kapselung der Daten (am Besten nix raus lassen)
"Leaky" OOP: also auch Zugriff von außen auf die Daten
"Funktional" ist für mich auch eine statische Funktion :freak:
Solange sie keinen Zustand hat, auf andere Daten als die Eingabedaten angewiesen ist und keine anderen Daten verändert. Kenne die genaue Definition nicht...gibt es eine?
Ich glaube da brauchen wir noch ein paar Infos:
Modifiziert der Optimierungsschritt interne Member (Data, MoreData).
das ist der Sinn :) - im Sinne von OOP wäre das natürlich finster von außen zu tun ;)
Ist der Baselayer eine Art abstrakte Klasse für n-viele weitere abgeleitete Klassen?
jaja, viele weitere
Kann es mehr als einen Optimierer geben (unterschiedliche, nicht mehrere Durchläufe eines Optimierers).
ja mehrere, deswegen fällt die erste Variante schon raus. Keine Lust den selben Kram 5 mal zu schreiben.
Monger
2018-06-20, 21:15:28
Ich schreib morgen nochmal was dazu, aber was ich aus meinem kurzen Ausflug nach F# gelernt habe: Funktional bedeutet
- klare Trennung von Verhalten und Daten
- keine Seiteneffekte
Das erfüllt keines deiner Beispiele. Jedes davon mischt Methoden und Daten in der selben Klasse.
Ich bin eigentlich eher in der OOP Welt zu Hause, aber die genannten Paradigmen haben unbestreitbare Vorteile.
Naja hier geht es nicht um Erbsenzählen...imo, sondern was am Praktikabelsten ist.
"DoCalc()" ist rein OOP: eine Memberfunktion ändert die Daten des Layerobjektes
"OptimizerClass(get data from layer)" ist (für mich) funktional, gesetzt dem Fall - diese Funktion macht nichts Anderes als die Layerdaten zu verwursten.
mal konkret ausm Code
void UpdateWeightsClassic(LayerData &layer,double alpha)
{
const size_t nsize=layer.weights.size();
const size_t isize=layer.weights[0].size();
for (size_t n=0;n<nsize;n++) {
for (size_t i=0;i<isize;i++)
layer.weights[n][i]+=-alpha*layer.G[n][i];
}
}
Ist das nicht funktional? :>
EPIC_FAIL
2018-06-20, 22:35:37
Eher nicht, da nicht Seiteneffektfrei. Du änderst ja konkret Layerdata, also einen Input. Eine Kopie von Layerdata zu verändern und zurückzugeben wäre hier ein Beispiel für eine "pure function".
Ganon
2018-06-20, 22:48:26
Deine Frage ist imo nicht ob "Funktional oder OOP" sondern ob "Prozedural oder OOP". Funktionale Programmierung und auch "reine funktionale Programmierung" ist da noch mal ein ganz anderes Kapitel.
Solange sie keinen Zustand hat, auf andere Daten als die Eingabedaten angewiesen ist und keine anderen Daten verändert. Kenne die genaue Definition nicht...gibt es eine?
Immutability?
Im Übrigen sind alle deine Ansätze OOP. Mal bringst du Dependency Injection auf die eine oder andere Art rein, aber im Grunde ändert das nichts.
Ich nehme von Vererbung wo es geht Abstand. Lässt sich nicht ganz vermeiden, aber im Prinzip: Composition over Inheritance.
Monger
2018-06-21, 08:00:32
So, hier mal ne Variante die n bissl stärker vom Gedanken der funktionalen Programmierung geprägt ist:
Baselayer {
DataType Data;
}
Superlayer extends Baselayer {
DataType MoreData;
}
Brain {
array: {BaseLayer, Superlayer...}
}
Optimizer {
int DoCalc(BaseLayer) { }
int DoCalc(SuperLayer) { }
Brain Optimize(Brain) {
for every layer:
(get data from layer)
}
}
Strenge Vererbungshierarchien sind nicht so schlimm, wenn sie nur Daten erweitern, aber kein Verhalten haben. In aller Regel schleppt Verhalten halt auch schwergewichtige Abhängigkeiten mit, die man durch alle Objekte durchreichen muss. Wenn man Verhalten von Daten klar trennt, hat man das Problem nicht.
Wenn man solche Baumstrukturen hat, bietet es sich auch an die tief zu kopieren: Baum rein, manipulierter Baum raus. Hat den Vorteil, dass sich z.B. die Optimize Funktion parallelisieren ließe. Immutable Objekte sind von Natur aus threadsicher, deshalb wäre es attraktiv die Baumknoten als solche zu modellieren.
Strenge Vererbungshierarchien sind nicht so schlimm, wenn sie nur Daten erweitern, aber kein Verhalten haben. In aller Regel schleppt Verhalten halt auch schwergewichtige Abhängigkeiten mit, die man durch alle Objekte durchreichen muss.
Das stimmt, deswegen halte ich die Hierarchieebenen rel. flach. Ja könnte man so machen, wie du es vorgeschlagen hast. Also "DoCalc()" einfach für alle Layer überladen.
Eher nicht, da nicht Seiteneffektfrei. Du änderst ja konkret Layerdata, also einen Input. Eine Kopie von Layerdata zu verändern und zurückzugeben wäre hier ein Beispiel für eine "pure function".
Ja lasse ich gelten. Im Sinne der Geschwindigkeit sind temporärere Objekte halt böse. Deswegen versuche ich das zu vermeiden.
Diverse Bibliotheken verwenden sogenannte "Expression Templates" bei dem z.B. der Ausdruck "a*(b+c)" für Vektoren keine tempoäre Objekte erzeugt.
Deine Frage ist imo nicht ob "Funktional oder OOP" sondern ob "Prozedural oder OOP". Funktionale Programmierung und auch "reine funktionale Programmierung" ist da noch mal ein ganz anderes Kapitel.
Ja an dem Punkt hast du Recht. Obwohl ich "prozedural" als eine Verallgemeinerung der funktionalen Programmierung sehen würde.
Immutability?
Im Übrigen sind alle deine Ansätze OOP. Mal bringst du Dependency Injection auf die eine oder andere Art rein, aber im Grunde ändert das nichts.
Ich nehme von Vererbung wo es geht Abstand. Lässt sich nicht ganz vermeiden, aber im Prinzip: Composition over Inheritance.
Ich habe gestern noch auf Stackoverflow gelesen und "Immutability" ist jetzt nicht unbedingt ein Schlüssel zur funktionalen Programmierung - und imo zu kleinlich gedacht.
Welche Definition ich gut fand:
Imperativ: konkreter Algorithmus wird vorgegeben (also wie oben, zwei Schleifen)
Funktional: Ausdruck des "Intends"
Wobei ich schon oft funktional begonnen habe (z.B. std::remove_if und lambdas...) und am Ende wieder imperativ programmierte, aus Geschwindigkeitsgründen.
Hier mal die 2-Norm zum Quadrat eines Vektors aus meinem Code
Imperativ, klassisches C++:
double norm2(const vec1D &v)
{
double sum=0.;
for (size_t i=0;i<v.size();i++) sum+=v[i]*v[i];
return sum;
}
Imperativ, modernes C++:
double norm2(const vec1D &v)
{
double sum=0.;
for (auto x:v) sum+=x;
return sum;
}
Funktional mit Lambda:
double norm2(const vec1D &v)
{
return std::accumulate(begin(v),end(v),0.0,[](auto a,auto b){return a+b*b;});
}
Funktional ohne Lambda:
double norm2(const vec1D &v)
{
return std::inner_product(begin(v),end(v),begin(v),0.0);
}
Was jetzt "schöner" ist, darf jeder selbst entscheiden.
Wie man jetzt z.b. ne Matrixmultiplikation in C++ funktional programmiert, weiß ich nicht.
Monger
2018-06-21, 08:44:01
Im Sinne der Geschwindigkeit sind temporärere Objekte halt böse. Deswegen versuche ich das zu vermeiden.
Kommt auf die Programmiersprache an, aber bei .NET, Java und Konsorten ist das erzeugen von Objekten sackeschnell. Bei veränderlichen Objekten kommst du schnell in Situationen wo du z.B. per Events Änderungen durch einen Baum propagieren musst. Das ist oftmals VIEL langsamer als den Baum neu aufzubauen.
Ja an dem Punkt hast du Recht. Obwohl ich "prozedural" als eine Verallgemeinerung der funktionalen Programmierung sehen würde.
Nein, das sind zwei grundlegend unterschiedliche Paradigmen. Prozedural heißt sinngemäß: Anweisung für Anweisung abarbeiten und einen bestehenden Zustand manipulieren. Funktional heißt u.a.: du formulierst Ausdrücke, die keine Zwischenzustände kennen, aber immer ein Ergebnis haben.
Ich habe gestern noch auf Stackoverflow gelesen und "Immutability" ist jetzt nicht unbedingt ein Schlüssel zur funktionalen Programmierung - und imo zu kleinlich gedacht.
In F# sind ALLE Objekte standardmäßig immutable. Geht auch.
Funktionale Programmierung hat erstmal nichts mit (im)mutable zu tun. Es ist richtig, dass Seiteneffekte vermieden werden aber am Ende hat jedes Programm zumindest implizite Seiteneffeke (Haskell IO) ansonsten würde das Programm nichts machen. Um es mal weiter aufzudröseln: funktionale Programmierung is Teil der deklarativen Programmierung, wenn man dass auf C++ anwenden möchte dann verwendet man zwangsläufig sehr viel overload resolution. Der funktionale Teil kommt von rechnen mit Funktionen: Funktionen sind Variablen gleichgestellt. Wenn man auch dass wieder auf C++ anwendet dann verwendet man sehr viel generic lambdas.
Ob man nun higher order functions oder higher kinded types verwendet hat auch erstmal nichts mit der Definition an sich zu tun.
In den ganzen Beispielen bisher sehe ich keines dass die oben genannten Sachen erfüllt. Ich würde empfehlen das ganze mal in Haskel umzusetzen damit man mal dazu gezwungen wird funktional zu denken.
Exxtreme
2018-06-21, 11:15:34
Also wenn ich entwickle dann mische ich beide Paradigmen, je nach dem was grad passender ist. Wobei mir der OO-Stil vom Mindset her doch viel näher liegt. Bei FP habe ich den Eindruck, da ist zu viel Magie im Spiel und die Syntax empfinde ich auch oft als unlogisch.
void UpdateWeightsClassic(LayerData &layer,double alpha)
{
const size_t nsize=layer.weights.size();
const size_t isize=layer.weights[0].size();
for (size_t n=0;n<nsize;n++) {
for (size_t i=0;i<isize;i++)
layer.weights[n][i]+=-alpha*layer.G[n][i];
}
}
auto UpdateWeightsClassic(double alpha)
{
return [=](auto& layer)
{
const size_t nsize=layer.weights.size();
const size_t isize=layer.weights[0].size();
for (size_t n=0;n<nsize;n++) {
for (size_t i=0;i<isize;i++)
layer.weights[n][i]+=-alpha*layer.G[n][i];
}
}
}
Das währe jetzt funktional inspiriert. Denn nun hast du einen computational builder, wenn du jetzt noch mehr von denen hast dann kannst du dir dein Verhalten aus diesen Bauteilen zusammen-stöpseln und genau dass ist es was die funktionale Programmierung ausmacht.
vBulletin®, Copyright ©2000-2024, Jelsoft Enterprises Ltd.