2013-09-28 16 views
7

Il sotto sembra funzionare ... ma sembra maldestro.Antipasto Esistenziale, come evitare

data Point = Point Int Int 
data Box = Box Int Int 
data Path = Path [Point] 
data Text = Text 

data Color = Color Int Int Int 
    data WinPaintContext = WinPaintContext Graphics.Win32.HDC 

class CanvasClass vc paint where 
    drawLine :: vc -> paint -> Point -> Point -> IO() 
    drawRect :: vc -> paint -> Box -> IO() 
    drawPath :: vc -> paint -> Path -> IO() 

class (CanvasClass vc paint) => TextBasicClass vc paint where 
    basicDrawText :: vc -> paint -> Point -> String -> IO() 

instance CanvasClass WinPaintContext WinPaint where 
    drawLine = undefined 
    drawRect = undefined 
    drawPath = undefined 

instance TextBasicClass WinPaintContext WinPaint where 
    basicDrawText (WinPaintContext a) = winBasicDrawText a 

op :: CanvasClass vc paint => vc -> Box -> IO() 
op canvas _ = do 
    basicDrawText canvas WinPaint (Point 30 30) "Hi" 

open :: IO() 
open = do 
    makeWindow (Box 300 300) op 

winBasicDrawText :: Graphics.Win32.HDC -> WinPaint -> Point -> String -> IO() 
winBasicDrawText hdc _ (Point x y) str = do 
    Graphics.Win32.setBkMode hdc Graphics.Win32.tRANSPARENT 
    Graphics.Win32.setTextColor hdc (Graphics.Win32.rgb 255 255 0) 
    Graphics.Win32.textOut hdc 20 20 str 
    return() 

windowsOnPaint :: (WinPaintContext -> Box -> IO()) -> 
        Graphics.Win32.RECT -> 
        Graphics.Win32.HDC -> 
        IO() 
windowsOnPaint f rect hdc = f (WinPaintContext hdc) (Box 30 30) 

makeWindow :: Box -> (WinPaintContext -> Box -> IO()) -> IO() 
makeWindow (Box w h) onPaint = 
    Graphics.Win32.allocaPAINTSTRUCT $ \ lpps -> do 
    hwnd <- createWindow w h (wndProc lpps (windowsOnPaint onPaint)) 
    messagePump hwnd 

Ora, quello che sembra essere il modo preferito è quello di avere semplicemente

data Canvas = Canvas { 
    drawLine :: Point -> Point -> IO(), 
    drawRect :: Box -> IO(), 
    drawPath :: Path -> IO() 
} 

hdc2Canvas :: Graphics.Win32.HDC -> Paint -> IO (Canvas) 
hdc2Canvas hdc paint = Canvas { drawLine = winDrawLine hdc paint ... } 

TUTTAVIA ...

