Uso corretto dei typeclass

5

Sto provando il modulo Gloss di Haskell e ho trovato un modello di cose necessarie per visualizzare correttamente un oggetto: posizione, dimensioni, scala e rappresentazione dell'immagine. Questo mi è sembrato un buon uso delle classificazioni, quindi ho scritto questo:

class Displayable d where

    toPicture :: d -> Picture

    getDims     :: d -> (Float,Float)
    getPos      :: d -> (Float,Float)
    getScale    :: d -> Float

    setDims     :: (Float,Float) -> d -> d
    setPos      :: (Float,Float) -> d -> d
    setScale    :: Float -> d -> d

    setDims _ d     = d
    setPos _ d      = d
    setScale _ d    = d

    getDims _   = (0,0)
    getPos _    = (0,0)
    getScale _  = 0

Non ho mai usato tipografie doganali prima però, quindi ho alcune domande sul loro utilizzo:

  1. Prima di tutto, questa situazione è appropriata per un typeclass?

  2. Ho notato che ho bisogno di molti metodi per farlo funzionare (probabilmente aggiungerò anche get/setRotation , quindi crescerà). È tipico? È un segnale che sto cercando di includere troppo?

  3. Devo definire definizioni predefinite come ho? Per alcuni scenari, potrei non aver bisogno di definire una caratteristica particolare, ma vorrei che l'oggetto approfittasse del resto della classe (come la necessità di ruotare un mondo o ottenere le dimensioni di una "entità non fisica"). Tuttavia, mi rendo conto che se le definizioni predefinite non vengono prese in considerazione, potrebbe portare a risultati strani (come gli oggetti che hanno una scala 0 di default, che potrebbe renderli invisibili).

  4. I getter / setter sono il modo migliore per ottenere ciò che sto cercando di fare (un'interfaccia centrale per manipolare oggetti visualizzabili)?

(So che le rappresentazioni di dati non sono pensate come oggetti in Haskell, ma dato che sto cercando di rappresentare qualcosa di visivo, ho pensato che il termine oggetto fosse appropriato)

Ogni pensiero sarebbe apprezzato.

    
posta Carcigenicate 05.11.2014 - 02:52
fonte

2 risposte

4

Solitamente in questo tipo di situazione, devi semplicemente creare un semplice tipo di dati o forse un record per aggregare i diversi campi:

data Displayable = Displayable Picture (Float,Float) (Float,Float) Float

Le classi di tipo sono per quando hai bisogno di invio polimorfico a molte implementazioni della stessa funzione. Questo non è certamente il caso per scala e posizione, per cui avrai solo un'implementazione. Le dimensioni possono essere calcolate in modi diversi, ad esempio in base alle dimensioni naturali di una bitmap, ma tali differenze possono quasi certamente essere prese in considerazione al momento della costruzione.

Potresti avere un caso per rendere toPicture parte di una classe di tipi, ma questo probabilmente può essere gestito principalmente anche in fase di costruzione, quindi avere una funzione che aggiunge le trasformazioni appropriate su una base Picture solo prima di renderlo.

Un'altra possibile struttura dati potrebbe essere un Picture e un elenco di funzioni di trasformazione da applicare ad esso:

data Displayable = Displayable Picture [Picture -> Picture]

Ciò semplifica l'aggiunta di nuovi tipi di trasformazioni, come rotazioni o persino combinazioni complesse di trasformazioni, ma rende più difficile eseguire operazioni come ripristinare la scala sul valore predefinito. Forse potresti aggiungere una descrizione di String per ogni trasformazione. Qual è il migliore dipenderà dalla tua applicazione. Il mio punto di includere questo esempio è mostrare che l'utilizzo di campi funzione può spesso impedirti di ricorrere a una classe di caratteri.

Non è che le classi tipo non siano utili. Lontano da esso. È solo che le situazioni in cui la creazione di una nuova è la soluzione migliore sono relativamente rare. Quando hai bisogno di crearli, in genere sono molto piccoli e quasi mai cambiano. Se stai pensando che potresti dover aggiungere una funzione a una classe di caratteri in un secondo momento, dovresti farti pensare due volte a usarne una.

Va bene avere un sacco di funzioni per manipolare il tipo di dati in modi più facili da programmare, ma si vuole che siano al di fuori delle classi di tipi, se possibile. Ogni volta che aggiungi una funzione a una classe di caratteri, devi aggiungerla a ogni singola istanza.

    
risposta data 05.11.2014 - 05:18
fonte
3

Di solito è una buona idea evitare classi di tipi a meno che non fornisca un beneficio specifico che non può essere ottenuto con altri mezzi. Questo articolo fornisce una visione un po 'estrema su questo, quindi prendilo con una grana di sale, ma può aiutarti a capire perché le classi di tipi possono essere problematiche.

Le classi di tipi di problemi principali operano su livello di tipo , il che significa che provare a fare cose complicate con le classi di tipi richiede in genere una pletora di estensioni Haskell e talvolta persino impossibile. al contrario, operare su livello di livello è molto più semplice e non è necessario combattere con il correttore di tipi e digitare regolarmente il processo di inferenza.

I getter e i setter hanno il loro posto in Haskell, ma sono principalmente per mantenere la compatibilità dell'interfaccia e usarli eccessivamente causerà un sacco di errori. Per semplici strutture di dati interne, potrebbero non valerne la pena.

Nel tuo esempio, sarebbe molto più semplice implementare Displayable come un tipo di dati ordinario (come mostrato nell'esempio di Karl Bielefeldt), con l'ulteriore vantaggio di poter memorizzare tutto Displayables in un contenitore senza estensioni funky come ExistentialTypes . Inoltre, puoi utilizzare la corrispondenza dei pattern (o la sintassi del record) per decostruire il tuo Displayables .

Sulla questione delle definizioni predefinite, dovrebbero essere usate con cautela. Inoltre, è probabilmente una cattiva idea scrivere le impostazioni predefinite che non funzionano su ogni istanza . Non è una linea guida rigorosa, ma aiuta davvero a prevenire errori stupidi come dimenticare di scrivere una funzione per un'istanza: senza valori predefiniti, se si dimentica qualcosa, il compilatore ti avviserà o, nel peggiore dei casi, causerà un errore di runtime; non è così se è impostato come predefinito, e se il comportamento predefinito è sbagliato , il tuo programma potrebbe finire in modo subdolo.

Addendum: un "record of methods" (o "instance dictionary" come viene spesso chiamato) è in realtà ciò che Haskell usa per implementare classi di tipi sotto il tappeto. Tale approccio sarebbe qualcosa di simile:

data Displayable d = Displayable
  { display   :: d
  , toPicture :: d -> Picture
  , getDims   :: d -> (Float, Float)
  , getPos    :: d -> (Float, Float)
  , getScale  :: d -> Float
  , setDims   :: (Float,Float) -> d -> d
  , setPos    :: (Float,Float) -> d -> d
  , setScale  :: Float -> d -> d
  }

Qualsiasi classe di tipo può essere trasformata in questo modulo: questa una traduzione letterale della classe tipo nella tua domanda. Probabilmente però non vuoi usare in questo esatto modulo: è meglio se riesci a capire quali parti di Displayable devono essere veramente astratte e quali parti puoi rendere concrete . Troppa flessibilità può rendere il codice più complesso di quanto non sia necessario.

    
risposta data 05.11.2014 - 05:21
fonte

Leggi altre domande sui tag