2010-06-26 15 views
16

Sono uno sviluppatore C#. Venendo dal mondo OO, comincio a pensare in termini di interfacce, classi e gerarchie di tipi. A causa della mancanza di OO in Haskell, a volte mi ritrovo bloccato e non riesco a pensare a un modo per modellare determinati problemi con Haskell.Come modellare le gerarchie di classi in Haskell?

Come modellare, in Haskell, situazioni del mondo reale che coinvolgono gerarchie di classi, come quella mostrata qui: http://www.braindelay.com/danielbray/endangered-object-oriented-programming/isHierarchy-4.gif

+1

Dipende in gran parte dalle operazioni che si desidera eseguire su di esse. Puoi descrivere come vorresti usare gli oggetti di quelle classi nel tuo programma? – sepp2k

+0

Ho scelto un esempio casuale. Sentiti libero di assumere qualsiasi operazione. Voglio solo avere l'idea generale di come tali situazioni siano gestite in Haskell. – Kibarim

risposta

13

Assumiamo le seguenti operazioni: Gli esseri umani possono parlare, cani possono abbaiare, e tutti i membri di una specie può accoppiarsi con membri della stessa specie se hanno sesso opposto. Definirei questo in Haskell come questo:

data Gender = Male | Female deriving Eq 

class Species s where 
    gender :: s -> Gender 

-- Returns true if s1 and s2 can conceive offspring 
matable :: Species a => a -> a -> Bool 
matable s1 s2 = gender s1 /= gender s2 

data Human = Man | Woman 
data Canine = Dog | Bitch 

instance Species Human where 
    gender Man = Male 
    gender Woman = Female 

instance Species Canine where 
    gender Dog = Male 
    gender Bitch = Female 

bark Dog = "woof" 
bark Bitch = "wow" 

speak Man s = "The man says " ++ s 
speak Woman s = "The woman says " ++ s 

Ora l'operazione matable ha tipo Species s => s -> s -> Bool, bark è di tipo Canine -> String e speak ha digitare Human -> String -> String.

Non so se questo sia d'aiuto, ma vista la natura piuttosto astratta della domanda, questo è il meglio che potrei inventare.

Edit: In risposta al commento di Daniel:

una semplice struttura per le collezioni potrebbe essere la seguente (ignorando le classi già esistenti come pieghevole e Functor):

class Foldable f where 
    fold :: (a -> b -> a) -> a -> f b -> a 

class Foldable m => Collection m where 
    cmap :: (a -> b) -> m a -> m b 
    cfilter :: (a -> Bool) -> m a -> m a 

class Indexable i where 
    atIndex :: i a -> Int -> a 

instance Foldable [] where 
    fold = foldl 

instance Collection [] where 
    cmap = map 
    cfilter = filter 

instance Indexable [] where 
    atIndex = (!!) 

sumOfEvenElements :: (Integral a, Collection c) => c a -> a 
sumOfEvenElements c = fold (+) 0 (cfilter even c) 

Ora sumOfEvenElements prende qualsiasi tipo di collezione di integrali e restituisce la somma di tutti gli elementi pari di quella raccolta.

+0

Cosa succede se la gerarchia delle classi è più di un livello profondo? Gli ADT non si applicano in tal caso, vero? – Kibarim

+0

Penso che la domanda sia più come una gerarchia di classi che usa l'ereditarietà apparirebbe in Haskell. Ad esempio, quale sarebbe l'Iterable <- Collection <- List <- ArrayList? –

+0

Sì, è quello che intendo. Grazie Daniele per aver chiarito la mia domanda. – Kibarim

6

Invece di classi e oggetti, Haskell utilizza i tipi di dati astratti . Queste sono in realtà due viste compatibili sul problema dell'organizzazione dei modi di che costruiscono e osservando le informazioni. Il miglior aiuto che io conosca su questo argomento è il saggio di William Cook Object-Oriented Programming Versus Abstract Data Types. Ha alcune molto chiare spiegazioni secondo le quali

  • In un sistema basato su classi, il codice è organizzato intorno modi diversi di costruire astrazioni. Generalmente ogni modo diverso di costruire un'astrazione viene assegnato alla propria classe. I metodi sanno solo osservare le proprietà di quella costruzione.

  • In un sistema basato su ADT (come Haskell), il codice è organizzato in diversi modi di osservando le astrazioni. Generalmente a ogni modo diverso di osservare un'astrazione viene assegnata una propria funzione. La funzione conosce all i modi in cui l'astrazione potrebbe essere costruita e sa come osservare una singola proprietà, ma di qualsiasi costruzione.

carta di Cook vi mostrerà una bella disposizione matrice delle astrazioni e ti insegnano come organizzare qualsiasi classe come un LSA o viceversa.

Classe gerarchie coinvolgono un altro elemento: il riutilizzo delle implementazioni tramite l'ereditarietà.In Haskell, tale riutilizzo è ottenuto attraverso funzioni di prima classe: una funzione in un'astrazione Primate è un valore e un'implementazione dell'astrazione Human può riutilizzare qualsiasi funzione dell'astrazione Primate, può avvolgerli per modificare i risultati e così via .

