Reputation: 53077
I'm trying to create a type, a 2D Matrix, that wraps around mutable vectors. I want a few operations such as set, get. The problem is that I have absolutely no idea on how to get the types for it to work. I've tried several things, but I'm really just shooting on the dark because there is absolutely no explanation nowhere on how to do this. See:
data Matrix v s a = Matrix { size :: (Int,Int), buffer :: v s a } -- ?? Wrong
newMatrix = ??
set (x,y) mat = ??
get (x,y) val mat = ??
Take, for example, the official documentation. Where is the constructor? What does v
and a
in class MVector v a where
mean? What the heck is m (v (PrimState m) a)
? Is that the "type" of a generic mutable vector? What is m
and v
? What even is PrimState?
Upvotes: 2
Views: 220
Reputation: 27636
As @user2407038 noted, it is easier to restrict yourself at first to a concrete MVector
type like IOVector
. If you specialize the type of new
, read
and write
to IOVector
, you get the following, distinctively non-intimidating types:
new :: Int -> IO (IOVector a)
read :: IOVector a -> Int -> IO a
write :: IOVector a -> Int -> a -> IO ()
So for the first version, the implementation of your Matrix
operations are straightforward:
import Data.Vector.Mutable as V
import Control.Monad (liftM)
data Matrix a = Matrix { size :: (Int, Int), buffer :: IOVector a }
newMatrix :: (Int, Int) -> IO (Matrix a)
newMatrix (w, h) = liftM (Matrix (w, h)) $ V.new (w * h)
set :: (Int, Int) -> a -> Matrix a -> IO ()
set pos e mtx = V.write (buffer mtx) (offset mtx pos) e
get :: (Int, Int) -> Matrix a -> IO a
get pos mtx = V.read (buffer mtx) (offset mtx pos)
offset :: Matrix a -> (Int, Int) -> Int
offset (Matrix (w, _h) _) (x, y) = w * y + x
So then, how do we generalize over the choice of s
in MVector s
? Matrix
itself needs to be generalized over the choice of s
:
data Matrix s a = Matrix { size :: (Int, Int), buffer :: MVector s a }
And we also need to thread this generalization through to all the functions. Let's look at newMatrix
in detail; the rest can be left as an exercise for the reader.
If we just abstract on s
, newMatrix
becomes
newMatrix :: (Int, Int) -> IO (Matrix s a)
newMatrix (w, h) = liftM (Matrix (w, h)) $ V.new (w * h) -- Same implementation as above
However, surely this can't be right -- we can't create MVector s a
in IO
for any choice of s
, only RealWorld
! Expectedly, the typechecker catches this:
Couldn't match type `s' with `RealWorld'
`s' is a rigid type variable bound by
the type signature for newMatrix :: (Int, Int) -> IO (Matrix s a)
Expected type: s
Actual type: PrimState IO
Expected type: IO (MVector s a)
Actual type: IO (MVector (PrimState IO) a)
In the return type of a call of `new'
In the second argument of `($)', namely `new (w * h)'
But suppose we wrote
newMatrix :: (Monad m) => (Int, Int) -> m (Matrix s a)
This is, in some sense, even worse: now we are saying that for any choice of m
and s
(independent of each other!), we can construct a Matrix s a
in m
. Clearly this is not the case.
This is where the PrimMonad
typeclass is needed: it provides the linkage between PrimState m
, the choice of s
for the vector being manipulated, and the monad m
where this manipulation is possible. newMatrix
thus becomes
newMatrix :: (PrimMonad m) => (Int, Int) -> m (Matrix (PrimState m) a)
newMatrix (w, h) = liftM (Matrix (w, h)) $ V.new (w * h) -- implementation is still the same!
The other operations can be typed in an analogous way.
Upvotes: 3