Existential antipattern, how to avoid

Below it seems to work ... but it seems awkward.

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 xy) 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 wh) onPaint = Graphics.Win32.allocaPAINTSTRUCT $ \ lpps -> do hwnd <- createWindow wh (wndProc lpps (windowsOnPaint onPaint)) messagePump hwnd 

Now, apparently, the preferred method is just plain

 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 ... } 

BUT...

We like to preserve paints and mutate them during the painting process, as they are expensive to create and destroy. The paint can be just a list, for example [bgColor red, fgColor blue, the font "Tahoma"] or something, or it can be a pointer to the internal structure used by the paint system (this is an abstraction over GDI windows, but will eventually be abstract via direct2d and coregraphics), which have “coloring” objects that I don’t want to recreate again and again, and then snap them.

The beauty of existential beings in my mind is that they can opaquely wrap something abstract above him, and we can save him somewhere, bring him back, whatever. When you partially apply, I think there is a problem that the thing that you partially apply is now “stuck inside” in the container. Here is an example. Say I have a drawing object like

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

Where can I put the pointer? When I give Paint a function in Canvas, how does it get a pointer? What is the correct way to develop this API?

+7
haskell
source share
1 answer

Interface

First, you need to ask: "What are my requirements?". Suppose we want to make a canvas in plain English (these are my guesses based on your question):

  • Some canvases may have figures on them.
  • Some canvases may overlay text on them.
  • Some canvases change what they do on the basis of paint
  • We do not know what colors are still there, but they will be different for different paintings.

Now we are translating these ideas into Haskell. Haskell is a type-one language, so when we talk about requirements and design, we probably talk about types.

  • In Haskell, when we see the word "some," when we talk about types, we think about class types. For example, the show class says "some types can be represented as strings."
  • When we talk about something that we don’t know yet, talking about requirements, this is the type where we don’t know what it is yet. This is a type variable.
  • “put on them” seems to mean that we take the canvas, put something on it and take the canvas again.

Now we could write classes for each of these requirements:

 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 pc where -- p is the type of Paint load :: p -> c -> c 

The variable type c used only once, appearing as c -> c . This suggests that we could make this more general by replacing c -> c with c .

 class ShapeCanvas c where -- c is the type of the canvas draw :: Shape -> c class TextCanvas c where write :: Text -> c class PaintCanvas pc where -- p is the type of paint load :: p -> c 

PaintCanvas now looks like a class , which is problematic in Haskell. It is difficult for the type system to figure out what happens in classes like

 class Implicitly ab where convert :: b -> a 

I would facilitate this by modifying PaintCanvas to take advantage of the TypeFamilies extension.

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

Now let's connect everything for our interface, including your data types for shapes and text (meaning changed for 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 

Some examples

In this section, an additional requirement will be added for useful paintings, except those that we have already developed. This is an analogue of what we lost when we replaced c -> c with c in canvas classes.

Start with the first op code example. With our new interface it is simple:

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

Make a slightly more complex example. How about something that draws an “X”? We can make the first stroke of "X"

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

But we cannot add another Path for the lateral impact. We need to somehow combine the two steps of drawing. Something with type c -> c -> c would be ideal. The simplest Haskell class that I can think of is Monoid a mappend :: a -> a -> a . A Monoid requires identity and associativity. Is it possible to assume that there is a drawing operation on the canvas that leaves them untouched? That sounds quite reasonable. Is it reasonable to assume that three drawing operations performed in the same order do the same, even if the first two are performed together and then the third, or if the first, and then the second and third are performed together? Again, this seems to me quite reasonable. This allows you to write ex as:

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

Finally, consider something interactive that decides what to draw based on something external:

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

This does not work, because we do not have an instance for (Monad m) => Monoid (m ()) , so ex will work. We could use Data.Semigroup.Monad from the gearbox or add it ourselves, but this puts us in incoherent instances of the earth. It would be easier to change ex to:

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

But the type system cannot understand that the unit from the first draw matches the unit from the second. Our difficulty here implies additional requirements that we could not bear at first:

  • Canvases extend existing sequences of operations, providing operations for drawing, entering text, etc.

Theft directly from http://www.haskellforall.com/2013/06/from-zero-to-cooperative-threads-in-33.html :

  • When you hear a "sequence of instructions", you should think: "monad."
  • When you qualify for this with the extension, you should think: "monad transformer".

Now we understand that the implementation of our canvas is likely to become a monad transformer. We can go back to our interface and change it so that each of the classes is a class for the monad, like the Transformer classes MonadIO and mtl monad.

Interface Revised

 {-# 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 () 

Revised Examples

Now all our examples of drawing operations are actions in the unknown 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 ()] 

We can also make an example of the use of paint. Since we do not know what colors will exist, all of them should be provided from the outside (as arguments for example):

 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) 

Implementation

If you can make your code work to draw dots, fields, lines and text using various colors without introducing an abstraction, we can change it to implement the interface from the first section.

+9
source share

All Articles