Ci piace mantenere le vernici in giro e li mutare durante il processo di disegno, poiché sono costosi da creare e distruggere. Una vernice potrebbe essere solo una lista come [bgColor red, fgColor blue, font "Tahoma"] o qualcosa del genere, oppure potrebbe essere un puntatore a una struttura interna utilizzata dal sistema di pittura (questa è un'astrazione su GDI di Windows, ma alla fine sarà astratta over direct2d e coregraphics), che hanno "vernice" oggetti che non voglio ricreare più e più volte e poi legare.

La bellezza degli esistenziali nella mia mente è che possono avvolgere opacamente qualcosa di astratto su di esso, e possiamo salvarlo da qualche parte, tirarlo indietro, qualunque cosa. Quando applichi parzialmente, penso che ci sia il problema che la cosa che hai parzialmente applicato è ora "bloccata all'interno" del contenitore. Ecco un esempio. Dire che ho un oggetto di vernice come

data Paint = Paint { 
    setFg :: Color -> IO() , 
    setBg :: Color -> IO() 
} 

Dove posso posizionare il puntatore? Quando conferisco a Paint una funzione in Canvas, come ottiene il puntatore? Qual è il modo giusto per progettare questa API?

+0

Che cos'è 'WinPaint'? Hai un main che funziona e fa qualcosa, quindi possiamo vedere cosa dovrebbe fare tutto questo? Dov'è il tipo esistenzialmente quantificato che stai cercando di eliminare? – Cirdec

+0

WinPaint è semplicemente qualcosa che racchiude un puntatore a un tipo di contesto di disegno specifico della piattaforma che contiene il foreground, lo sfondo, il font, ecc. Non li ho quantificati esplicitamente in modo esistenziale, ma avrei quantificato esistenzialmente CanvasClass in Canvas. "Open" qui è main, e dovrebbe semplicemente aprire una finestra con un po 'di testo. Questo codice non funziona, ma spero che riesca a trasmettere le mie intenzioni. – Evan

+0

"Qual è il modo giusto per progettare questa API?" sembra più appropriato per programmers.stackexchange.com. Avrai risposte abbastanza varie a questa domanda. Il mio, ad esempio, inizierebbe con "Sbarazzarsi di IO()" o "Guarda come fa la lucentezza", nessuno dei due ha nulla a che fare con la quantificazione esistenziale. – Cirdec

risposta

9

L'interfaccia

In primo luogo, è necessario chiedere "Quali sono le mie esigenze?". Facciamo Stato in parole povere ciò che vogliamo una tela da fare (queste sono le mie supposizioni basate sulla tua domanda):

  • Alcune tele possono avere forme mettere su di loro
  • Alcune tele possono avere testo mettere su di loro
  • alcune tele cambiare quello che fanno basano su una vernice
  • non sappiamo quali vernici sono ancora, ma saranno diversi per le diverse tele

Ora traduciamo queste idee in Haskell. Haskell è un linguaggio "type-first", quindi quando parliamo di requisiti e design, probabilmente stiamo parlando di tipi.

  • In Haskell, quando vediamo la parola "alcuni" mentre parliamo di tipi, pensiamo alle classi di tipi. Ad esempio, la classe show dice "alcuni tipi possono essere rappresentati come stringhe".
  • Quando parliamo di qualcosa di cui ancora non sappiamo, mentre parliamo di requisiti, è un tipo in cui non sappiamo ancora cosa sia. Questa è una variabile di tipo.
  • "mettere su di loro" sembra significare che dovremmo prendere una tela, mettere qualcosa sopra e avere di nuovo una tela.

Ora potremmo scrivere classi per ognuno di questi requisiti:

class ShapeCanvas c where -- c is the type of the Canvas 
    draw :: Shape -> c -> c 

class TextCanvas c where 
    write :: Text -> c -> c 

class PaintCanvas p c where -- p is the type of Paint 
    load :: p -> c -> c 

Il tipo di variabile c viene utilizzato solo una volta, appare come c -> c.Ciò suggerisce che potremmo renderli più generali sostituendo c -> c con c.

class ShapeCanvas c where -- c is the type of the canvas 
    draw :: Shape -> c 

class TextCanvas c where 
    write :: Text -> c 

class PaintCanvas p c where -- p is the type of paint 
    load :: p -> c 

Ora PaintCanvas sembra un class che è problematico in Haskell. E 'difficile per il sistema di tipi per capire cosa sta succedendo nelle classi come

class Implicitly a b where 
    convert :: b -> a 

avrei alleviare questo modificando PaintCanvas per sfruttare l'estensione TypeFamilies.

class PaintCanvas c where 
    type Paint c :: * -- (Paint c) is the type of Paint for canvases of type c 
    load :: (Paint c) -> c 

Ora, mettiamo insieme tutto per la nostra interfaccia, inclusi i tipi di dati per le forme e il testo (modificato per dare un senso a me):

{-# LANGUAGE TypeFamilies #-} 

module Data.Canvas (
    Point(..), 
    Shape(..), 
    Text(..), 
    ShapeCanvas(..), 
    TextCanvas(..), 
    PaintCanvas(..) 
) where 

data Point = Point Int Int 

data Shape = Dot Point 
      | Box Point Point 
      | Path [Point] 

data Text = Text Point String 

class ShapeCanvas c where -- c is the type of the Canvas 
    draw :: Shape -> c 

class TextCanvas c where 
    write :: Text -> c 

class PaintCanvas c where 
    type Paint c :: * -- (Paint c) is the type of Paint for canvases of type c 
    load :: (Paint c) -> c 

Alcuni esempi

Questo sezione introdurrà un ulteriore requisito per le tele utili oltre a quelle che abbiamo già elaborato. È l'analogo di ciò che abbiamo perso quando abbiamo sostituito c -> c con c nelle classi canvas.

Iniziamo con il primo codice di esempio, op. Con la nostra nuova interfaccia è semplice:

op :: (TextCanvas c) => c 
op = write $ Text (Point 30 30) "Hi" 

Facciamo un esempio un po 'più complicato. Che ne dici di qualcosa che disegna una "X"? Siamo in grado di fare la prima corsa del "X"

ex :: (ShapeCanvas c) => c 
ex = draw $ Path [Point 10 10, Point 20 20] 

Ma noi non hanno modo di aggiungere un altro Path per la corsa croce. Abbiamo bisogno di un modo per mettere insieme due fasi di disegno. Qualcosa con il tipo c -> c -> c sarebbe perfetto. La più semplice classe Haskell che io possa pensare è che questo è mappend :: a -> a -> aMonoid a. A Monoid richiede un'identità e un'associatività. È ragionevole ritenere che ci sia un'operazione di disegno su tele che le lascia intatte? Sembra abbastanza ragionevole. È ragionevole assumere che tre operazioni di disegno, eseguite nello stesso ordine, facciano la stessa cosa anche se le prime due sono eseguite insieme, e poi la terza, o se la prima è eseguita, e poi la seconda e la terza sono eseguite insieme ? Di nuovo, mi sembra abbastanza ragionevole. Ciò suggerisce possiamo scrivere ex come:

ex :: (Monoid c, ShapeCanvas c) => c 
ex = (draw $ Path [Point 10 10, Point 20 20]) `mappend` (draw $ Path [Point 10 20, Point 20 10]) 

Infine, consideriamo qualcosa di interattivo, che decide cosa disegnare basa su qualcosa di esterno:

randomDrawing :: (MonadIO m, ShapeCanvas (m()), TextCanvas (m())) => m() 
randomDrawing = do 
    index <- liftIO . getStdRandom $ randomR (0,2) 
    choices !! index   
    where choices = [op, ex, return()] 

Questo non funziona del tutto, perché noi don' t avere un'istanza per (Monad m) => Monoid (m()) in modo che ex funzioni. Potremmo usare Data.Semigroup.Monad dal pacchetto di riduttori o aggiungerne uno noi stessi, ma questo ci mette in zone incoerenti. Sarebbe più facile cambiare ex per:

ex :: (Monad m, ShapeCanvas (m())) => m() 
ex = do 
    draw $ Path [Point 10 10, Point 20 20] 
    draw $ Path [Point 10 20, Point 20 10] 

Ma il sistema di tipo non riesco a capire che l'unità dal primo draw è lo stesso che l'unità dal secondo.La nostra difficoltà qui suggerisce requisiti aggiuntivi, che non riuscivamo a mettere il dito su un primo momento:

  • Tele estendono sequenze di operazioni esistenti, fornendo le operazioni per il disegno, la scrittura del testo, ecc

Rubare direttamente da http://www.haskellforall.com/2013/06/from-zero-to-cooperative-threads-in-33.html:

  • Quando si sente "sequenza di istruzioni" si dovrebbe pensare: "monade".
  • Quando si qualifica con "estendere" si dovrebbe pensare: "trasformatore monade".

Ora ci rendiamo conto che la nostra implementazione su tela è molto probabilmente un trasformatore monade. Possiamo tornare alla nostra interfaccia e cambiarla in modo che ciascuna delle classi sia una classe per una monade, simile alla classe MonadIO dei trasformatori e alle classi monad di mtl.

L'interfaccia, rivisitato

{-# LANGUAGE TypeFamilies #-} 

module Data.Canvas (
    Point(..), 
    Shape(..), 
    Text(..), 
    ShapeCanvas(..), 
    TextCanvas(..), 
    PaintCanvas(..) 
) where 

data Point = Point Int Int 

data Shape = Dot Point 
      | Box Point Point 
      | Path [Point] 

data Text = Text Point String 

class Monad m => ShapeCanvas m where -- c is the type of the Canvas 
    draw :: Shape -> m() 

class Monad m => TextCanvas m where 
    write :: Text -> m() 

class Monad m => PaintCanvas m where 
    type Paint m :: * -- (Paint c) is the type of Paint for canvases of type c 
    load :: (Paint m) -> m() 

Esempi, rivisitato

Ora tutto del nostro esempio disegnando operazioni sono azioni di alcuni sconosciuti Monad m:

op :: (TextCanvas m) => m() 
op = write $ Text (Point 30 30) "Hi" 

ex :: (ShapeCanvas m) => m() 
ex = do 
    draw $ Path [Point 10 10, Point 20 20] 
    draw $ Path [Point 10 20, Point 20 10] 


randomDrawing :: (MonadIO m, ShapeCanvas m, TextCanvas m) => m() 
randomDrawing = do 
    index <- liftIO . getStdRandom $ randomR (0,2) 
    choices !! index   
    where choices = [op, ex, return()] 

Possiamo fare anche un esempio con la vernice. Dal momento che non sappiamo che cosa dipinge esisterà, tutti hanno da prevedere esternamente (come argomenti l'esempio):

checkerBoard :: (ShapeCanvas m, PaintCanvas m) => Paint m -> Paint m -> m() 
checkerBoard red black = 
    do 
     load red 
     draw $ Box (Point 10 10) (Point 20 20) 
     draw $ Box (Point 20 20) (Point 30 30) 
     load black 
     draw $ Box (Point 10 20) (Point 20 30) 
     draw $ Box (Point 20 10) (Point 30 20) 

Un Attuazione

Se si può fare il vostro lavoro codice per disegnare punti, riquadri, linee e testo utilizzando varie pitture senza introdurre astrazioni, possiamo modificarlo per implementare l'interfaccia dalla prima sezione.

+0

Ho letto prima commenti su come evitare le classi di tipi a meno che non siano necessarie, e che le classi di tipi siano solitamente "solo" utili quando si suppone che anche la struttura con cui si gestiscono si comporti secondo le leggi. Qual è la tua opinione su questo, visto che usi le classi di tipo abbastanza (da quello che posso dire) liberamente? Sono solo un principiante anch'io e se non sono in linea con questa domanda (potenzialmente molto stupida) dimmelo.Ho difficoltà a capire come strutturare il codice con Haskell, proveniente da un mondo OOP! – kqr

+1

Se vuoi vedere qualcosa di simile fatto senza classi di caratteri, dai un'occhiata al gloss. L'uso "liberale" delle classi di tipi qui deriva dal fatto che non tutte le tele supportano le stesse cose, ma dovrebbero essere uguali a un programmatore. Una domanda come questa sollecita risposte che possono non sembrare niente l'una dell'altra, né come la domanda originale. Cerco di rispondere a loro come il più vicino possibile alla domanda originale, quindi ci sono classi di tipi per le stesse cose per cui la domanda originale aveva classi di tipi. – Cirdec

+0

Questo è assolutamente incredibile. Grazie mille per il tuo tempo e il dettaglio e il ritmo meraviglioso! – Evan