In che modo i proponenti della programmazione funzionale rispondono a questa affermazione nel codice completo?

30

Nella pagina 839 della seconda edizione, Steve McConnell sta discutendo su tutti i modi in cui i programmatori possono "conquistare la complessità" nei grandi programmi. I suoi consigli culminano con questa affermazione:

"La programmazione orientata agli oggetti fornisce un livello di astrazione che si applica a algoritmi e dati allo stesso tempo , una sorta di astrazione che la sola decomposizione funzionale non ha fornito."

Accoppiato con la sua conclusione che "ridurre la complessità è probabilmente la chiave più importante per essere un programmatore efficace" (stessa pagina), questa sembra quasi una sfida per la programmazione funzionale.

Il dibattito tra FP e OO è spesso inquadrato dai proponenti di FP attorno alle questioni di complessità che derivano specificamente dalle sfide della concorrenza o della parallelizzazione. Ma la concorrenza non è certamente l'unico tipo di complessità che i programmatori di software devono conquistare. Forse concentrarsi sulla riduzione di un tipo di complessità aumenta notevolmente in altre dimensioni, così che per molti casi il guadagno non vale il costo.

Se spostassimo i termini del confronto tra FP e OO da problemi particolari come la concorrenza o la riusabilità alla gestione della complessità globale, come sarebbe il dibattito?

Modifica

Il contrasto che volevo sottolineare è che OO sembra incapsulare e astratto lontano dalla complessità di dati e algoritmi, mentre la programmazione funzionale sembra incoraggiare a lasciare i dettagli di implementazione delle strutture di dati più "esposti" in tutto il programma.

Vedi, per esempio, Stuart Halloway (un sostenitore di Clojure FP) qui che afferma che "l'eccessiva specificazione dei tipi di dati" è "conseguenza negativa dello stile OOM idiomatico" e favorisce la concettualizzazione di un AddressBook come semplice vettore o mappa invece di un oggetto OO più ricco con ulteriori (non-vector & ; non-maplike) proprietà e metodi. (Inoltre, i sostenitori di OO e Domain-Driven Design possono dire che esporre un AddressBook come un vettore o una mappa sovraesposta i dati incapsulati a metodi che sono irrilevanti o addirittura pericolosi dal punto di vista del dominio).

    
posta dan 12.01.2012 - 04:02
fonte

10 risposte

13

Ricorda che il libro è stato scritto per oltre 20 anni. Per i programmatori professionisti del giorno, FP non esisteva - era interamente nel regno degli accademici e degli ampli; i ricercatori.

Dobbiamo inquadrare la "decomposizione funzionale" nel contesto appropriato del lavoro. L'autore non si riferisce alla programmazione funzionale. Abbiamo bisogno di ricollegarlo a "programmazione strutturata" e al pasticcio pieno di GOTO che è venuto prima. Se il tuo punto di riferimento è un vecchio FORTRAN / COBOL / BASIC che non aveva funzioni (forse, se sei stato fortunato, avresti ottenuto un singolo livello di GOSUB) e tutte le tue variabili sono globali, potendo abbattere il tuo programma in strati di funzioni è un grande vantaggio.

OOP è un ulteriore raffinamento in questo tipo di "decomposizione funzionale". Non solo puoi raggruppare le istruzioni insieme nelle funzioni, ma puoi raggruppare le funzioni correlate con i dati su cui stanno lavorando. Il risultato è una parte di codice chiaramente definita che puoi guardare e comprendere (idealmente) senza dover inseguire tutto intorno alla base di codice per trovare cos'altro potrebbe operare sui tuoi dati.

    
risposta data 30.01.2013 - 22:56
fonte
27

Immagino che i proponenti della programmazione funzionale sostengano che la maggior parte dei linguaggi FP fornisce più mezzi di astrazione che "sola decomposizione funzionale" e in effetti consentono mezzi di astrazione comparabili in potenza a quelli delle lingue orientate agli oggetti. Ad esempio si potrebbero citare le classi di tipo Haskell oi moduli di ordine superiore di ML come tali mezzi di astrazione. Quindi l'affermazione (che sono quasi certo riguardo all'orientamento agli oggetti rispetto alla programmazione procedurale, non alla programmazione funzionale) non si applica a loro.

Va inoltre sottolineato che FP e OOP sono concetti ortogonali e non si escludono a vicenda. Quindi non ha senso confrontarli l'uno con l'altro. Si potrebbe benissimo confrontare "OOP imperativo" (ad es. Java) con "OOP funzionale" (ad esempio Scala), ma l'affermazione citata non si applica a tale confronto.

    
risposta data 12.01.2012 - 04:20
fonte
7

