Quali caratteristiche funzionali valgono un po 'di confusione OOP per i vantaggi che apportano?


13

Dopo aver appreso la programmazione funzionale in Haskell e F #, il paradigma OOP sembra arretrato con classi, interfacce, oggetti. Quali aspetti del PQ posso mettere in atto affinché i miei colleghi possano capire? Vale la pena parlare con il mio capo di stili FP per riqualificare la mia squadra in modo che possiamo usarli?

Possibili aspetti del PQ:

  • Immutabilità
  • Applicazione parziale e curricula
  • Funzioni di prima classe (puntatori a funzione / Oggetti funzionali / Modello di strategia)
  • Valutazione pigra (e monadi)
  • Funzioni pure (senza effetti collaterali)
  • Espressioni (rispetto alle dichiarazioni: ogni riga di codice produce un valore anziché o in aggiunta a causare effetti collaterali)
  • ricorsione
  • Pattern Matching

È gratuito per tutti dove possiamo fare qualunque cosa il linguaggio di programmazione supporti al limite che il linguaggio lo supporta? O c'è una linea guida migliore?


6
Ho avuto un'esperienza simile. Dopo circa 2 mesi di dolore ho iniziato a trovare un buon equilibrio tra "cose ​​che si mappano agli oggetti" e "cose ​​che si mappano alle funzioni". Aiuta a fare degli hacker seri in un linguaggio che supporta entrambi. Alla fine, entrambe le mie competenze in termini di FP e OOP sono notevolmente migliorate
Daniel Gratzer,

3
FWIW, Linq è sia funzionale che pigro e puoi simulare la programmazione funzionale in C # usando metodi statici ed evitando la persistenza dello stato.
Robert Harvey,

1
A questo punto, dovresti leggere sicp . È gratuito e ben scritto. Offre un bel confronto tra i due paradigmi.
Simon Bergot,

4
FP e OOP sono allo stesso tempo in un certo senso ortogonali e in un certo senso doppi. OOP riguarda l'astrazione dei dati, FP riguarda (l'assenza di) effetti collaterali. Indipendentemente dal fatto che tu abbia o meno degli effetti collaterali, è ortogonale a come si astraggono i dati. Lambda Calculus è sia funzionale che orientato agli oggetti, ad esempio. Sì, FP in genere utilizza tipi di dati astratti, non oggetti, ma è possibile utilizzare oggetti invece senza essere meno FP. OTOH, c'è anche una relazione profonda: una funzione è isomorfa a un oggetto con un solo metodo (è così che vengono "falsificati" in Java e implementati in Java8, ...
Jörg W Mittag

3
Penso che l'aspetto più forte della tua domanda riguardi la leggibilità. "Quanto stile di programmazione funzionale è appropriato per lavorare in un negozio orientato agli oggetti?" O quali caratteristiche funzionali valgono un po 'di confusione OOP per i vantaggi che apportano.
GlenPeterson,

Risposte:


13

La programmazione funzionale è un paradigma diverso dalla programmazione orientata agli oggetti (una mentalità diversa e un modo diverso di pensare ai programmi). Hai iniziato a capire che qui c'è più di un modo (orientato agli oggetti) di pensare ai problemi e alle loro soluzioni. Ce ne sono altri (mi viene in mente la programmazione procedurale e generica). Come reagirai a questa nuova conoscenza, se accetti e integri questi nuovi strumenti e approcci nel tuo set di abilità, determinerà se cresci e diventi uno sviluppatore più completo e competente.

Siamo tutti addestrati a gestire e ci sentiamo a nostro agio con un certo livello di complessità. Mi piace chiamare questo limite di hrair di una persona (da Watership Down, quanto in alto puoi contare). È una grande cosa espandere la tua mente, la tua capacità di considerare più opzioni e avere più strumenti per affrontare e risolvere i problemi. Ma è un cambiamento e ti tira fuori dalla tua zona di comfort.

Un problema che potresti incontrare è che diventerai meno contento per seguire la folla "tutto è un oggetto". Potrebbe essere necessario sviluppare la pazienza mentre si lavora con persone che potrebbero non capire (o voler capire) perché un approccio funzionale allo sviluppo del software funziona bene per determinati problemi. Proprio come un approccio di programmazione generico funziona bene per alcuni problemi.

In bocca al lupo!


3
Vorrei aggiungere che si può comprendere meglio alcuni concetti OOP tradizionali quando si lavora con linguaggi funzionali come Haskell o Clojure. Personalmente mi sono reso conto di come il polimorfismo sia davvero un concetto importante (Interfacce in Java o caratteri tipografici in Haskell), mentre l'ereditarietà (quello che pensavo fosse un concetto che definisce) è una specie di strana astrazione.
Wirrbel,

