DrawingCombinators study
Graphics.DrawingCombinators seems to be a very good fit for my purposes. I was just in the process of converting a game written in gloss to sdl2 to allow input from a joystick (which gloss lacks). The problem is that sdl2
lacks a corresponding simple abstraction for vector graphics, Picture, which gloss has.
The Picture
(in gloss) defines an abstract data type that allow basic shapes, transformations and colors to be composed in a simple way. You really don't need any knowledge of OpenGL to use gloss
.
DrawingCombinators
library is also very compact (only two files) and elegantly written.
The problem is that the library is so abstract and pure that I have a really hard time to understand it. Because I want to be able to extend the library I need to understand how it works. I'm also considering using gloss abstraction with sdl2 because that is much more straight forward, but with much more code.
Still, DrawingCombinators
is really the essence of what I need. Actually it has two parts 2D drawing and 2D shape detection on a point and I only need the drawing part.
So here's my attempt to understand the libraries core parts ...
DrawingCombinators
use an Image
as its basic building block for 2D pictures.
data Image a = Image { dRender :: Renderer
, dPick :: Picker a }
An image is really a Renderer
.
type Renderer = Affine -> Color -> IO ()
The picker part includes the type parameter a
, but I will not go into that part much.
type Picker a = R2 -> a
So a renderer can render its image in a color transformed by an affine transformation (represented as the transformation matrix).
data Affine = M !R !R !R
!R !R !R
-- 0 0 1
An image may be composed of many images. This is done by instantiating the Monoid instance for Image
.
instance (Monoid m) => Monoid (Image m) where
mempty = pure mempty
mappend = liftA2 mappend
That looks simple enough!
pure mempty :: (Applicative f, Monoid a) => f a
mappend :: Monoid a => a -> a -> a
liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c
liftA2 mappend :: (Applicative f, Monoid c) => f c -> f c -> f c
First I was very confused by the "lifting". But monoid instance of Image
seems to require Image
to be an Applicative
.
This turns out to be a common instantiation of monoid. See Equational reasoning at scale for more background on this.
mappend
for e.g. IO () can be implemented the exact same way.
print "hello" :: IO ()
liftA2 mappend (print "hello") (print "world")
"hello"
"world"
mappend io1 io2 = do
a1 <- io1
a2 <- io2
return (mappend a1 a2)
OR
mappend = liftA2 mappend
For monads like IO
, >>
has the same meaning i.e. it composes two monads where the first is computed before the other and its results are appended.
(>>) :: Monad m => m a -> m b -> m b
(print "hello") (print "world")
"hello"
"world"
The same works for functions (a -> b
).
(+1) :: Num a => a -> a
liftA2 (+) (+2) (*3) 4
18
liftA2 (mappend) (+2) (*3) 4 :: Sum Int
Sum {getSum = 18}
We can always get a monoid from an applicative this way.
So, Image
also instantiates Applicative
instance Applicative Image where
pure x = Image {
dRender = (pure.pure.pure) (),
dPick = const x
}
df <*> dx = Image {
-- reversed so that things that come first go on top
dRender = (liftA2.liftA2.liftA2) mappend (dRender dx) (dRender df),
dPick = dPick df <*> dPick dx
}
}
What happens here? mappend
is lifted 3 times on the renderer!
(<*>) :: Applicative f => f (a -> b) -> f a -> f b
<*>
lifts a function inside an applicative.
First parameter is function on the result of the rendering.
let renderer = \a b -> print a >> print b
renderer :: (Show a, Show a1) => a -> a1 -> IO ()
We lifted IO ()
once to allow mappend
its outputs (effects).
Help from author Luke Palmer:
The pattern (liftA2 . liftA2 . ...) f x y
, with n
liftA2
s, is a semantic editor combinator style pattern, which just means to apply f to x and y nested under n applicatives.
We're working with Affine -> Color -> IO ()
objects, so the mappend
is applied to the type 3 levels deep: ()
.
In more detail:
(liftA2.liftA2.liftA2) mappend :: f (f1 (f2 c)) -> f (f1 (f2 c)) -> f (f1 (f2 c))
f ~ (Affine ->)
f1 ~ (Color ->)
f2 ~ IO
c ~ ()
The monoid instance used here is the trivial monoid instance on () ...
But another part was still confusing. First parameter of <*>
is a function nested under an applicative.
So what does a function on the applicative image really mean? The previous discussion require df
and dx
to have the same type.
dRender df , dRender dx :: Affine -> Color -> IO ()
It turns out that because the render part of the applicative instance of image doesn't include the type parameter a
we can assume this part to have the same type. So this means that appending the results of the IO operations is a perfectly valid way to handle this part.
And the other part dPick
does assume a df
to be a function nested under an applicative.
type Picker a = R2 -> a
dPick = dPick df <*> dPick dx
A similar example is the following.
(+) <*> (*2) $ 3
9
dPick
is a unary function just as *2
. +
is a function on a unary function i.e. a function of two parameters.
The <*>
creates a function where the parameter 3
is first applied to *2
, and then is 3
and the result of 2*3
applied to +
i.e. 9
.
f <*> g = \x -> f x (g x)
An abstract data type (ADT) is really a product type like a tuple.
instance Monoid a => Applicative ((,) a) where
pure x = (mempty, x)
(u, f) <*> (v, x) = (u `mappend` v, f x)
In the implementation of applicative for tuple above the monoid part is just appended!