Programmazione funzionale rispetto a OOP con classi


32

Ultimamente mi sono interessato ad alcuni concetti di programmazione funzionale. Ho usato OOP per un po 'di tempo ormai. Vedo come costruirò un'app abbastanza complessa in OOP. Ogni oggetto saprebbe come fare ciò che fa l'oggetto. O qualunque cosa faccia anche la lezione dei genitori. Quindi posso semplicemente dire Person().speak()di far parlare la persona.

Ma come posso fare cose simili nella programmazione funzionale? Vedo come le funzioni sono elementi di prima classe. Ma quella funzione fa solo una cosa specifica. Avrei semplicemente un say()metodo fluttuante e lo chiamerei con un equivalente di Person()argomento in modo da sapere che tipo di cosa sta dicendo qualcosa?

Quindi posso vedere le cose semplici, come farei il paragonabile di OOP e oggetti nella programmazione funzionale, in modo da poter modulare e organizzare la mia base di codice?

Per riferimento, la mia esperienza principale con OOP è Python, PHP e alcuni C #. Le lingue che sto guardando con caratteristiche funzionali sono Scala e Haskell. Anche se mi sto sporgendo verso la Scala.

Esempio di base (Python):

Animal(object):
    def say(self, what):
        print(what)

Dog(Animal):
    def say(self, what):
        super().say('dog barks: {0}'.format(what))

Cat(Animal):
    def say(self, what):
        super().say('cat meows: {0}'.format(what))

dog = Dog()
cat = Cat()
dog.say('ruff')
cat.say('purr')

Scala è progettato come OOP + FP, quindi non devi scegliere
Karthik T

1
Sì, lo so, ma voglio anche saperlo per motivi intellettuali. Non riesco a trovare nulla sull'equivalente dell'oggetto nei linguaggi funzionali. Per quanto riguarda scala, vorrei comunque sapere quando / dove / come dovrei usare funzionale su oop, ma che IMHO è un'altra domanda.
skift

2
"Soprattutto enfatizzato, l'IMO è l'idea che non manteniamo lo stato.": Questa è l'idea sbagliata. Non è vero che FP non usa lo stato, ma FP gestisce lo stato in modo diverso (ad es. Monadi in Haskell o tipi unici in Clean).
Giorgio,


3
possibile duplicato di Programmazione funzionale vs. OOP
Caleb

Risposte:


21

Quello che ti stai veramente chiedendo qui è come fare il polimorfismo nei linguaggi funzionali, cioè come creare funzioni che si comportano diversamente in base ai loro argomenti.

Nota che il primo argomento di una funzione è in genere equivalente all '"oggetto" in OOP, ma nei linguaggi funzionali di solito vuoi separare le funzioni dai dati, quindi l' "oggetto" è probabilmente un valore di dati puro (immutabile).

I linguaggi funzionali in generale offrono varie opzioni per raggiungere il polimorfismo:

  • Qualcosa come i metodi multipli che chiamano una funzione diversa in base all'esame degli argomenti forniti. Questo può essere fatto sul tipo del primo argomento (che è effettivamente uguale al comportamento della maggior parte dei linguaggi OOP), ma potrebbe anche essere fatto su altri attributi degli argomenti.
  • Strutture dati prototipo / simil-oggetto che contengono funzioni di prima classe come membri . Quindi potresti incorporare una funzione "dire" all'interno delle strutture dati del tuo cane e gatto. In effetti hai reso il codice parte dei dati.
  • Pattern matching - dove la logica di pattern matching è integrata nella definizione della funzione e garantisce comportamenti diversi per parametri diversi. Comune in Haskell.
  • Branching / condizioni - equivalenti alle clausole if / else in OOP. Potrebbe non essere altamente estensibile, ma può comunque essere appropriato in molti casi quando si dispone di un insieme limitato di valori possibili (ad esempio, la funzione ha passato un numero o una stringa o null?)

Ad esempio, ecco un'implementazione Clojure del tuo problema usando i metodi multipli:

;; define a multimethod, that dispatched on the ":type" keyword
(defmulti say :type)  

;; define specific methods for each possible value of :type. You can add more later
(defmethod say :cat [animal what] (println (str "Car purrs: " what)))
(defmethod say :dog [animal what] (println (str "Dog barks: " what)))
(defmethod say :default [animal what] (println (str "Unknown noise: " what)))

(say {:type :dog} "ruff")
=> Dog barks: ruff

(say {:type :ape} "ook")
=> Unknown noise: ook