6

La programmazione funzionale produce una produttività molto pratica, concreta, nella scrittura di codice di tutti i giorni: alcune funzionalità favoriscono la terseness, il che è fantastico perché meno codice scrivi, meno guasti fai e meno manutenzione è richiesta.

Essendo un matematico, trovo interessanti elementi funzionali fantasiosi, ma di solito è utile quando si progetta un'applicazione: queste strutture possono codificare nella struttura del programma molti invarianti del programma, senza rappresentare questi invarianti con variabili.

La mia combinazione preferita può sembrare piuttosto banale, tuttavia credo che abbia un impatto molto elevato sulla produttività. Questa combinazione è Applicazione parziale e Currying e funzioni di prima classe che non scriverei mai più un for-loop : invece passa il corpo del loop a una funzione di iterazione o mappatura. Sono stato recentemente assunto per un lavoro in C ++ e ho notato in modo divertente, ho completamente perso l'abitudine di scrivere loop for!

La combinazione di Ricorsione e Pattern Matching annulla la necessità di quel modello di design del Visitatore . Basta confrontare il codice necessario per programmare un valutatore di espressioni booleane: in qualsiasi linguaggio di programmazione funzionale questo dovrebbe essere di circa 15 righe di codice, in OOP la cosa giusta da fare è usare quel modello di progettazione di Visitor , che trasforma l'esempio di giocattolo in un ampio saggio. I vantaggi sono evidenti e non sono a conoscenza di alcun inconveniente.


2
Sono completamente d'accordo, ma ho avuto respingimenti da parte di persone che in tutto il settore tendono ad essere d'accordo: sanno che sono modelli di visitatori, l'hanno visto e usato molte volte, quindi il codice in esso è qualcosa che comprendono e hanno familiarità, il un altro approccio, sebbene ridicolmente più semplice e facile, è estraneo e quindi più difficile per loro. Questo è un fatto spiacevole del settore che ha avuto più di 15 anni di OOP sbattuti nella testa di tutti i programmatori che 100+ righe di codice sono più facili da capire per loro di 10 semplicemente perché hanno memorizzato più di 100 righe dopo averle ripetute per oltre un decennio
Jimmy Hoffa,

1
-1 - Più codice conciso non significa che stai scrivendo un codice "meno". Stai scrivendo lo stesso codice usando meno caratteri. Semmai, fai più errori perché il codice è (spesso) più difficile da leggere.
Telastyn,

8
@Telastyn: Terse non è la stessa di illeggibile. Inoltre, enormi masse di piastre gonfiabili hanno il loro modo di essere illeggibili.
Michael Shaw,

1
@Telastyn Penso che tu abbia appena toccato il vero punto cruciale qui, sì il terse può essere cattivo e illeggibile, anche il gonfiore può essere cattivo e illeggibile, ma la chiave non è la lunghezza variabile e il codice scritto in modo strano. La chiave è come si parla sopra il numero di operazioni, sono d'accordo che il numero di operazioni non è correlato alla manutenibilità, penso a fare meno cose (con il codice scritto chiaramente) fa beneficio leggibilità e la manutenibilità. Ovviamente facendo lo stesso numero di cose con nomi singola lettera di funzioni e variabili non sarà di aiuto, buona FP ha bisogno di molto meno operazioni ancora chiaramente scritte
Jimmy Hoffa

2
@ user949300: se vuoi un esempio completamente diverso, che ne dici di questo esempio Java 8 ?: list.forEach(System.out::println);Da un punto di vista FP, printlnuna funzione accetta due argomenti, una destinazione PrintStreame un valore Objectma il metodo di Collections forEachprevede una funzione con un solo argomento che può essere applicato ad ogni elemento. Quindi il primo argomento è legato all'istanza trovata nel System.outcedere a una nuova funzione con un argomento. È più semplice diBiConsumer<…> c=PrintStream::println; PrintStream a1=System.out; list.forEach(a2 -> c.accept(a1, a2));
Holger il

5

Potrebbe essere necessario limitare quali parti della tua conoscenza usi al lavoro, come Superman deve fingere di essere Clark Kent per godersi i vantaggi di una vita normale. Ma sapere di più non ti farà mai del male. Detto questo, alcuni aspetti della programmazione funzionale sono appropriati per un negozio orientato agli oggetti e altri aspetti possono valere la pena parlarne con il tuo capo in modo da poter aumentare il livello medio di conoscenza del tuo negozio e conseguentemente scrivere un codice migliore.

FP e OOP non si escludono a vicenda. Guarda Scala. Alcuni pensano che sia il peggio perché è FP impuro, ma alcuni pensano che sia il migliore per lo stesso motivo.

