Auf Seite 839 der zweiten Ausgabe erörtert Steve McConnell alle Möglichkeiten, wie Programmierer die Komplexität in großen Programmen "beherrschen" können. Seine Tipps gipfeln in dieser Aussage:
"Objektorientierte Programmierung bietet eine Abstraktionsebene, die gleichzeitig für Algorithmen und Daten gilt , eine Art von Abstraktion, die die funktionale Zerlegung allein nicht bot."
In Verbindung mit seiner Schlussfolgerung, dass "die Reduzierung der Komplexität wohl der wichtigste Schlüssel zu einem effektiven Programmierer ist" (dieselbe Seite), scheint dies eine echte Herausforderung für die funktionale Programmierung zu sein.
Die Debatte zwischen FP und OO wird häufig von FP-Befürwortern über die Komplexität geführt, die sich speziell aus den Herausforderungen der Parallelität oder Parallelisierung ergibt. Aber Parallelität ist sicherlich nicht die einzige Art von Komplexität, die Software-Programmierer erobern müssen. Wenn Sie sich vielleicht darauf konzentrieren, eine Art von Komplexität zu reduzieren, erhöht sich dies in anderen Dimensionen erheblich, sodass der Gewinn in vielen Fällen die Kosten nicht wert ist.
Wie würde diese Debatte aussehen, wenn wir die Vergleichsbedingungen zwischen FP und OO von bestimmten Aspekten wie Parallelität oder Wiederverwendbarkeit auf das Management globaler Komplexität verlagern würden?
BEARBEITEN
Der Kontrast, den ich hervorheben wollte, ist, dass OO die Komplexität sowohl von Daten als auch von Algorithmen zu kapseln und zu abstrahieren scheint, wohingegen die funktionale Programmierung zu ermutigen scheint, die Implementierungsdetails von Datenstrukturen im gesamten Programm "exponierter" zu lassen.
Siehe z. B. Stuart Halloway (ein Befürworter von Clojure FP), der hier sagt, dass "die Überbestimmung von Datentypen" eine "negative Folge des idiomatischen OO-Stils" ist und die Konzeption eines Adressbuchs als einfacher Vektor oder Karte anstelle eines reichhaltigeren OO-Objekts bevorzugt mit zusätzlichen (nicht vektoriellen und nicht kartenartigen) Eigenschaften und Methoden. (Auch OO- und Domain-Driven-Design-Befürworter können sagen, dass das Offenlegen eines Adressbuchs als Vektor oder Karte die eingekapselten Daten gegenüber Methoden überbelichtet, die vom Standpunkt der Domäne irrelevant oder sogar gefährlich sind.)
Antworten:
Denken Sie daran, dass das Buch über 20 Jahre geschrieben wurde. Für professionelle Programmierer dieser Zeit existierte FP nicht - es befand sich ausschließlich im Bereich von Akademikern und Forschern.
Wir müssen die "funktionale Zerlegung" in den richtigen Kontext der Arbeit stellen. Der Autor bezieht sich nicht auf die funktionale Programmierung. Wir müssen dies auf die "strukturierte Programmierung" und das
GOTO
gefüllte Chaos zurückführen, das davor auftrat. Wenn Ihr Bezugspunkt ein altes FORTRAN / COBOL / BASIC ist, das keine Funktionen hatte (wenn Sie Glück hatten, könnten Sie eine einzige Ebene von GOSUB erhalten) und alle Ihre Variablen global sind, können Sie Ihr Programm auflösen in Schichten von Funktionen ist ein großer Segen.OOP ist eine weitere Verfeinerung dieser Art von "funktionaler Zersetzung". Sie können nicht nur Anweisungen in Funktionen bündeln, sondern auch verwandte Funktionen mit den Daten gruppieren, an denen sie arbeiten. Das Ergebnis ist ein klar definierter Code, den Sie (im Idealfall) anzeigen und verstehen können, ohne die gesamte Codebasis nachverfolgen zu müssen, um herauszufinden, was sonst noch mit Ihren Daten zu tun hat.
quelle
Ich stelle mir vor, dass Befürworter der funktionalen Programmierung argumentieren würden, dass die meisten FP-Sprachen mehr Abstraktionsmittel als "funktionale Zerlegung allein" bieten und in der Tat Abstraktionsmittel zulassen, die leistungsmäßig mit denen objektorientierter Sprachen vergleichbar sind. Beispielsweise könnte man Haskells Typklassen oder MLs Module höherer Ordnung als solche Abstraktionsmittel anführen. Daher trifft die Aussage (bei der ich mir ziemlich sicher bin, dass es um Objektorientierung vs. prozedurale Programmierung und nicht um funktionale Programmierung geht) nicht auf sie zu.
Es sollte auch darauf hingewiesen werden, dass FP und OOP orthogonale Konzepte sind und sich nicht gegenseitig ausschließen. Es macht also keinen Sinn, sie miteinander zu vergleichen. Sie könnten sehr gut "imperative OOP" (z. B. Java) mit "funktionale OOP" (z. B. Scala) vergleichen, aber die von Ihnen angegebene Aussage würde für diesen Vergleich nicht gelten.
quelle
Ich finde die funktionale Programmierung beim Umgang mit Komplexität äußerst hilfreich. Sie tendieren jedoch dazu, Komplexität auf andere Weise zu betrachten und sie als Funktionen zu definieren, die auf unveränderlichen Daten auf verschiedenen Ebenen agieren, anstatt sie in einem OOP-Sinne zu kapseln.
Ich habe zum Beispiel kürzlich ein Spiel in Clojure geschrieben und der gesamte Status des Spiels wurde in einer einzigen unveränderlichen Datenstruktur definiert:
Und die Hauptspielschleife könnte so definiert werden, dass sie einige reine Funktionen auf den Spielstatus in einer Schleife anwendet:
Die aufgerufene Schlüsselfunktion ist
update-game
, die einen Simulationsschritt ausführt, wenn ein vorheriger Spielstatus und Benutzereingaben vorliegen, und den neuen Spielstatus zurückgibt.Wo ist die Komplexität? Meiner Meinung nach ist es ganz gut gelungen:
OOP kann auch Komplexität durch Kapselung verwalten. Vergleicht man dies jedoch mit OOP, hat das Funktionsprinzip einige sehr große Vorteile:
Für Leute, die mehr über den Umgang mit Komplexität in funktionalen und OOP-Sprachen erfahren möchten, empfehle ich das Video von Rich Hickeys Keynote " Simple Made Easy" (gedreht auf der Strange Loop- Technologiekonferenz).
quelle
Eine funktionale Zerlegung allein reicht nicht aus, um einen Algorithmus oder ein Programm zu erstellen: Sie müssen auch die Daten darstellen. Ich denke, die obige Aussage geht implizit davon aus (oder kann zumindest so verstanden werden), dass die "Daten" im funktionalen Fall von der rudimentärsten Art sind: nur Listen von Symbolen und sonst nichts. Das Programmieren in einer solchen Sprache ist offensichtlich nicht sehr bequem. Viele, insbesondere die neuen und modernen, funktionalen (oder multiparadigmen) Sprachen wie Clojure bieten jedoch umfangreiche Datenstrukturen: nicht nur Listen, sondern auch Zeichenfolgen, Vektoren, Karten und Mengen, Datensätze, Strukturen und Objekte! - Mit Metadaten und Polymorphismus.
Der enorme praktische Erfolg von OO-Abstraktionen ist kaum zu bestreiten. Aber ist es das letzte Wort? Wie Sie geschrieben haben, sind Nebenläufigkeitsprobleme bereits das größte Problem, und das klassische OO enthält überhaupt keine Vorstellung von Nebenläufigkeit. Daher sind die OO-Lösungen für den Umgang mit Nebenläufigkeiten de facto nur überlagertes Klebeband: Sie funktionieren, sind jedoch leicht zu beschädigen, nehmen der eigentlichen Aufgabe beträchtliche Mengen an Gehirnressourcen ab und lassen sich nicht gut skalieren. Vielleicht ist es möglich, das Beste aus vielen Welten zu holen. Das ist, was moderne Multiparadigmasprachen verfolgen.
quelle
Der veränderbare Zustand ist die Wurzel der meisten Komplexitäten und Probleme im Zusammenhang mit der Programmierung und dem Entwurf von Software / Systemen.
OO umfasst veränderlichen Zustand. FP verabscheut veränderlichen Zustand.
Sowohl OO als auch FP haben ihre Verwendungszwecke und Sweet Spots. Mit Bedacht wählen. Und denken Sie an das Sprichwort: "Verschlüsse sind Gegenstände des armen Mannes. Gegenstände sind Verschlüsse des armen Mannes."
quelle
Funktionale Programmierung kann Objekte haben, aber diese Objekte neigen dazu, unveränderlich zu sein. Reine Funktionen (Funktionen ohne Nebenwirkungen) arbeiten dann mit diesen Datenstrukturen. Es ist möglich, unveränderliche Objekte in objektorientierten Programmiersprachen zu erstellen, aber sie wurden nicht dafür entwickelt, und so werden sie normalerweise nicht verwendet. Dies macht es schwierig, über objektorientierte Programme nachzudenken.
Nehmen wir ein sehr einfaches Beispiel. Angenommen, Oracle hat entschieden, dass Java Strings eine umgekehrte Methode haben soll, und Sie haben den folgenden Code geschrieben.
Was wertet die letzte Zeile aus? Sie benötigen spezielle Kenntnisse der String-Klasse, um zu wissen, dass diese als falsch ausgewertet wird.
Was wäre, wenn ich meine eigene Klasse WuHoString machen würde?
Es ist unmöglich zu wissen, was die letzte Zeile ergibt.
In einem funktionalen Programmierstil würde es mehr wie folgt geschrieben werden:
und es sollte wahr sein.
Wenn eine Funktion in einer der grundlegendsten Klassen so schwer zu erklären ist, fragt man sich, ob die Einführung dieser Idee von veränderlichen Objekten die Komplexität erhöht oder verringert hat.
Offensichtlich gibt es alle möglichen Definitionen dessen, was objektorientiert ist und was es bedeutet, funktional zu sein und was es bedeutet, beides zu haben. Für mich kann man einen "funktionalen Programmierstil" in der Sprache haben, der keine erstklassigen Funktionen hat, sondern für den andere Sprachen gemacht sind.
quelle
Ich denke, in den meisten Fällen deckt die klassische OOP-Abstraktion die Komplexität der Parallelität nicht ab. Daher schließt OOP (nach seiner ursprünglichen Bedeutung) FP nicht aus, und deshalb sehen wir Dinge wie Scala.
quelle
Die Antwort hängt von der Sprache ab. Lisps, zum Beispiel, haben die wirklich ordentlich richtig , dass Code ist Daten - die Algorithmen schreiben Sie sind eigentlich nur Listen Lisp! Sie speichern Daten genauso, wie Sie das Programm schreiben. Diese Abstraktion ist gleichzeitig einfacher und gründlicher als OOP und lässt Sie einige wirklich nette Dinge tun (siehe Makros).
Haskell (und eine ähnliche Sprache, wie ich mir vorstelle) haben eine ganz andere Antwort: algebraische Datentypen. Ein algebraischer Datentyp ist wie a
C
Struktur, bietet jedoch mehr Optionen. Diese Datentypen stellen die Abstraktion bereit, die zum Modellieren von Daten erforderlich ist. Funktionen liefern die zur Modellierung von Algorithmen erforderliche Abstraktion. Typklassen und andere erweiterte Funktionen bieten eine noch höhere Abstraktionsebene für beide.Zum Beispiel arbeite ich aus Spaß an einer Programmiersprache namens TPL. Algebraische Datentypen machen es wirklich einfach Werte zu repräsentieren:
Dies besagt auf sehr visuelle Weise, dass ein TPLValue (jeder Wert in meiner Sprache) ein
Null
oder ein sein kannNumber
mit einemInteger
Wert oder sogarFunction
eine Liste von Werten (die Parameter) und ein Endwert (der Körper) sein kann ).Als nächstes kann ich Typklassen verwenden, um ein allgemeines Verhalten zu kodieren. Zum Beispiel könnte ich machen
TPLValue
InstanzShow
erstellen, die bedeutet, dass sie in eine Zeichenfolge konvertiert werden kann.Außerdem kann ich meine eigenen Typklassen verwenden, wenn ich das Verhalten bestimmter Typen angeben muss (einschließlich solcher, die ich nicht selbst implementiert habe). Zum Beispiel habe ich eine
Extractable
Typklasse, mit der ich eine Funktion schreiben kann, die a annimmtTPLValue
und einen entsprechenden normalen Wert zurückgibt. Somitextract
kann man aNumber
zu aInteger
oder aString
zu aString
solange konvertierenInteger
undString
Instanzen vonExtractable
.Schließlich ist die Hauptlogik meines Programms in mehreren Funktionen wie
eval
undapply
. Dies ist wirklich der Kern - sie nehmenTPLValue
s und verwandeln sie in mehrTPLValue
s sowie den Umgang mit Status und Fehlern.Insgesamt sind die Abstraktionen ich benutze in meinem Haskell Code tatsächlich mehr leistungsfähig als das, was ich in einer OOP - Sprache verwendet haben.
quelle
eval
. "Hey, sieh mich an! Ich muss nicht meine eigenen Sicherheitslücken schreiben. Ich habe eine willkürliche Sicherheitslücke, die direkt in die Programmiersprache eingebaut ist!" Das Verknüpfen von Daten mit Code ist die Hauptursache für eine der beiden beliebtesten Sicherheitslücken aller Zeiten. Jedes Mal, wenn Sie sehen, dass jemand wegen eines SQL-Injection-Angriffs (unter anderem) gehackt wird, weiß ein Programmierer nicht, wie er Daten richtig vom Code trennen kann.eval
hängt nicht sehr von der Struktur von Lisp ab - Sie können eseval
in Sprachen wie JavaScript und Python haben. Die wahre Kraft liegt im Schreiben von Makros, also Programmen, die auf Programme wie Daten einwirken und andere Programme ausgeben. Dies macht die Sprache sehr flexibel und das Erstellen leistungsfähiger Abstraktionen einfach.and
. kurzschließenor
.let
.let-rec
.cond
.defn
. Keines davon kann mit Funktionen in anwendbaren Auftragssprachen implementiert werden.for
(Listenverständnisse).dotimes
.doto
.and
!" Ich höre: "Hey schau mich an, meine Sprache ist so verkrüppelt, dass es nicht einmal zu einem Kurzschluss kommtand
und ich das Rad für alles neu erfinden muss !"Der zitierte Satz hat meines Erachtens keine Gültigkeit mehr.
Zeitgenössische OO-Sprachen können nicht über Typen abstrahieren, deren Typ nicht * ist, dh Typen höherer Typen sind unbekannt. Ihr Typensystem erlaubt es nicht, die Idee von "einem Container mit Int-Elementen, der es erlaubt, eine Funktion über die Elemente abzubilden" auszudrücken.
Daher funktioniert diese Grundfunktion wie Haskells
kann nicht einfach in Java *) geschrieben werden, zumindest nicht typsicher. Um grundlegende Funktionen zu erhalten, müssen Sie daher eine Menge Boilerplate schreiben, da Sie benötigen
Und doch sind diese fünf Methoden im Grunde derselbe Code, geben oder nehmen einige. Im Gegensatz dazu würde ich in Haskell brauchen:
Beachten Sie, dass sich dies mit Java 8 nicht ändern wird (nur, dass man Funktionen einfacher anwenden kann, aber dann genau das oben genannte Problem auftritt. Solange Sie nicht einmal Funktionen höherer Ordnung haben, sind Sie höchstwahrscheinlich nicht einmal verstehen, wofür höherwertige Typen gut sind.)
Selbst neue OO-Sprachen wie Ceylon haben keine höherwertigen Typen. (Ich habe kürzlich Gavin King gefragt, und er sagte mir, es sei zu diesem Zeitpunkt nicht wichtig.) Ich weiß jedoch nichts über Kotlin.
*) Um fair zu sein, können Sie ein Interface Functor haben, das eine Methode fmap hat. Schlimm ist, dass man nicht sagen kann: Hey, ich weiß, wie man fmap für die Bibliotheksklasse SuperConcurrentBlockedDoublyLinkedDequeHasMap implementiert.
quelle
Jeder, der jemals in dBase programmiert hat, würde wissen, wie nützlich einzeilige Makros für die Erstellung von wiederverwendbarem Code sind. Obwohl ich nicht in Lisp programmiert habe, habe ich von vielen anderen gelesen, die auf Makros zur Kompilierungszeit schwören. Die Idee, Code zur Kompilierzeit in Ihren Code einzufügen, wird in jedem C-Programm mit der Anweisung "include" auf einfache Weise verwendet. Weil Lisp dies mit einem Lisp-Programm tun kann und weil Lisp sehr reflektierend ist, erhalten Sie viel flexiblere Includes.
Jeder Programmierer, der nur einen beliebigen Text aus dem Internet entnimmt und an seine Datenbank weiterleitet, ist kein Programmierer. Ebenso ist jeder, der zulässt, dass "Benutzer" -Daten automatisch zu ausführbarem Code werden, offensichtlich dumm. Das bedeutet nicht, dass es eine schlechte Idee ist, Programmen zu erlauben, Daten zur Ausführungszeit zu manipulieren und sie dann als Code auszuführen. Ich glaube, dass diese Technik in Zukunft unverzichtbar sein wird, da "intelligenter" Code die meisten Programme tatsächlich schreibt. Das ganze "Daten / Code-Problem" oder nicht ist eine Frage der Sicherheit in der Sprache.
Eines der Probleme mit den meisten Sprachen ist, dass sie für eine einzelne Offline-Person erstellt wurden, um einige Funktionen für sich selbst auszuführen. Für reale Programme ist es erforderlich, dass viele Benutzer jederzeit und gleichzeitig von mehreren Kernen und mehreren Computerclustern aus Zugriff haben. Sicherheit sollte ein Teil der Sprache sein und nicht des Betriebssystems, und in nicht allzu ferner Zukunft wird es sein.
quelle