Si noti che questo comportamento non richiede la definizione di classi esplicite: le mappe regolari funzionano correttamente. La funzione dispatch (: in questo caso digitare) potrebbe essere qualsiasi funzione arbitraria degli argomenti.


Non chiaro al 100%, ma abbastanza per vedere dove stai andando. Ho potuto vedere questo come il codice "animale" in un determinato file. Anche la parte sulle ramificazioni / condizioni è buona. Non lo avevo considerato come l'alternativa a if / else.
skift

11

Questa non è una risposta diretta, né è necessariamente accurata al 100% in quanto non sono un esperto di linguaggio funzionale. Ma in entrambi i casi, condividerò con te la mia esperienza ...

Circa un anno fa ero su una barca simile a te. Ho fatto C ++ e C # e tutti i miei progetti erano sempre molto pesanti su OOP. Ho sentito parlare delle lingue FP, ho letto alcune informazioni online, ho sfogliato il libro F # ma non riesco ancora a capire come può un linguaggio FP sostituire OOP o essere utile in generale, poiché la maggior parte degli esempi che ho visto erano troppo semplici.

Per me la "svolta" è arrivata quando ho deciso di imparare il pitone. Ho scaricato Python, poi sono andato alla homepage di Project Euler e ho appena iniziato a fare un problema dopo l'altro. Python non è necessariamente un linguaggio FP e puoi sicuramente creare delle classi in esso, ma rispetto a C ++ / Java / C #, ha molti più costrutti FP, quindi quando ho iniziato a giocarci, ho preso la decisione consapevole di non definire una classe a meno che non sia assolutamente necessario.

Ciò che ho trovato interessante su Python è stato quanto fosse facile e naturale prendere le funzioni e "cucirle" per creare funzioni più complesse e alla fine il tuo problema era ancora risolto chiamando una singola funzione.

Hai sottolineato che durante la codifica devi seguire il principio della responsabilità singola e questo è assolutamente corretto. Ma solo perché la funzione è responsabile di una singola attività, non significa che può fare solo il minimo indispensabile. In FP, hai ancora livelli di astrazione. Quindi le tue funzioni di livello superiore possono ancora fare "una" cosa, ma possono delegare a funzioni di livello inferiore per implementare i dettagli più fini su come tale "una" cosa viene raggiunta.

La chiave con FP è tuttavia che non si hanno effetti collaterali. Finché trattate l'applicazione come una semplice trasformazione dei dati con un set definito di input e set di output, potete scrivere codice FP che realizzerebbe ciò di cui avete bisogno. Ovviamente non tutte le applicazioni si adatteranno perfettamente a questo stampo, ma una volta che inizierai a farlo, rimarrai sorpreso da quante applicazioni si adattano. Ed è qui che penso che Python, F # o Scala brillino perché ti danno costrutti FP ma quando hai bisogno di ricordare il tuo stato e "introdurre effetti collaterali" puoi sempre ricorrere a tecniche OOP vere e provate.

Da allora, ho scritto un sacco di codice Python come programmi di utilità e altri script di supporto per il lavoro interno e alcuni di essi si sono ridimensionati abbastanza lontano, ma ricordando i principi SOLID di base, la maggior parte di quel codice è comunque risultata molto mantenibile e flessibile. Proprio come in OOP la tua interfaccia è una classe e le sposti mentre rifatti e / o aggiungi funzionalità, in FP fai esattamente la stessa cosa con le funzioni.

La scorsa settimana ho iniziato a scrivere codice in Java e da allora, quasi quotidianamente, mi viene ricordato che quando in OOP, devo implementare interfacce dichiarando le classi con metodi che sovrascrivono le funzioni, in alcuni casi potrei ottenere la stessa cosa in Python usando un semplice espressione lambda, ad esempio 20-30 righe di codice che ho scritto per scansionare una directory, sarebbero state 1-2 righe in Python e nessuna classe.

FP stessi sono lingue di livello superiore. In Python (scusate, la mia unica esperienza FP) ho potuto mettere insieme la comprensione dell'elenco all'interno di un'altra comprensione dell'elenco con lambda e altre cose gettate dentro e il tutto sarebbe solo 3-4 righe di codice. In C ++, potrei assolutamente realizzare la stessa cosa, ma poiché C ++ è di livello inferiore, dovrei scrivere molto più codice di 3-4 linee e man mano che il numero di linee aumenta, il mio allenamento SRP si avvia e inizierei pensando a come dividere il codice in pezzi più piccoli (cioè più funzioni). Ma nell'interesse della manutenibilità e nascondendo i dettagli dell'implementazione, metterei tutte quelle funzioni nella stessa classe e le renderei private. E il gioco è fatto ... Ho appena creato una classe mentre in Python avrei scritto "return (.... lambda x: .. ....)"