Non esiste un adattamento esatto tra progettazione con gerarchie di classi e progettazione con tipi di dati astratti. Se provi a traslitterare da uno all'altro, ti ritroverai con qualcosa di imbarazzante e non idiomatico allo — come un programma FORTRAN scritto in Java. Ma se si comprendono i principi delle gerarchie di classi e i principi dei tipi di dati astratti, è possibile adottare una soluzione a un problema in uno stile e creare una soluzione ragionevolmente idiomatica allo stesso problema nell'altro stile. Ci vuole pratica.


Addendum: E 'anche possibile utilizzare il sistema di tipo di classe di Haskell per cercare di emulare gerarchie di classi, ma questo è un altro paio di maniche. Le classi di tipi sono abbastanza simili alle classi ordinarie che funzionano un certo numero di esempi standard, ma sono abbastanza diversi da dare luogo a grandi sorprese e disadattati. Mentre le classi di tipi sono uno strumento inestimabile per un programmatore Haskell, raccomanderei a chiunque apprenda ad Haskell di imparare a progettare programmi usando tipi di dati astratti.

+0

Non capisco come sia possibile utilizzare le funzioni di prima classe in sostituzione dell'ereditarietà. Per favore, può dare un esempio? – Kibarim

31

Prima di tutto: il design OO standard non funzionerà correttamente in Haskell. Puoi combattere la lingua e provare a fare qualcosa di simile, ma sarà un esercizio di frustrazione. Quindi il primo passo è cercare soluzioni in stile Haskell al tuo problema invece di cercare modi per scrivere una soluzione in stile OOP in Haskell.

Ma è più facile a dirsi che a farsi! Dove iniziare anche?

Quindi, smontiamo i dettagli grintosi di ciò che OOP fa per noi e pensiamo a come potrebbero apparire in Haskell.

  • Oggetti: Approssimativamente parlando, un oggetto è la combinazione di alcuni dati con metodi che operano su tali dati. In Haskell, i dati sono normalmente strutturati utilizzando i tipi di dati algebrici ; i metodi possono essere considerati come funzioni che assumono i dati dell'oggetto come argomento iniziale implicito.
  • Incapsulamento: Tuttavia, la capacità di ispezionare i dati di un oggetto è in genere limitata ai propri metodi. In Haskell, ci sono vari modi per nascondere una parte di dati, due esempi sono:
    • definire il tipo di dati in un modulo separato che non esporta costruttori del tipo. Solo le funzioni in quel modulo possono ispezionare o creare valori di quel tipo. Questo è in qualche modo paragonabile ai membri protected o internal.
    • Utilizzare l'applicazione parziale. Considerare la funzione map con i suoi argomenti capovolti. Se lo si applica a un elenco di Int s, si otterrà una funzione di tipo (Int -> b) -> [b]. La lista che hai dato è ancora "lì", in un certo senso, ma nient'altro può usarlo se non attraverso la funzione. Questo è paragonabile ai membri private e la funzione originale parzialmente applicata è paragonabile a un costruttore in stile OOP.
  • "Ad-hoc" polimorfismo: Spesso, nella programmazione OO si cura solo che qualcosa implementa un metodo; quando lo chiamiamo, il metodo specifico chiamato viene determinato in base al tipo effettivo. Haskell fornisce le classi di tipo per sovraccaricare le funzioni in fase di compilazione, che sono in molti modi più flessibili rispetto a quelle che si trovano nelle lingue OOP.
  • Riutilizzo codice: Onestamente, la mia opinione è che il riutilizzo del codice via ereditarietà sia stato ed è un errore. I missaggi come in qualcosa di simile a Ruby mi sembrano una soluzione OO migliore. In ogni caso, in qualsiasi linguaggio funzionale, l'approccio standard consiste nel trarre in considerazione un comportamento comune utilizzando le funzioni di ordine superiore, quindi specializzarsi nel modulo generale. Un esempio classico qui sono le funzioni fold, che generalizzano quasi tutti i cicli iterativi, le trasformazioni elenco e le funzioni linearmente ricorsive.
  • Interfacce: A seconda di come si sta utilizzando un'interfaccia, ci sono diverse opzioni:
    • disaccoppiare implementazione: funzioni polimorfi con i vincoli di classe tipo sono ciò che si vuole qui. Ad esempio, la funzione sort ha tipo (Ord a) => [a] -> [a]; è completamente disaccoppiato dai dettagli del tipo che gli viene dato oltre che deve essere un elenco di qualche tipo che implementa Ord.
    • Lavorare con più tipi con un'interfaccia condivisa: Per questo è necessario sia un'estensione del linguaggio per tipi esistenziali, o di mantenerlo semplice, utilizzare qualche variazione sul applicazione parziale come sopra - al posto dei valori e funzioni che puoi applicare a loro, applicare le funzioni in anticipo e lavorare con i risultati.
  • Sottotipizzazione, anche noto come il "è-un" rapporto: Questo è dove si è per lo più fuori di fortuna. Ma - parlando per esperienza, essendo stato uno sviluppatore C# professionale per anni - i casi in cui davvero hanno bisogno di sottotitolare non sono molto comuni. Invece, pensa a quanto sopra, e quale comportamento stai cercando di acquisire con la relazione di sottotitoli.

