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 -> a
Monoid 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.
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
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
"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