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):
{-
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
{-
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.