Haskell using IO on a function that doesn't expect it

128 Views Asked by At

Question:

How do I give an "IO SDL.Surface" to a function that expects an "SDL.Surface"?

I'd rather rethink my entire approach than resort to using something like "unsafePerformIO", unless this is actually the correct time to use it (which I doubt).

Further info:

I had a file filled with numbers and filepaths and I've parsed this file and loaded the images located at these paths into a list [(Int, IO SDL.Surface)]. Problem is, that the SDL.blitSurface function expects a normal SDL.Surface.

Error message:

Couldn't match type `IO SDL.Surface'
              with `GHC.ForeignPtr.ForeignPtr SDL.SurfaceStruct'
Expected type: SDL.Surface
Actual type: IO SDL.Surface

I'm not sure that source code is necessary to answer the question, but I'll provide some anyway just in case it helps:

To load an image file I use:

loadImage :: FilePath -> IO SDL.Surface
loadImage [] = error "empty list"
loadImage a =
  SDL.loadBMP a

To create the list of numbers and images I use:

createIDImageList :: [Tiletype] -> [(Int, IO SDL.Surface)]
createIDImageList a =
  if null a then []
  else [(tiletypeid $ a !! 0, loadImage (C8.unpack ( tiletypeimage ( a !! 0))))] ++ createIDImageList (tail a)

To retrieve the correct picture from this list, I use this function:

imageFromID :: Int -> [(Int, IO SDL.Surface)] -> Maybe (IO SDL.Surface)
imageFromID a b =
  if null b then Nothing
  else if a == (fst $ b !! 0) then Just (snd $ b !! 0)
  else imageFromID a (tail b)

And finally I use the imageFromID with the SDL.blitSurface to draw the image, except that I can't due to IO.

2

There are 2 best solutions below

4
On BEST ANSWER

Any time you end up with [IO Foobar], what you probably want is actually IO [Foobar]. The sequence function transforms one into the other. Or you can use mapM instead of map when creating the list in the first place.

In your example, it's a little more complicated, since we have [(Int, IO Surface)]. Let me see what I can suggest...

loadImage is an I/O action. It takes a filename and returns an IO action to load the image. Your createIDImageList function is really

createIDImageList = map f
  where
    f a = (tiletypeid a, loadImage (C8.unpack ( tiletypeimage a) ) )

What you probably want to do is change f to have type IO (Int, Surface) rather than (Int, IO Surface). And then you can mapM f, yielding a single I/O action that returns a list of stuff.

createIDImageList :: [Tiletype] -> IO [(Int, SDL.Surface)]
createIDImageList = mapM f
  where
    f a = do
      surface <- loadImage (C8.unpack (tiletypeimage a) )
      return (tiletypeid a, surface)

Regarding imageFromID: what you probably want to do is something like this:

main = do
  images <- createIDImageList (...)
  ...
  let image5 = imageFromID 5 images
  SDL.blitSurface image5 ...

The type of imageFromID then becomes

imageFromID :: `Int -> [(Int, SDL.Surface)] -> Maybe SDL.Surface

(Since images now has type [(Int, SDL.Surface)], with no IO in it, thanks to <-.)

What you're doing here is that createIDImageList is actually loading everything off disk, and then you can use imageFromID (which has no I/O in it) whenever you want to get the surface you're interested in.

0
On
do image <- loadImage "imagefile"
   blitSurface image rect1 dest rect2