Welche funktionalen Merkmale sind für die Vorteile, die sie mit sich bringen, eine kleine OOP-Verwirrung wert?

13

Nach dem Erlernen der funktionalen Programmierung in Haskell und F # scheint das OOP-Paradigma mit Klassen, Schnittstellen und Objekten rückständig zu sein. Welche Aspekte von FP kann ich zur Arbeit bringen, die meine Mitarbeiter verstehen können? Lohnt es sich, mit meinem Chef über die Umschulung meines Teams zu sprechen, damit wir sie einsetzen können?

Mögliche Aspekte von FP:

  • Unveränderlichkeit
  • Teilanwendung und Currying
  • First-Class-Funktionen (Funktionszeiger / Funktionsobjekte / Strategiemuster)
  • Lazy Evaluation (und Monaden)
  • Reine Funktionen (keine Nebenwirkungen)
  • Ausdrücke (vs. Anweisungen - jede Codezeile erzeugt einen Wert anstatt oder zusätzlich zu Nebenwirkungen)
  • Rekursion
  • Mustervergleich

Ist es ein Alleskönner, bei dem wir alles tun können, was die Programmiersprache unterstützt, bis zu der Grenze, in der die Sprache es unterstützt? Oder gibt es eine bessere Richtlinie?

Trident D'Gao
quelle
6
Ich hatte eine ähnliche Erfahrung. Nach ungefähr 2 Monaten Schmerz fing ich an, ein gutes Gleichgewicht zwischen "Sachen, die auf Objekte abgebildet sind" und "Sachen, die auf Funktionen abgebildet sind" zu finden. Es hilft, ernsthafte Hackerangriffe in einer Sprache durchzuführen, die beides unterstützt. Am Ende sind sowohl meine FP- als auch meine OOP-Fähigkeiten erheblich verbessert
Daniel Gratzer,
3
FWIW, Linq ist sowohl funktional als auch faul, und Sie können die funktionale Programmierung in C # simulieren, indem Sie statische Methoden verwenden und die Persistenz des Status vermeiden.
Robert Harvey
1
An dieser Stelle sollten Sie sicp lesen . Es ist kostenlos und gut geschrieben. Es bietet einen schönen Vergleich zwischen den beiden Paradigmen.
Simon Bergot
4
FP und OOP sind gleichzeitig in gewissem Sinne orthogonal und in gewissem Sinne dual. Bei OOP geht es um Datenabstraktion, bei FP um (das Fehlen von) Nebenwirkungen. Ob Sie Nebenwirkungen haben oder nicht, hängt davon ab, wie Sie Ihre Daten abstrahieren. Lambda Calculus ist zum Beispiel sowohl funktional als auch objektorientiert. Ja, FP verwendet normalerweise abstrakte Datentypen und keine Objekte. Sie können jedoch genauso gut Objekte verwenden, ohne weniger FP zu haben. OTOH, es gibt auch eine tiefe Beziehung: Eine Funktion ist mit nur einer Methode isomorph zu einem Objekt (so werden sie in Java "gefälscht" und in Java8 implementiert, ...
Jörg W Mittag
3
Ich denke, der stärkste Aspekt Ihrer Frage hat mit der Lesbarkeit zu tun. "Wie viel funktionaler Programmierstil ist angemessen, um in einem objektorientierten Geschäft zu arbeiten?" Oder welche funktionalen Merkmale für die Vorteile, die sie mit sich bringen, eine kleine OOP-Verwirrung wert sind.
GlenPeterson

Antworten:

13

Funktionale Programmierung ist ein anderes Paradigma als objektorientierte Programmierung (eine andere Denkweise und eine andere Art, über Programme zu denken). Sie haben erkannt, dass es hier mehr als eine Möglichkeit (objektorientiert) gibt, über Probleme und deren Lösungen nachzudenken. Es gibt andere (prozedurale und generische Programmierung kommen in den Sinn). Wie Sie auf dieses neue Wissen reagieren, ob Sie diese neuen Tools und Ansätze akzeptieren und in Ihre Fähigkeiten integrieren, entscheidet darüber, ob Sie wachsen und ein vollständigerer, erfahrener Entwickler werden.

Wir sind alle geschult und beherrschen eine gewisse Komplexität. Ich nenne dies gerne das Hrair- Limit einer Person (von Watership Down, wie hoch kann man zählen). Es ist eine großartige Sache, Ihren Verstand zu erweitern, mehr Optionen in Betracht zu ziehen und mehr Werkzeuge zu haben, um Probleme anzugehen und zu lösen. Aber es ist eine Veränderung, die Sie aus Ihrer Komfortzone zieht.

Ein Problem, auf das Sie stoßen können, ist, dass Sie weniger zufrieden sind, der Menge "Alles ist ein Objekt" zu folgen. Möglicherweise müssen Sie Geduld entwickeln, wenn Sie mit Menschen arbeiten, die möglicherweise nicht verstehen (oder verstehen möchten), warum ein funktionaler Ansatz für die Softwareentwicklung bei bestimmten Problemen gut funktioniert. Genauso gut funktioniert ein generischer Programmieransatz bei bestimmten Problemen.

Viel Glück!

ChuckCottrill
quelle
3
Ich möchte hinzufügen, dass man einige traditionelle OOP-Konzepte besser verstehen kann, wenn man mit funktionalen Sprachen wie Haskell oder Clojure arbeitet. Persönlich wurde mir klar, dass Polymorphismus wirklich ein wichtiges Konzept ist (Interfaces in Java oder Typeclasses in Haskell), während Vererbung (was ich für ein definierendes Konzept hielt) eine Art seltsame Abstraktion ist.
Wirrbel
6

Die funktionale Programmierung führt zu einer sehr praktischen, bodenständigen Produktivität beim alltäglichen Schreiben von Code: Einige Funktionen begünstigen die Kürze. Dies ist großartig, da je weniger Code Sie schreiben, desto weniger Fehler auftreten und desto weniger Wartung erforderlich ist.

Als Mathematiker finde ich ausgefallene Funktionen sehr ansprechend, aber sie sind normalerweise hilfreich beim Entwerfen einer Anwendung: Diese Strukturen können viele Invarianten des Programms in der Programmstruktur codieren, ohne diese Invarianten durch Variablen darzustellen.

Meine Lieblingskombination mag ziemlich trivial aussehen, ich glaube jedoch, dass sie einen sehr hohen Einfluss auf die Produktivität hat. Diese Kombination ist Teilanwendung und Currying und First-Class-Funktionen, die ich neu benennen würde, um nie wieder eine for-Schleife zu schreiben . Ich wurde vor kurzem für einen C ++ - Job eingestellt und merkwürdigerweise habe ich die Gewohnheit verloren, for-loops zu schreiben !

Die Kombination aus Rekursion und Mustererkennung macht die Notwendigkeit dieses Besucherdesignmusters zunichte . Vergleichen Sie einfach den Code, den Sie benötigen, um einen Evaluator für boolesche Ausdrücke zu programmieren: In jeder funktionalen Programmiersprache sollten dies etwa 15 Codezeilen sein. In OOP ist es das Richtige, dieses Besucher- Designmuster zu verwenden, das dieses Spielzeugbeispiel zum Vorbild macht ein umfangreicher Aufsatz. Die Vorteile liegen auf der Hand und mir sind keine Unannehmlichkeiten bekannt.

user40989
quelle
2
Ich stimme voll und ganz zu, aber ich hatte Zurückschub von Leuten, denen in der Branche eher zugestimmt wird: Sie wissen, dass sie Besuchermuster haben, sie haben es oft gesehen und verwendet, so dass der Code darin etwas ist, das sie verstehen und mit dem sie vertraut sind Ein anderer Ansatz, der lächerlich einfacher und leichter ist, ist fremd und daher für sie schwieriger. Dies ist eine unglückliche Tatsache in der Branche, in der über 15 Jahre OOP in jeden Programmierkopf verstrickt waren, dass über 100 Codezeilen für sie einfacher zu verstehen sind als 10, nur weil sie sich über 100 Zeilen gemerkt haben, nachdem sie sie über ein Jahrzehnt wiederholt haben
Jimmy Hoffa
1
-1 - Mehr knapper Code bedeutet nicht, dass Sie "weniger" Code schreiben. Sie schreiben denselben Code mit weniger Zeichen. Wenn überhaupt, machen Sie mehr Fehler, weil der Code (oft) schwerer zu lesen ist.
Telastyn
8
@ Telastyn: Terse ist nicht dasselbe wie unlesbar. Außerdem haben riesige Mengen von aufgeblähtem Boilerplate ihre eigene Art, unlesbar zu sein.
Michael Shaw
1
@Telastyn Ich denke, Sie haben gerade den wahren Kern hier angesprochen, ja, knapp kann schlecht und unleserlich sein, aufgebläht kann schlecht und unleserlich sein, aber der Schlüssel ist nicht die variable Länge und der verwirrend geschriebene Code. Der Schlüssel ist, wie Sie oben erwähnen, die Anzahl der Operationen. Ich bin nicht der Meinung, dass die Anzahl der Operationen nicht mit der Wartbarkeit korreliert. Ich denke, dass weniger Dinge (mit klar geschriebenem Code) der Lesbarkeit und Wartbarkeit zugute kommen. Offensichtlich hilft es nicht, die gleiche Anzahl von Dingen mit Einzelbuchstabenfunktion und Variablennamen zu tun. Eine gute FP benötigt deutlich weniger Operationen, die noch klar geschrieben sind
Jimmy Hoffa,
2
@ user949300: Wenn Sie ein völlig anderes Beispiel wünschen, wie wäre es mit diesem Java 8-Beispiel ?: list.forEach(System.out::println);Aus FP-Sicht printlnbesteht eine Funktion aus zwei Argumenten, einem Ziel PrintStreamund einem Wert, Objectaber die Methode Collection's forEacherwartet eine Funktion mit nur einem Argument die auf jedes Element angewendet werden kann. Das erste Argument ist also an die Instanz gebunden, die darin besteht System.out, eine neue Funktion mit einem Argument zu ergeben. Es ist einfacher alsBiConsumer<…> c=PrintStream::println; PrintStream a1=System.out; list.forEach(a2 -> c.accept(a1, a2));
Holger
5

Sie müssen möglicherweise einschränken, welche Teile Ihres Wissens Sie bei der Arbeit verwenden, wie Superman vorgeben muss, Clark Kent zu sein, um die Vorteile eines normalen Lebens zu genießen. Aber mehr zu wissen, wird dir niemals weh tun. Allerdings sind einige Aspekte der funktionalen Programmierung für einen objektorientierten Shop geeignet, und andere Aspekte können es wert sein, mit Ihrem Chef darüber zu sprechen, damit Sie den durchschnittlichen Wissensstand Ihres Shops erhöhen und dadurch besseren Code schreiben können.

FP und OOP schließen sich nicht aus. Schau dir Scala an. Einige halten es für das Schlimmste, weil es ein unreines FP ist, andere halten es aus demselben Grund für das Beste.

Hier sind einige Aspekte, die sich hervorragend für OOP eignen:

  • Reine Funktionen (keine Nebenwirkungen) - Jede mir bekannte Programmiersprache unterstützt dies. Sie machen Ihren Code viel einfacher zu überlegen und sollten verwendet werden, wann immer dies praktisch ist. Sie müssen es nicht FP nennen. Nennen Sie es einfach gute Codierungspraktiken.

  • Unveränderlichkeit: String ist wohl das am häufigsten verwendete Java-Objekt und unveränderlich. In meinem Blog beschreibe ich unveränderliche Java-Objekte und unveränderliche Java-Sammlungen . Einige davon können auf Sie zutreffen.

  • First-Class-Funktionen (Funktionszeiger / Funktionsobjekte / Strategiemuster) - Java hat seit Version 1.1 eine manipulierte, mutierte Version davon, wobei die meisten API-Klassen (und es gibt Hunderte) die Listener-Schnittstelle implementieren. Runnable ist wahrscheinlich das am häufigsten verwendete Funktionsobjekt. Erstklassige Funktionen sind mehr Arbeit, um in einer Sprache zu programmieren, die sie nicht von Haus aus unterstützt, aber manchmal den zusätzlichen Aufwand wert, wenn sie andere Aspekte Ihres Codes vereinfachen.

  • Rekursion ist nützlich für die Verarbeitung von Bäumen. In einem OOP-Shop ist dies wahrscheinlich die primäre angemessene Verwendung der Rekursion. Die Verwendung von Rekursion zum Spaß in OOP sollte wahrscheinlich verpönt werden, wenn aus keinem anderen Grund als den meisten OOP-Sprachen standardmäßig kein Stapelspeicher zur Verfügung steht, um dies zu einer guten Idee zu machen.

  • Ausdrücke (im Gegensatz zu Anweisungen - jede Codezeile erzeugt einen Wert anstelle von oder zusätzlich zu den Nebenwirkungen) - Der einzige auswertende Operator in C, C ++ und Java ist der Ternäre Operator . Auf meinem Blog diskutiere ich über die angemessene Verwendung. Möglicherweise schreiben Sie einige einfache Funktionen, die in hohem Maße wiederverwendbar und auswertbar sind.

  • Lazy Evaluation (und Monaden) - hauptsächlich auf Lazy Initialization in OOP beschränkt. Ohne Sprachfunktionen, die dies unterstützen, finden Sie möglicherweise einige nützliche APIs, aber das Schreiben eigener APIs ist schwierig. Maximieren Sie stattdessen die Verwendung von Streams - Beispiele finden Sie in den Benutzeroberflächen "Writer" und "Reader".

  • Teilanwendung und Currying - Ohne erstklassige Funktionen nicht praktikabel.

  • Pattern Matching - in der Regel in OOP entmutigt.

Zusammenfassend sollte die Arbeit meines Erachtens kein alltägliches Unterfangen sein, bei dem Sie alles tun können, was die Programmiersprache bis zu der von der Sprache unterstützten Grenze unterstützt. Ich denke, die Lesbarkeit durch Ihre Mitarbeiter sollte Ihr Lackmustest für gemieteten Code sein. Wo das dich am meisten ärgert, würde ich versuchen, eine Ausbildung bei der Arbeit zu beginnen, um den Horizont deiner Kollegen zu erweitern.

GlenPeterson
quelle
Seitdem ich FP gelernt habe, habe ich mich daran gewöhnt, Dinge so zu gestalten, dass sie fließende Benutzeroberflächen haben, die zu Ausdrücken führen, einer Funktion mit einer einzigen Anweisung, die eine Reihe von Dingen ausführt. Es ist die beste Methode, die Sie jemals finden werden, aber es ist ein Ansatz, der auf natürliche Weise von der Reinheit abhängt, wenn Sie feststellen, dass Sie keine leeren Methoden mehr haben. Auf diese Weise ist Ihr Ausdruckspunkt der einzige Punkt, mit dem ich nicht einverstanden bin. Alles andere hängt von meinen eigenen Erfahrungen mit dem Lernen von FP und der Arbeit an einem .NET-Tagesjob ab
Jimmy Hoffa,
Was mich in C # jetzt wirklich stört, ist, dass ich aus zwei einfachen Gründen keine Delegaten anstelle von Ein-Methoden-Schnittstellen verwenden kann: 1. Sie können keine rekursiven Lambas ohne einen Hack erstellen (indem Sie zuerst null und dann eine Lambda-Sekunde zuweisen) oder ein Y- Kombinator (der in C # höllisch hässlich ist). 2. Es gibt keine Typaliase, die Sie in einem Projektbereich verwenden können, sodass die Signaturen Ihrer Delegierten ziemlich schnell nicht mehr verwaltet werden können. Nur aus diesen zwei dummen Gründen kann ich C # nicht mehr genießen, weil das einzige, was ich machen kann, die Verwendung von Ein-Methoden-Schnittstellen ist, was nur unnötige zusätzliche Arbeit ist.
Trident D'Gao
@bonomo Java 8 verfügt über eine generische java.util.function.BiConsumer, die in C # public interface BiConsumer<T, U> { public void accept(T t, U u); }nützlich sein kann: In java.util.function gibt es weitere nützliche Funktionsschnittstellen.
GlenPeterson,
@bonomo Hey, ich verstehe, das ist der Schmerz von Haskell. Wann immer Sie jemanden lesen, der sagt, dass "Learning FP mich bei OOP besser gemacht hat", bedeutet, dass er Ruby oder etwas gelernt hat, das nicht rein und aussagekräftig ist wie Haskell. Haskell macht klar, dass der OOP-Wert unnütz niedrig ist. Der größte Schmerz, auf den Sie stoßen, besteht darin, dass ein Constraint-basierter Rückschluss auf einen Typ nicht entschieden werden kann, wenn Sie sich nicht im HM-Typensystem befinden. Daher wird ein cosntraint-basierter Rückschluss auf einen Typ nicht vollständig durchgeführt: blogs.msdn.com/b/ericlippert/archive / 2012/03/09 /…
Jimmy Hoffa
1
Most OOP languages don't have the stack space for it "Ja wirklich?" Sie benötigen lediglich 30 Rekursionsebenen, um Milliarden von Knoten in einem ausgeglichenen Binärbaum zu verwalten. Ich bin mir ziemlich sicher, dass mein Stapelplatz für viel mehr Ebenen als diese geeignet ist.
Robert Harvey
3

Neben funktionaler Programmierung und objektorientierter Programmierung gibt es auch deklarative Programmierung (SQL, XQuery). Das Erlernen jedes Stils hilft Ihnen dabei, neue Erkenntnisse zu gewinnen, und Sie lernen, das richtige Werkzeug für den Job auszuwählen.

Aber ja, es kann sehr frustrierend sein, Code in einer Sprache zu schreiben und zu wissen, dass Sie, wenn Sie etwas anderes verwenden, für eine bestimmte Problemdomäne viel produktiver sein könnten. Selbst wenn Sie eine Sprache wie Java verwenden, ist es möglich, Konzepte von FP auf Ihren Java-Code anzuwenden, wenn auch auf Umwegen. Das Guava-Framework macht zum Beispiel einiges davon.

sgwizdak
quelle
2

Als Programmierer sollte man nie aufhören zu lernen. Das heißt, es ist sehr interessant, dass das Lernen von FP Ihre OOP-Fähigkeiten beeinträchtigt. Ich neige dazu, OOP zu lernen, als zu lernen, wie man Fahrrad fährt. du vergisst nie, wie es geht.

Als ich die Vor- und Nachteile von FP lernte, musste ich mathematischer denken und mir einen besseren Überblick über die Mittel verschaffen, mit denen ich Software schreibe. Das ist meine persönliche Erfahrung.

Je mehr Erfahrung Sie sammeln, desto schwerer wird es, grundlegende Programmierkonzepte zu verlieren. Deshalb schlage ich vor, dass Sie das FP so lange locker angehen, bis die OOP-Konzepte in Ihrem Kopf vollständig gefestigt sind. FP ist ein eindeutiger Paradigmenwechsel. Viel Glück!

Bobby Gammill
quelle
4
OOP zu lernen ist wie das Kriechen lernen. Aber wenn Sie erst einmal aufgestanden sind, können Sie nur dann kriechen, wenn Sie zu betrunken sind. Natürlich kannst du nicht vergessen, wie es geht, aber normalerweise möchtest du es nicht. Und es wird eine schmerzhafte Erfahrung sein, mit den Crawlern zu gehen, wenn Sie wissen, dass Sie rennen können.
SK-logic
@ SK-Logik, ich mag Ihre Methapher
Trident D'Gao
@ SK-Logic: Wie lernt man Imperative Programmierung? Sich auf dem Bauch mitschleppen?
Robert Harvey
@ Robert Harvey versucht, mit einem rostigen Löffel und einem Stapel Lochkarten im Untergrund zu graben.
Jimmy Hoffa
0

Es gibt bereits viele gute Antworten, daher werden meine Antworten einen Teil Ihrer Frage beantworten. Ich nehme die Prämisse Ihrer Frage in den Hintergrund, da sich OOP und Funktionsmerkmale nicht gegenseitig ausschließen.

Wenn Sie C ++ 11 verwenden, enthält die Sprach- / Standardbibliothek viele dieser funktionalen Programmierfunktionen, die sich gut mit OOP kombinieren lassen. Natürlich bin ich mir nicht sicher, wie gut TMP von Ihrem Chef oder Ihren Mitarbeitern empfangen wird, aber der Punkt ist, dass Sie viele dieser Funktionen in irgendeiner Form in nicht funktionalen / OOP-Sprachen wie C ++ erhalten können.

Die Verwendung von Vorlagen mit Rekursion zur Kompilierungszeit basiert auf Ihren ersten 3 Punkten.

  • Unveränderlichkeit
  • Rekursion
  • Mustervergleich

Da Template-Werte unveränderlich sind (Konstanten für die Kompilierungszeit), wird jede Iteration unter Verwendung von Rekursion ausgeführt, und die Verzweigung wird unter Verwendung von (mehr oder weniger) Musterabgleich in Form einer Überladungsauflösung ausgeführt.

Was die anderen Punkte betrifft, verwenden Sie std::bindund, um std::functioneine teilweise Funktionsanwendung zu erhalten, und Funktionszeiger sind in die Sprache integriert. Aufrufbare Objekte sind Funktionsobjekte (sowie Teilfunktionsanwendungen). Beachten Sie, dass mit aufrufbaren Objekten diejenigen gemeint sind, die ihre definieren operator ().

Lazy Evaluation und reine Funktionen wären etwas schwieriger; Für reine Funktionen können Sie Lambda-Funktionen verwenden, die nur nach Wert erfassen. Dies ist jedoch nicht ideal.

Im Folgenden finden Sie ein Beispiel für die Verwendung der Rekursion zur Kompilierungszeit mit Teilfunktionsanwendung. Es ist ein etwas ausgeklügeltes Beispiel, aber es zeigt die meisten der obigen Punkte. Es bindet die Werte in einem gegebenen Tupel rekursiv an eine gegebene Funktion und erzeugt ein (aufrufbares) Funktionsobjekt

#include <iostream>
#include <functional>

//holds a compile-time index sequence
template<std::size_t ... >
struct index_seq
{};

//builds the index_seq<...> struct with the indices (boils down to compile-time indexing)
template<std::size_t N, std::size_t ... Seq>
struct gen_indices
  : gen_indices<N-1, N-1, Seq ... >
{};

template<std::size_t ... Seq>
struct gen_indices<0, Seq ... >
{
    typedef index_seq<Seq ... > type;
};


template <typename RType>
struct bind_to_fcn
{
    template <class Fcn, class ... Args>
    std::function<RType()> fcn_bind(Fcn fcn, std::tuple<Args...> params)
    {
        return bindFunc(typename gen_indices<sizeof...(Args)>::type(), fcn, params);
    }

    template<std::size_t ... Seq, class Fcn, class ... Args>
    std::function<RType()> bindFunc(index_seq<Seq...>, Fcn fcn, std::tuple<Args...> params)
    {
        return std::bind(fcn, std::get<Seq>(params) ...);
    }
};

//some arbitrary testing function to use
double foo(int x, float y, double z)
{
    return x + y + z;
}

int main(void)
{
    //some tuple of parameters to use in the function call
    std::tuple<int, float, double> t = std::make_tuple(1, 2.04, 0.1);                                                                                                                                                                                                      
    typedef double(*SumFcn)(int,float,double);

    bind_to_fcn<double> binder;
    auto other_fcn_obj = binder.fcn_bind<SumFcn>(foo, t);
    std::cout << other_fcn_obj() << std::endl;
}
Alrikai
quelle