Sto cercando di ottenere una comprensione della programmazione di stile orientata agli oggetti in Haskell, sapendo che le cose saranno un po ‘diverse a causa della mancanza di mutevolezza. Ho giocato con le classi di tipi, ma la mia comprensione di esse è limitata a esse come interfacce. Quindi ho codificato un esempio in C ++, che è il diamante standard con una base pura e un’eredità virtuale. Bat
eredita Flying
e Mammal
, e sia Flying
che Mammal
ereditano Animal
.
#include class Animal { public: virtual std::string transport() const = 0; virtual std::string type() const = 0; std::string describe() const; }; std::string Animal::describe() const { return "I am a " + this->transport() + " " + this->type(); } class Flying : virtual public Animal { public: virtual std::string transport() const; }; std::string Flying::transport() const { return "Flying"; } class Mammal : virtual public Animal { public: virtual std::string type() const; }; std::string Mammal::type() const { return "Mammal"; } class Bat : public Flying, public Mammal {}; int main() { Bat b; std::cout << b.describe() << std::endl; return 0; }
Fondamentalmente sono interessato a come tradurre una struttura di questo tipo in Haskell, in pratica questo mi permetterebbe di avere una lista di Animal
, come se potessi avere una serie di puntatori (intelligenti) in Animal
s in C ++.
Semplicemente non vuoi farlo, non iniziare nemmeno. OO sicuramente ha i suoi meriti, ma “esempi classici” come il tuo C ++ sono quasi sempre strutture pensate per battere il paradigma nel cervello degli studenti universitari, così non inizieranno a lamentarsi di quanto siano stupide le lingue che dovrebbero usare † .
L’idea sembra fondamentalmente modellare “oggetti del mondo reale” da oggetti nel tuo linguaggio di programmazione. Quale può essere un buon approccio per problemi di programmazione effettivi, ma ha senso solo se si può in effetti tracciare un’analogia tra il modo in cui si utilizzerà l’object del mondo reale e come vengono gestiti gli oggetti OO all’interno del programma.
Il che è semplicemente ridicolo per questi esempi di animali. Se mai, i metodi dovrebbero essere roba come “alimentazione”, “latte”, “macellazione” … ma “trasporto” è un termine improprio, direi che per spostare effettivamente l’animale, che sarebbe piuttosto un metodo dell’ambiente in cui vive l’animale, e fondamentalmente ha senso solo come parte di un modello di visitatore.
describe
, type
e ciò che chiamate transport
sono, d’altra parte, molto più semplici. Queste sono fondamentalmente costanti dipendenti dal tipo o semplici funzioni pure. Solo la paranoia OO ratifica rendendoli metodi di class.
Qualsiasi cosa sulla falsariga di questa roba animale, in cui ci sono fondamentalmente solo dati , diventa molto più semplice se non provi a forzarla in qualcosa di simile a OO, ma stai semplicemente con (utilmente digitato) i dati in Haskell.
Quindi, dato che ovviamente questo esempio non ci porta più in là, consideriamo qualcosa in cui l’OOP abbia senso. I toolkit Widget vengono in mente. Qualcosa di simile a
class Widget; class Container : public Widget { std::vector> children; public: // getters ... }; class Paned : public Container { public: Rectangle childBoundaries(int) const; }; class ReEquipable : public Container { public: void pushNewChild(std::unique_ptr&&); void popChild(int); }; class HJuxtaposition: public Paned, public ReEquipable { ... };
Perché OO ha senso qui? Innanzitutto, ci consente facilmente di memorizzare una raccolta eterogenea di widget. Questo in realtà non è facile da ottenere in Haskell, ma prima di provarlo, potresti chiederti se ne hai davvero bisogno. Per alcuni contenitori, forse non è così desiderabile permetterlo, dopotutto. In Haskell, il polimorfismo parametrico è molto piacevole da usare. Per qualsiasi tipo di widget, osserviamo che la funzionalità di Container
praticamente si riduce a una semplice lista. Quindi perché non usare solo un elenco, ovunque sia necessario un Container
?
Naturalmente, in questo esempio, probabilmente troverai che hai bisogno di contenitori eterogenei; il modo più diretto per ottenerli è {-# LANGUAGE ExistentialQuantification #-}
:
data GenericWidget = GenericWidget { forall w . Widget w => getGenericWidget :: w }
In questo caso il Widget
sarebbe una class di tipo (potrebbe essere una traduzione piuttosto letterale della class astratta Widget
). In Haskell questa è piuttosto una cosa da fare, ma potrebbe essere proprio qui.
Paned
è più un’interfaccia. Potremmo usare un’altra class di tipi qui, fondamentalmente la traslitterazione di C ++:
class Paned c where childBoundaries :: c -> Int -> Maybe Rectangle
ReEquipable
è più difficile, perché i suoi metodi effettivamente mutano il contenitore. Questo è ovviamente problematico in Haskell. Ma ancora una volta potresti scoprire che non è necessario: se hai sostituito la class Container
con elenchi semplici, potresti essere in grado di eseguire gli aggiornamenti come aggiornamenti puramente funzionali.
Probabilmente però, questo sarebbe troppo inefficiente per l’attività in corso. Discutere in modo completo i modi per fare aggiornamenti mutabili in modo efficiente sarebbe troppo per lo scopo di questa risposta, ma esistono modi simili, ad esempio l’uso di lenses
.
OO non si traduce troppo bene in Haskell. Non esiste un semplice isomorfismo generico, solo le approssimazioni multiple tra cui scegliere richiede esperienza. Il più spesso ansible, dovresti evitare di affrontare il problema da un angolo OO e pensare invece a dati, funzioni, livelli di monade. Si scopre che questo ti porta molto lontano in Haskell. Solo in alcune applicazioni, OO è così naturale che vale la pena di inserirlo nella lingua.
† Scusa, questo argomento mi spinge sempre in una modalità rant di opinione forte …
‡ Queste paranoie sono in parte motivate dai problemi di mutabilità, che non sorgono in Haskell.
In Haskell non esiste un buon metodo per creare “alberi” di eredità. Invece, di solito facciamo qualcosa di simile
data Animal = Animal ... data Mammal = Mammal Animal ... data Bat = Bat Mammal ...
Quindi incapsuliamo informazioni comuni. Che non è così raro in OOP, “favorire la composizione sull’eredità”. Successivamente creiamo queste interfacce, chiamate classi di tipi
class Named a where name :: a -> String
Poi faremmo le istanze di Named
di Animal
, Mammal
e Bat
che comunque avevano senso per ognuna di esse.
Da quel momento in poi, dovremmo semplicemente scrivere le funzioni nella combinazione appropriata di classi di tipi, non ci interessa davvero che Bat
abbia un Animal
sepolto al suo interno con un nome. Diciamo solo
prettyPrint :: Named a => a -> String prettyPrint a = "I love " ++ name a ++ "!"
e lasciamo che i tipici typeclass si preoccupino di capire come gestire i dati specifici. Questo, diciamo, scriviamo codice più sicuro in molti modi, per esempio
foo :: Top -> Top bar :: Topped a => a -> a
Con foo
, non abbiamo idea di quale sottotipo di Top
venga restituito, dobbiamo fare brutto casting basato su runtime per capirlo. Con la bar
, garantiamo staticamente che ci atteniamo alla nostra interfaccia, ma che l’implementazione sottostante è coerente per tutta la funzione. Ciò rende molto più facile comporre in modo sicuro funzioni che funzionano su interfacce diverse per lo stesso tipo.
TLDR; In Haskell, componiamo i dati trattati in modo più compositivo, quindi facciamo affidamento su un polimorfismo parametrico limitato per garantire un’astrazione sicura tra i tipi di calcestruzzo senza sacrificare le informazioni sul tipo.
Ci sono molti modi per implementarlo con successo in Haskell, ma pochi che “sentiranno” molto come Java. Ecco un esempio: modelleremo ciascun tipo in modo indipendente ma forniremo operazioni “cast” che ci consentiranno di trattare i sottotipi di Animal
come Animal
data Animal = Animal String String String data Flying = Flying String String data Mammal = Mammal String String castMA :: Mammal -> Animal castMA (Mammal transport description) = Animal transport "Mammal" description castFA :: Flying -> Animal castFA (Flying type description) = Animal "Flying" type description
Puoi quindi ovviamente creare una lista di Animal
senza problemi. A volte alle persone piace implementarlo tramite ExistentialTypes
e typeclasss
class IsAnimal a where transport :: a -> String type :: a -> String description :: a -> String instance IsAnimal Animal where transport (Animal tr _ _) = tr type (Animal _ t _) = t description (Animal _ _ d) = d instance IsAnimal Flying where ... instance IsAnimal Mammal where ... data AnyAnimal = forall t. IsAnimal t => AnyAnimal t
che ci consente di iniettare Flying
e Mammal
direttamente in una lista insieme
animals :: [AnyAnimal] animals = [AnyAnimal flyingType, AnyAnimal mammalType]
ma questo in realtà non è molto meglio dell’esempio originale dato che abbiamo gettato via tutte le informazioni su ogni elemento dell’elenco eccetto che ha un’istanza IsAnimal
, che, guardando attentamente, è del tutto equivalente a dire che è solo un Animal
.
projectAnimal :: IsAnimal a => a -> Animal projectAnimal a = Animal (transport a) (type a) (description a)
Quindi potremmo anche aver seguito la prima soluzione.
Molte altre risposte suggeriscono già come le classi di tipi potrebbero essere interessanti per te. Tuttavia, voglio sottolineare che, nella mia esperienza, molte volte quando pensi che una class di tipizzazione sia la soluzione a un problema, in realtà non lo è. Credo che ciò sia particolarmente vero per le persone con un background OOP.
In realtà c’è un articolo del blog molto popolare su questo, Haskell Antipattern: Existential Typeclass , potresti divertirti!
Un approccio più semplice al tuo problema potrebbe essere quello di modellare l’interfaccia come un semplice tipo di dati algebrico, ad es
data Animal = Animal { animalTransport :: String, animalType :: String }
In modo tale che il tuo bat
diventi un semplice valore:
flyingTransport :: String flyingTransport = "Flying" mammalType :: String mammalType = "Mammal" bat :: Animal bat = Animal flyingTransport mammalType
Con questo a portata di mano, puoi definire un programma che descriva qualsiasi animale, proprio come fa il tuo programma:
describe :: Animal -> String describe a = "I am a " ++ animalTransport a ++ " " ++ animalType a main :: IO () main = putStrLn (describe bat)
In questo modo è facile avere una lista di valori Animal
e ad esempio stampare la descrizione di ciascun animale.