Potresti anche trovare utile this blog post; fornisce un rapido riepilogo di ciò che usereste in Haskell per risolvere gli stessi problemi in cui spesso vengono utilizzati alcuni Pattern Design standard in OOP.

Come addendum finale, come programmatore C#, potrebbe essere interessante ricercare le connessioni tra esso e Haskell. Un bel po 'di persone responsabili di C# sono anche programmatori di Haskell, e alcune aggiunte recenti a C# sono state pesantemente influenzate da Haskell. Il più notevole è probabilmente la struttura monadica sottostante LINQ, con IEnumerable che è essenzialmente la lista monad.

+0

Wow, questa è un'analisi approfondita. Abbiamo la stessa opinione sull'ereditarietà, combinando interfaccia e comportamento è una scelta terribile ... –

+0

Ottima risposta! Penso che le classi di tipo siano * parametriche * polimorfismo, btw. –

+0

@Jonathan Sterling: In realtà no: il polimorfismo parametrico significa astrazione su tipi opachi senza alcuna conoscenza di ciò che sono - la garanzia corrispondente è che la funzione si comporterà in modo identico per tutti i parametri di tipo. Questo è solo normale tipo di variabili in Haskell, o "generici" in varie lingue. Il polimorfismo ad-hoc significa scegliere un comportamento diverso per tipi diversi, che utilizza classi di tipi in Haskell o funzioni sovraccaricate in vari altri linguaggi. –

2

Haskell è la mia lingua preferita, è un linguaggio puramente funzionale. Non ha effetti collaterali, non ci sono incarichi. Se trovi difficile il passaggio a questa lingua, forse F # è un posto migliore per iniziare con la programmazione funzionale. F # non è puro.

Gli stati di incapsulamento degli oggetti, c'è un modo per ottenere questo in Haskell, ma questo è uno dei problemi che richiede più tempo per imparare perché è necessario imparare alcuni concetti di teoria delle categorie per capire profondamente le monadi.C'è zucchero sintattico che ti permette di vedere le monadi come compiti non distruttivi, ma a mio avviso è meglio dedicare più tempo a comprendere le basi della teoria delle categorie (la nozione di categoria) per ottenere una migliore comprensione.

Prima di provare a programmare in stile OO in Haskell, dovresti chiederti se usi veramente lo stile orientato agli oggetti in C#, molti programmatori usano le lingue OO, ma i loro programmi sono scritti nello stile strutturato.

La dichiarazione dei dati consente di definire strutture di dati che combinano prodotti (equivalenti a struttura in linguaggio C) e unioni (equivalenti a unione in C), la parte derivante della dichiarazione consente di ereditare metodi predefiniti.

Un tipo di dati (struttura dati) appartiene a una classe se ha un'implementazione dell'insieme di metodi nella classe. Ad esempio, se è possibile definire un metodo show :: a -> String per il proprio tipo di dati, quindi appartiene alla classe Show, è possibile definire il tipo di dati come un'istanza della classe Show.

Questo è diverso dall'uso della classe in alcune lingue OO in cui viene utilizzato come metodo per definire strutture + metodi.

Un tipo di dati è astratto se è indipendente dalla sua implementazione. Crea, muta e distruggi l'oggetto con un'interfaccia astratta, non hai bisogno di sapere come è implementato.

L'astrazione è supportata in Haskell, è molto semplice da dichiarare. Per esempio questo codice dal sito Haskell:

data Tree a = Nil 
      | Node { left :: Tree a, 
        value :: a, 
        right :: Tree a } 

dichiara i selettori di sinistra, il valore, a destra. i costruttori possono essere definiti come segue, se si desidera aggiungerli alla lista di esportazione nella dichiarazione modulo:

node = Node 
nil = Nil 

I moduli sono costruiti in modo simile come in Modula. Ecco un altro esempio dello stesso sito:

module Stack (Stack, empty, isEmpty, push, top, pop) where 

empty :: Stack a 
isEmpty :: Stack a -> Bool 
push :: a -> Stack a -> Stack a 
top :: Stack a -> a 
pop :: Stack a -> (a,Stack a) 

newtype Stack a = StackImpl [a] -- opaque! 
empty = StackImpl [] 
isEmpty (StackImpl s) = null s 
push x (StackImpl s) = StackImpl (x:s) 
top (StackImpl s) = head s 
pop (StackImpl (s:ss)) = (s,StackImpl ss) 

C'è ancora qualcosa da dire su questo argomento, spero che questo commento aiuti!

Problemi correlati