Sì, non risponde direttamente alla domanda, ma comunque un'ottima risposta. quando scrivo script o pacchetti più piccoli in Python, non uso nemmeno le classi. molte volte solo averlo in un formato pacchetto si adatta perfettamente. soprattutto se non ho bisogno di stato. Concordo anche sul fatto che la comprensione dell'elenco sia dannatamente utile. Da quando ho letto su FP, mi sono reso conto di quanto più potenti possano essere. che mi ha portato a voler saperne di più su FP, rispetto a OOP.
skift

Bella risposta. Parla con tutti quelli che si trovano a bordo piscina, ma non sono sicuri se immergere il dito del piede in acqua
Robben_Ford_Fan_boy

E Ruby ... Una delle sue filosofie progettuali si concentra sui metodi che prendono un blocco di codice come argomento, in genere facoltativo. E data la sintassi pulita, pensare e codificare in questo modo è facile. È difficile pensare e comporre in questo modo in C #. Funzionalmente scritto in C # è prolisso e disorientante, sembra impresso nella lingua. Mi piace il fatto che Ruby abbia aiutato a pensare in modo più semplice dal punto di vista funzionale, per vedere il potenziale nella mia casella di pensiero C # stabile. In ultima analisi, vedo funzionale e OO come complementari; Direi che Ruby sicuramente la pensa così.
radarbob,

8

In Haskell, il più vicino che hai è "classe". Questa classe, sebbene non uguale alla classe in Java e C ++ , funzionerà per quello che vuoi in questo caso.

Nel tuo caso, ecco come apparirà il tuo codice.

classe Animale a dove 
dire :: String -> suono 

Quindi è possibile avere singoli tipi di dati adattando questi metodi.

esempio Animal Dog dove
say s = "bark" ++ s 

EDIT: - Prima di poter specializzare dire per Dog è necessario dire al sistema che Dog è animale.

data Dog = \ - qualcosa qui - \ (Animale derivante)

EDIT: - Per Wilq.
Ora se vuoi usare say in una funzione say foo, dovrai dire a haskell che foo può funzionare solo con Animal.

foo :: (Animal a) => a -> String -> String
foo a str = dire a str 

ora se chiami foo con il cane abbaia, se chiami con cat miagolerà.

main = do 
let d = dog (\ - parametri cstr - \)
    c = cat  
in mostra $ foo d "Hello World"

Ora non puoi avere altre definizioni di funzione da dire. Se say viene chiamato con qualsiasi cosa che non sia animale, causerà un errore di compilazione.


dovrò davvero capire un po 'di più su haskell per comprenderlo appieno, ma penso di averne capito. Sono ancora curioso di sapere come questo si allineerebbe con una base di codice più complessa.
skift

nitpick L' animale dovrebbe essere in maiuscolo
Daniel Gratzer il

1
Come fa la funzione say a sapere che lo chiami su un cane se prende solo una stringa? E "derivare" non è solo per alcune classi integrate?
WilQu,

6

I linguaggi funzionali usano 2 costrutti per ottenere il polimorfismo:

  • Funzioni del primo ordine
  • Generics

La creazione di codice polimorfico con questi è completamente diversa da come OOP utilizza l'ereditarietà e i metodi virtuali. Mentre entrambi potrebbero essere disponibili nella tua lingua OOP preferita (come C #), la maggior parte delle lingue funzionali (come Haskell) aumentano fino a undici. È raro che la funzione sia non generica e la maggior parte delle funzioni ha funzioni come parametri.

È difficile da spiegare in questo modo e richiederà molto tempo per imparare questo nuovo modo. Ma per fare questo, devi dimenticare completamente OOP, perché non è così che funziona nel mondo funzionale.


2
OOP è tutto basato sul polimorfismo. Se pensi che OOP riguardi le funzioni legate ai tuoi dati, allora non sai nulla di OOP.
Euforico,

4
il polimorfismo è solo un aspetto di OOP, e penso che non sia quello che l'OP sta davvero chiedendo.
Doc Brown,

2
Il polimorfismo è un aspetto chiave di OOP. Tutto il resto è lì per supportarlo. OOP senza ereditarietà / metodi virtuali è quasi identico alla programmazione procedurale.
Euforico,

1
@ErikReppen Se "applicare un'interfaccia" non è necessario spesso, allora non stai facendo OOP. Inoltre, Haskell ha anche dei moduli.
Euforico,