Uno ad uno, ecco alcuni aspetti che funzionano alla grande con OOP:

  • Funzioni pure (senza effetti collaterali) - Ogni linguaggio di programmazione che conosco lo supporta. Rendono il tuo codice molto, molto più facile da ragionare e dovrebbero essere usati quando possibile. Non devi chiamarlo FP. Chiamalo solo buone pratiche di codifica.

  • Immutabilità: String è probabilmente l'oggetto Java più comunemente usato ed è immutabile. Copro oggetti immutabili Java e immutabili Java collezioni sul mio blog. Alcuni di questi potrebbero essere applicabili a te.

  • Funzioni di prima classe (puntatori a funzione / Oggetti funzionali / Modello di strategia) - Java ha avuto una versione mutevole e mutante di questa versione dalla 1.1 con la maggior parte delle classi API (e ce ne sono centinaia) che implementano l'interfaccia di Listener. Runnable è probabilmente l'oggetto funzionale più comunemente usato. Le funzioni di prima classe sono più impegnative per codificare in un linguaggio che non le supporta in modo nativo, ma a volte merita lo sforzo extra quando semplificano altri aspetti del codice.

  • La ricorsione è utile per l'elaborazione degli alberi. In un negozio OOP, questo è probabilmente l'uso primario appropriato della ricorsione. L'uso della ricorsione per divertimento in OOP dovrebbe probabilmente essere disapprovato se per nessun motivo diverso dalla maggior parte delle lingue OOP non ha lo spazio di stack di default per renderlo una buona idea.

  • Espressioni (vs. dichiarazioni - ogni riga di codice produce un valore anziché, o oltre a causare effetti collaterali) - L'unico operatore valutativo in C, C ++ e Java è l' operatore ternario . Discuto l'uso appropriato sul mio blog. Potresti scoprire di scrivere alcune semplici funzioni che sono altamente riutilizzabili e valutative.

  • Valutazione pigra (e Monadi) - principalmente limitata all'inizializzazione pigra in OOP. Senza funzionalità linguistiche a supporto, potresti trovare alcune API utili, ma scrivere le tue è difficile. Ottimizza invece l'utilizzo dei flussi - vedi le interfacce di Writer e Reader per esempi.

  • Applicazione parziale e curricula - Non pratico senza funzioni di prima classe.

  • Pattern Matching - generalmente scoraggiato in OOP.

In sintesi, non credo che il lavoro debba essere un servizio gratuito in cui puoi fare qualunque cosa il linguaggio di programmazione supporti al limite che il linguaggio lo supporta. Penso che la leggibilità dei tuoi colleghi dovrebbe essere la tua cartina di tornasole per il codice fatto a noleggio. Dove questo ti infastidisce di più, vorrei iniziare a studiare sul posto di lavoro per ampliare gli orizzonti dei tuoi collaboratori.


Da quando ho imparato FP ho abituato a progettare cose per avere interfacce fluide che si traducono in qualcosa di simile alle espressioni, una funzione che ha un'affermazione che fa un sacco di cose. È il più vicino che otterrai davvero, ma è un approccio che fluisce naturalmente dalla purezza quando scopri che non hai più metodi vuoti, usando i metodi di estensione statica in C # aiuta molto. In questo modo il tuo punto di espressione è l'unico punto su cui non sono d'accordo, tutto il resto è perfetto con le mie esperienze personali imparando FP e facendo un lavoro giornaliero su .NET
Jimmy Hoffa,

Ciò che mi disturba davvero in C # ora è che non posso usare i delegati invece delle interfacce a un metodo per 2 semplici motivi: 1. non puoi creare lambas ricorsivi senza un hack (assegnando prima a null e poi a un lambda) o Y- combinatore (che è brutto come l'inferno in C #). 2. non ci sono alias di tipo che è possibile utilizzare nell'ambito di un progetto, quindi le firme dei delegati diventano piuttosto rapidamente ingestibili. Quindi proprio per questi 2 stupidi motivi non riesco più a divertirmi con C #, perché l'unica cosa che posso far funzionare è usare interfacce a un metodo che sono solo inutili lavori extra.
Trident D'Gao,

@bonomo Java 8 ha un generico java.util.function.BiConsumer che potrebbe essere utile in C #: public interface BiConsumer<T, U> { public void accept(T t, U u); }ci sono altre interfacce funzionali utili in java.util.function.
GlenPeterson,

@bonomo Ehi, capisco, questo è il dolore di Haskell. Ogni volta che leggi qualcuno dice "Imparare FP mi ha reso migliore in OOP" significa che hanno imparato Ruby o qualcosa che non è puro e dichiarativo come Haskell. Haskell chiarisce che OOP è inutilmente di basso livello. Il dolore maggiore che stai riscontrando è che l'inferenza di tipo basata su vincoli è indecidibile quando non sei nel sistema di tipo HM, quindi l'inferenza di tipo basata su cosntraint non è completamente fatta: blogs.msdn.com/b/ericlippert/archive / 2012/03/09 /…
Jimmy Hoffa,