Trovo estremamente utile la programmazione funzionale nella gestione della complessità. Si tende a pensare alla complessità in un modo diverso, definendolo come funzioni che agiscono su dati immutabili a diversi livelli piuttosto che incapsularsi in un senso OOP.

Ad esempio, ho recentemente scritto un gioco in Clojure e l'intero stato del gioco è stato definito in una singola struttura di dati immutabile:

(def starting-game-state {:map ....
                          :player ....
                          :weather ....
                          :other-stuff ....}

E il ciclo di gioco principale potrebbe essere definito come l'applicazione di alcune funzioni pure allo stato di gioco in un ciclo:

 (loop [initial-state starting-game-state]
   (let [user-input (get-user-input)
         game-state (update-game initial-state user-input)]
     (draw-screen game-state)
     (if-not (game-ended? game-state) (recur game-state))))

La funzione chiave chiamata è update-game , che esegue un passo di simulazione dato un precedente stato di gioco e alcuni input dell'utente, e restituisce il nuovo stato di gioco.

Quindi dov'è la complessità? Secondo me è stato gestito abbastanza bene:

  • Certamente la funzione di aggiornamento del gioco fa un sacco di lavoro, ma è essa stessa costruita componendo altre funzioni, quindi è in realtà piuttosto semplice. Una volta scesi di alcuni livelli, le funzioni sono ancora piuttosto semplici, facendo qualcosa come "aggiungi un oggetto a una tessera mappa".
  • Certamente lo stato del gioco è una grande struttura dati. Ma ancora una volta, è stato creato componendo strutture di dati di livello inferiore. Inoltre è "dati puri" piuttosto che avere metodi incorporati o definizione di classe richiesta (si può pensare a un oggetto JSON immutabile molto efficiente, se lo si desidera), quindi c'è pochissimo testo di riferimento.

OOP può anche gestire la complessità attraverso l'incapsulamento, ma se si confronta questo con OOP, il funzionale ha alcuni vantaggi molto grandi:

  • La struttura dei dati sullo stato del gioco è immutabile, quindi è possibile eseguire molte elaborazioni in parallelo. Ad esempio, è perfettamente sicuro che un rendering richiama lo schermo a sorteggio in un thread diverso dalla logica di gioco: non possono influire l'uno sull'altro o vedere uno stato incoerente. Questo è sorprendentemente difficile con un grande oggetto grafico mutevole ......
  • Puoi fare un'istantanea dello stato del gioco in qualsiasi momento. Le riproduzioni sono banali (qualsiasi grazie alle persistenti strutture dati di Clojure, le copie occupano pochissima memoria poiché la maggior parte dei dati è condivisa). Puoi anche eseguire update-game per "prevedere il futuro" per aiutare l'IA a valutare mosse diverse, ad esempio.
  • In nessun caso ho dovuto effettuare alcun compromesso difficile da inserire nel paradigma OOP, come la definizione di una rigida gerarchia di classe. In questo senso, la struttura dei dati funzionali si comporta più come un sistema flessibile basato su prototipi.

Infine, per le persone che sono interessate a maggiori informazioni su come gestire la complessità nei linguaggi funzionali rispetto a OOP, consiglio caldamente il video del discorso chiave di Rich Hickey Simple Made Easy (filmato alla Strange Loop conferenza tecnologica)

    
risposta data 12.01.2012 - 06:33
fonte
3

"Object-oriented programming provides a level of abstraction that applies to algorithms and data at the same time, a kind of abstraction that functional decomposition alone didn't provide."

La scomposizione funzionale solo non è sufficiente per creare algoritmi o programmi di qualsiasi genere: è necessario anche rappresentarli. Penso che la dichiarazione di cui sopra implicitamente implichi (o almeno possa essere intesa come) che i "dati" nel caso funzionale sono del tipo più rudimentale: solo liste di simboli e nient'altro. Programmare in un tale linguaggio ovviamente non è molto conveniente. Tuttavia, molti, specialmente i linguaggi nuovi e moderni, funzionali (o multiparadigm), come Clojure, offrono strutture di dati avanzate: non solo elenchi, ma anche stringhe, vettori, mappe e insiemi, record, strutture e oggetti! - con metadati e polimorfismo.

L'enorme successo pratico delle astrazioni OO non può essere contestato. Ma è l'ultima parola? Come hai scritto, i problemi di concorrenza sono già il principale problema, e il classico OO non ha alcuna idea di concorrenza. Di conseguenza, le soluzioni OO de facto per affrontare la concorrenza sono solo nastro adesivo doppietto sovrapposto: funziona, ma è facile rovinarsi, prende una quantità considerevole di risorse cerebrali dall'impegno essenziale a portata di mano e non si adatta bene. Forse è possibile prendere il meglio di molti mondi. Questo è ciò che i moderni linguaggi multiparadigm stanno perseguendo.

    
risposta data 12.01.2012 - 08:52
fonte
2

Lo stato mutevole è la radice della maggior parte delle complessità e dei problemi legati alla programmazione e alla progettazione di software / sistema.

OO abbraccia lo stato mutabile. FP aborza lo stato mutevole.

Sia OO che FP hanno i loro usi e amp; punti dolci. Scegliere saggiamente. E ricorda l'adagio: "Le chiusure sono oggetti poveri dell'uomo, gli oggetti sono la chiusura di un povero uomo".

    
risposta data 12.01.2012 - 12:39
fonte
1

La programmazione funzionale può avere oggetti, ma questi oggetti tendono ad essere immutabili. Le funzioni pure (funzioni senza effetti collaterali) operano quindi su tali strutture di dati. È possibile creare oggetti immutabili in linguaggi di programmazione orientati agli oggetti, ma non sono stati progettati per farlo e non è così che tendono ad essere utilizzati. Ciò rende difficile ragionare sui programmi orientati agli oggetti.

Facciamo un esempio molto semplice. Diciamo che Oracle ha deciso che Java Strings dovrebbe avere un metodo inverso e hai scritto il seguente codice.

String x = "abc";
StringBuffer y = new StringBuffer(x);
y.reverse();
x.reverse();
x.toString().equals(y.toString());

a cosa valuta l'ultima riga? Hai bisogno di una conoscenza speciale della classe String per sapere che ciò valuterà come falso.

Cosa succede se ho creato la mia classe WuHoString

String x = "abc";
WuHoString y = new WuHoString(x);
y.reverse();
x.reverse();
x.toString().equals(y.toString())

È impossibile sapere a cosa valuta l'ultima riga.

In uno stile di programmazione funzionale dovrebbe essere scritto più come segue:

String x;
equals(toString(reverse(x)), toString(reverse(WuHoString(x))))

e dovrebbe essere vero.

Se 1 funzione in una delle classi più elementari è così difficile da ragionare, allora ci si chiede se l'introduzione di questa idea di oggetti mutabili abbia aumentato o diminuito la complessità.

Ovviamente ci sono tutti i tipi di definizioni di ciò che costituisce un oggetto orientato e cosa significa essere funzionale e cosa significa avere entrambi. Per me puoi avere uno "stile di programmazione funzionale" in lingue diverse che non hanno funzioni come le funzioni di prima classe, ma altri linguaggi sono fatti per questo.

    
risposta data 12.01.2012 - 06:01
fonte
0

Penso che nella maggior parte dei casi la classica astrazione OOP non copra la complessità della concorrenza. Quindi OOP (con il suo significato originale) non esclude FP, ed è per questo che vediamo cose come scala.

    
risposta data 12.01.2012 - 04:26
fonte
0

La risposta dipende dalla lingua. I Lisps, ad esempio, hanno il codice veramente accurato che è del codice - gli algoritmi che scrivi sono in realtà solo elenchi Lisp! Memorizzate i dati nello stesso modo in cui scrivete il programma. Questa astrazione è allo stesso tempo più semplice e più completa di OOP e ti consente di fare cose davvero belle (controlla le macro).

Haskell (e un linguaggio simile, immagino) hanno una risposta completamente diversa: i tipi di dati algebrici. Un tipo di dati algebrico è come una struttura C , ma con più opzioni. Questi tipi di dati forniscono l'astrazione necessaria per modellare i dati; le funzioni forniscono l'astrazione necessaria per modellare gli algoritmi. Le classi di tipi e altre funzioni avanzate forniscono un livello di astrazione pari a superiore su entrambi.

Ad esempio, sto lavorando su un linguaggio di programmazione chiamato TPL per divertimento. I tipi di dati algebrici rendono veramente facile rappresentare i valori:

data TPLValue = Null
              | Number Integer
              | String String
              | List [TPLValue]
              | Function [TPLValue] TPLValue
              -- There's more in the real code...

Ciò che questo dice - in un modo molto visivo - è che un TPLValue (qualsiasi valore nella mia lingua) può essere un Null o un Number con un valore Integer o anche un Function con un elenco di valori (i parametri) e un valore finale (il corpo).

Avanti Posso usare le classi di tipi per codificare un comportamento comune. Ad esempio, potrei creare TPLValue e istanza di Show , il che significa che può essere convertito in una stringa.

Inoltre, posso usare le mie classi di tipo quando devo specificare il comportamento di alcuni tipi (compresi quelli che non ho implementato personalmente). Ad esempio, ho una classe di tipo Extractable che mi consente di scrivere una funzione che prende un TPLValue e restituisce un valore normale appropriato. Pertanto extract può convertire Number in Integer o String in String fintanto che Integer e String sono istanze di Extractable .

Infine, la logica principale del mio programma è in diverse funzioni come eval e apply . Questi sono davvero il nucleo: prendono TPLValue s e li trasformano in più TPLValue s, oltre a gestire lo stato e gli errori.

Nel complesso, le astrazioni che sto usando nel mio codice Haskell sono in realtà più potenti di quelle che avrei usato in un linguaggio OOP.

    
risposta data 12.01.2012 - 11:43
fonte
0

La frase citata non ha più alcuna validità, per quanto posso vedere.

Le lingue OO contemporanee non possono astrarre su tipi il cui tipo non è *, cioè tipi di tipi più elevati sono sconosciuti. Il loro sistema di tipi non consente di esprimere l'idea di "un contenitore con elementi Int, che consente di mappare una funzione sugli elementi".

Quindi, questa funzione di base come Haskells

fmap :: Functor f => (a -> b) -> f a -> f b 

non può essere scritto facilmente in Java *), ad esempio, almeno non in un modo sicuro. Quindi, per ottenere le funzionalità di base, è necessario scrivere molto standard, poiché è necessario

  1. un metodo per applicare una funzione semplice agli elementi di una lista
  2. un metodo per applicare la stessa semplice funzione agli elementi di un array
  3. un metodo per applicare la stessa semplice funzione ai valori di un hash,
  4. .... set
  5. .... albero
  6. ... 10. test unitari per lo stesso