1
Non hai sempre bisogno di un'interfaccia. Ma sono molto utili quando ne hai bisogno. E IMO un'altra parte importante di OOP. Per quanto riguarda i moduli in Haskell, penso che questo sia probabilmente il più vicino a OOP per i linguaggi funzionali, per quanto riguarda l'organizzazione del codice. Almeno da quello che ho letto finora. So che sono ancora molto diversi.
skift

0

dipende davvero da ciò che vuoi realizzare.

se hai solo bisogno di un modo per organizzare il comportamento in base a criteri selettivi, puoi usare ad esempio un dizionario (tabella hash) con oggetti funzione. in Python potrebbe essere qualcosa sulla falsariga di:

def bark(what):
    print "barks: {0}".format(what) 

def meow(what):
    print "meows: {0}".format(what)

def climb(how):
    print "climbs: {0}".format(how)

if __name__ == "__main__":
    animals = {'dog': {'say': bark},
               'cat': {'say': meow,
                       'climb': climb}}
    animals['dog']['say']("ruff")
    animals['cat']['say']("purr")
    animals['cat']['climb']("well")

nota comunque che (a) non ci sono 'esempi' di cane o gatto e (b) dovrai tenere traccia del 'tipo' dei tuoi oggetti da solo.

Come per esempio: pets = [['martin','dog','grrrh'], ['martha', 'cat', 'zzzz']]. allora potresti fare una lista di comprensione come[animals[pet[1]]['say'](pet[2]) for pet in pets]


0

Le lingue OO possono essere utilizzate al posto delle lingue di basso livello a volte per interfacciarsi direttamente con una macchina. C ++ Certamente, ma anche per C # ci sono adattatori e simili. Sebbene la scrittura di codice per controllare parti meccaniche e avere un controllo minuto sulla memoria, è meglio conservarla il più vicino possibile al livello più basso. Ma se questa domanda è correlata agli attuali software orientati agli oggetti come Line Of Business, applicazioni Web, IOT, servizi Web e la maggior parte delle applicazioni di massa, allora ...

Rispondi, se applicabile

I lettori potrebbero provare a lavorare con un'architettura orientata ai servizi (SOA). Cioè, DDD, N-Layered, N-Tiered, Hexagonal, qualunque cosa. Non ho visto un'applicazione per grandi aziende utilizzare in modo efficiente OO "tradizionale" (Active-Record o Rich-Models) come è stato descritto negli anni '70 e '80 molto nell'ultimo decennio +. (Vedi nota 1)

La colpa non è dell'OP, ma ci sono un paio di problemi con la domanda.

  1. L'esempio fornito è semplicemente quello di dimostrare il polimorfismo, non è un codice di produzione. A volte esempi esattamente così vengono presi alla lettera.

  2. In FP e SOA, i dati sono separati dalla logica di business. Cioè, dati e logica non vanno insieme. La logica va in Servizi e i Dati (modelli di dominio) non hanno un comportamento polimorfico (Vedi Nota 2).

  3. I servizi e le funzioni possono essere polimorfici. In FP, si passano spesso funzioni come parametri ad altre funzioni anziché a valori. Puoi fare lo stesso in OO Languages ​​con tipi come Callable o Func, ma non funziona in modo dilagante (Vedi Nota 3). In FP e SOA, i tuoi modelli non sono polimorfici, ma solo i tuoi servizi / funzioni. (Vedi nota 4)

  4. C'è un brutto caso di hardcoding in questo esempio. Non sto solo parlando della stringa di colore rosso "dog barks". Sto anche parlando del CatModel e del DogModel stessi. Cosa succede quando vuoi aggiungere una pecora? Devi andare nel tuo codice e creare un nuovo codice? Perché? Nel codice di produzione, preferirei vedere solo un AnimalModel con le sue proprietà. Nel peggiore dei casi, un AnfibianModel e un FowlModel se le loro proprietà e gestione sono così diverse.

Questo è quello che mi aspetterei di vedere in una lingua "OO" corrente:

public class Animal
{
    public int AnimalID { get; set; }
    public int LegCount { get; set; }
    public string Name { get; set; }
    public string WhatISay { get; set; }
}

public class AnimalService : IManageAnimals
{
    private IPersistAnimals _animalRepo;
    public AnimalService(IPersistAnimals animalRepo) { _animalRepo = animalRepo; }

    public List<Animal> GetAnimals() => _animalRepo.GetAnimals();

    public string WhatDoISay(Animal animal)
    {
        if (!string.IsNullOrWhiteSpace(animal.WhatISay))
            return animal.WhatISay;

        return _animalRepo.GetAnimalNoise(animal.AnimalID);
    }
}

Flusso di base