1
Most OOP languages don't have the stack space for it Veramente? Tutto ciò di cui hai bisogno sono 30 livelli di ricorsione per gestire miliardi di nodi in un albero binario bilanciato. Sono abbastanza sicuro che il mio spazio stack sia adatto a molti più livelli di questo.
Robert Harvey,

3

Oltre alla programmazione funzionale e alla programmazione orientata agli oggetti, esiste anche la programmazione dichiarativa (SQL, XQuery). Imparare ogni stile ti aiuta a ottenere nuove intuizioni e imparerai a scegliere lo strumento giusto per il lavoro.

Sì, può essere molto frustrante scrivere codice in una lingua e sapere che se si utilizzava qualcos'altro, si potrebbe essere molto più produttivi per un determinato dominio problematico. Tuttavia, anche se stai usando un linguaggio come Java, è possibile applicare concetti da FP al tuo codice Java, anche se in modo indiretto. Il framework Guava per esempio fa un po 'di questo.


2

Come programmatore penso che non dovresti mai smettere di imparare. Detto questo, è molto interessante che l'apprendimento del PQ sta contaminando le tue abilità OOP. Tendo a pensare di imparare OOP come imparare ad andare in bicicletta; non dimentichi mai come farlo.

Quando ho imparato i dettagli di FP, mi sono ritrovato a pensare in modo più matematico e ho acquisito una migliore prospettiva dei mezzi con cui scrivo il software. Questa è la mia esperienza personale.

Man mano che acquisisci più esperienza, i concetti di programmazione di base saranno molto più difficili da perdere. Quindi ti suggerisco di prenderti comodo con il PF fino a quando i concetti di OOP non saranno totalmente solidificati nella tua mente. FP è un cambio di paradigma definito. In bocca al lupo!


4
Imparare OOP è come imparare a gattonare. Ma una volta che ti alzi costantemente, ricorri a gattonare solo quando sei troppo ubriaco. Ovviamente non puoi dimenticare come farlo, ma normalmente non vorresti. E sarà un'esperienza dolorosa camminare con i cingoli quando sai che puoi correre.
Logica SK

@ SK-logic, mi piace il tuo methaphor
Trident D'Gao

@ SK-Logic: com'è l'apprendimento della programmazione imperativa? Ti trascini sullo stomaco?
Robert Harvey,

@RobertHarvey prova a scavare sottoterra con un cucchiaio arrugginito e un mazzo di schede perforate.
Jimmy Hoffa,

0

Ci sono già molte buone risposte, quindi la mia affronterà un sottoinsieme della tua domanda; vale a dire, prendo in considerazione la premessa della tua domanda, poiché OOP e le funzionalità funzionali non si escludono a vicenda.

Se usi C ++ 11, ci sono molte di queste funzionalità di programmazione funzionale integrate nella libreria linguaggio / standard che si sinergizzano (piuttosto) bene con OOP. Certo, non sono sicuro di quanto TMP verrà ricevuto dal tuo capo o dai tuoi colleghi, ma il punto è che puoi ottenere molte di queste funzionalità in un modo o nell'altro in linguaggi non funzionali / OOP, come C ++.

L'utilizzo di modelli con ricorsione in fase di compilazione si basa sui primi 3 punti,

  • Immutabilità
  • ricorsione
  • Pattern Matching

In quel modello i valori sono immutabili (costanti del tempo di compilazione), ogni iterazione viene eseguita usando la ricorsione e la ramificazione viene eseguita usando (più o meno) la corrispondenza del modello, sotto forma di risoluzione del sovraccarico.

Per quanto riguarda gli altri punti, l'utilizzo std::binde la std::functionfunzione di applicazione parziale ti dà e i puntatori di funzione sono integrati nella lingua. Gli oggetti richiamabili sono oggetti funzionali (nonché un'applicazione con funzione parziale). Nota che per oggetti richiamabili, intendo quelli che ne definiscono operator ().

La valutazione pigra e le funzioni pure sarebbero un po 'più difficili; per funzioni pure, puoi usare funzioni lambda che catturano solo per valore, ma questo non è l'ideale.

Infine, ecco un esempio dell'utilizzo della ricorsione in fase di compilazione con l'applicazione di funzioni parziali. È un esempio un po 'inventato, ma dimostra la maggior parte dei punti sopra. Assocerà ricorsivamente i valori di una data tupla a una data funzione e genererà un oggetto funzione (richiamabile)

#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;
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.