Eppure, questi cinque metodi sono fondamentalmente lo stesso codice, ne danno o ne prendono alcuni. Al contrario, in Haskell, avrei bisogno di:

  1. Un'istanza di Functor per elenco, array, mappa, set e struttura ad albero (principalmente predefinita, o può essere derivata automaticamente dal compilatore)
  2. la semplice funzione

Si noti che questo non cambierà con Java 8 (solo che si possono applicare le funzioni più facilmente, ma poi, esattamente, si materializzerà il problema sopra. Finché non si hanno nemmeno funzioni di ordine superiore, si è più probabilmente non è nemmeno in grado di capire a cosa servono i tipi più elevati.)

Anche i nuovi linguaggi OO come Ceylon non hanno tipi più elevati. (Ho chiesto recentemente a Gavin King e mi ha detto che non era importante in questo momento.) Non so su Kotlin, però.

*) Per essere onesti, puoi avere un'interfaccia Functor che ha un metodo fmap. La cosa brutta è che non puoi dire: Hey, so come implementare fmap per la classe di libreria SuperConcurrentBlockedDoublyLinkedDequeHasMap, caro compilatore, per favore accetta che d'ora in poi tutte le SuperConcurrentBlockedDoublyLinkedDequeHasMaps sono Functors.

    
risposta data 31.01.2013 - 00:19
fonte
-2