Come si passa dalle classi in OO alla programmazione funzionale? Come altri hanno già detto; Puoi, ma non davvero. Il punto di cui sopra è dimostrare che non dovresti nemmeno usare Classi (nel senso tradizionale del mondo) quando fai Java e C #. Una volta che hai iniziato a scrivere codice in un'architettura orientata ai servizi (DDD, Layered, Tiered, Hexagonal, qualunque cosa), sarai un passo avanti verso Functional perché separi i tuoi dati (modelli di dominio) dalle tue funzioni logiche (servizi).

OO Language un passo avanti verso FP

Potresti anche andare un po 'oltre e suddividere i tuoi servizi SOA in due tipi.

Tipo di classe 1 opzionale : Servizi comuni di implementazione dell'interfaccia per punti di ingresso. Si tratterebbe di punti di accesso "impuri" che possono chiamare altre funzionalità "pure" o "impure". Questi potrebbero essere i tuoi punti di ingresso da un'API RESTful.

Tipo di classe 2 opzionale : Servizi di logica aziendale pura. Queste sono classi statiche con funzionalità "Pura". In FP, "Pure" significa che non ci sono effetti collaterali. Non imposta esplicitamente Stato o Persistenza da nessuna parte. (Vedi nota 5)

Pertanto, quando si pensa alle classi in linguaggi orientati agli oggetti, utilizzati in un'architettura orientata ai servizi, non solo avvantaggia il codice OO, ma inizia a far sembrare la programmazione funzionale molto facile da capire.

Gli appunti

Nota 1 : il design orientato agli oggetti "Rich" o "Active-Record" originale è ancora in circolazione. C'è un sacco di codice legacy come quello quando le persone "facevano bene" un decennio o più fa. L'ultima volta che ho visto quel tipo di codice (fatto correttamente) proveniva da un videogioco Codebase in C ++, dove controllavano esattamente la memoria e avevano uno spazio molto limitato. Per non dire che le architetture FP e Service-Oriented sono bestie e non dovrebbero considerare l'hardware. Ma mettono la possibilità di cambiare costantemente, essere mantenuti, avere dimensioni di dati variabili e altri aspetti come priorità. Nei videogiochi e nell'IA artificiale, controlli segnali e dati in modo molto preciso.

Nota 2 : i modelli di dominio non hanno un comportamento polimorfico, né hanno dipendenze esterne. Sono "isolati". Ciò non significa che debbano essere anemici al 100%. Possono avere molte logiche legate alla loro costruzione e all'alterazione della proprietà mutevole, se applicabile. Vedi DDD "Oggetti valore" ed Entità di Eric Evans e Mark Seemann.

Nota 3 : Linq e Lambda sono molto comuni. Ma quando un utente crea una nuova funzione, usa raramente Func o Callable come parametri, mentre in FP sarebbe strano vedere un'app senza funzioni che seguono quel modello.

Nota 4 : non confondere il polimorfismo con l'ereditarietà. Un CatModel potrebbe ereditare AnimalBase per determinare quali Proprietà ha tipicamente un Animale. Ma come mostro, modelli come questo sono un odore di codice . Se vedi questo schema, potresti considerare di scomporlo e trasformarlo in dati.

Nota 5 : le funzioni pure possono (e devono) accettare funzioni come parametri. La funzione in entrata potrebbe essere impura, ma potrebbe essere pura. A scopo di test, sarebbe sempre puro. Ma in produzione, sebbene sia trattato come puro, potrebbe contenere effetti collaterali. Ciò non cambia il fatto che la funzione pura è pura. Sebbene la funzione del parametro possa essere impura. Non confondere! : D


-2

Potresti fare qualcosa del genere ... php

    function say($whostosay)
    {
        if($whostosay == 'cat')
        {
             return 'purr';
        }elseif($whostosay == 'dog'){
             return 'bark';
        }else{
             //do something with errors....
        }
     }

     function speak($whostosay)
     {
          return $whostosay .'\'s '.say($whostosay);
     }
     echo speak('cat');
     >>>cat's purr
     echo speak('dog');
     >>>dogs's bark

1
Non ho dato alcun voto negativo. Ma la mia ipotesi è che è perché questo approccio non è né funzionale né orientato agli oggetti.
Manoj R,

1
Ma il concetto trasmesso è vicino al pattern matching utilizzato nella programmazione funzionale, ovvero $whostosaydiventa il tipo di oggetto che determina ciò che viene eseguito. Quanto sopra può essere modificato per accettare ulteriormente un altro parametro in $whattosaymodo che un tipo che lo supporta (ad es. 'human') Possa utilizzarlo.
Syockit,
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.