Chiunque abbia mai programmato in dBase potrebbe sapere quanto siano utili macro a linea singola per creare codice riutilizzabile. Anche se non ho programmato in Lisp, ho letto da molti altri che giurano su macro di compilazione. L'idea di iniettare codice nel tuo codice in fase di compilazione viene utilizzata in una forma semplice in ogni programma C con la direttiva "include". Perché Lisp può farlo con un programma Lisp e poiché Lisp è altamente riflettente, puoi includere molto più flessibile.

Qualsiasi programmatore che prenda una stringa di testo arbitraria dal web e lo trasmetta al suo database non è un programmatore. Allo stesso modo, chiunque possa consentire ai dati "utente" di diventare automaticamente codice eseguibile è ovviamente stupido. Ciò non significa che consentire ai programmi di manipolare i dati al momento dell'esecuzione e quindi eseguirli come codice è una cattiva idea. Credo che questa tecnica sarà indispensabile in futuro, che avrà un codice "intelligente" che in realtà scrive la maggior parte dei programmi. L'intero "problema di dati / codice" o non è una questione di sicurezza nella lingua.

Uno dei problemi con la maggior parte delle lingue è che sono stati creati per una persona single off line per eseguire alcune funzioni per se stessi. I programmi del mondo reale richiedono che molte persone abbiano accesso in qualsiasi momento e allo stesso tempo da più core e più cluster di computer. La sicurezza dovrebbe essere una parte della lingua piuttosto che del sistema operativo e in un futuro non troppo lontano.

    
risposta data 30.01.2013 - 20:09
fonte