Charlie Harvey

Haskell: PGM Parser from Chapter 10 of Real World Haskell

I am working my way through the Real World Haskell Book at the moment and finding it fun and challenging. In chapter 10 is devoted to parsing PGM files. One of the excersises is to extend the code from the book to deal with ASCII PGM files as well as raw ones. This is how I did it, along with a couple of example files, which I found hard to find.

My approach was to do the simplest thing that I could imagine working. I first made a new data type for ASCII greymaps. I could, I suppose have added a field for the plain ASCII data. This seemed cleaner somehow. --in PNM.hs/module PNM data ASCIIGreymap = ASCIIGreymap { aGreyWidth :: Int , aGreyHeight :: Int , aGreyMax :: Int , aGreyData :: String } deriving (Eq) instance Show ASCIIGreymap where show ( ASCIIGreymap w h m _ ) = "ASCIIGreymap w: " ++ show w ++ "X h: " ++ show h ++ " max: " ++ show m

Easy enough. Next we need a new parsing method in the Parse.hs file. My first shot was just a copy and paste of the existing ParseRawPGM method, with the header file type spec changed from P5 to P2 and mapping the bytes from the ByteString to ASCII characters. Once I had that working I refactored to pull out the duplicate code into a function that parses the header. The finished bit looked like this.--in Parse.hs/module Parse parsePGMHeader hspec = parseWhileWith w2c notWhite ==> \header -> skipSpaces ==>& assert (header == hspec) "invalid ascii header" ==>& parseNat ==> \width -> skipSpaces ==>& parseNat ==> \height -> skipSpaces ==>& parseNat ==> \maxGrey -> parseByte ==>& identity (width,height,maxGrey) where notWhite = (`notElem` "\r\n\t ") parseASCIIPGM = parsePGMHeader "P2" ==> \(width, height, maxGrey) -> parseBytes (width * height) ==> \bitmap -> identity (ASCIIGreymap width height maxGrey (map w2c $ L.unpack bitmap)) parseRawPGM = parsePGMHeader "P5" ==> \(width, height, maxGrey) -> parseBytes (width * height) ==> \bitmap -> identity (Greymap width height maxGrey bitmap)

I was fairly happy with the code, but I wanted to do some testing. There weren't any files hanging round on the interwebs to test on, so I made a couple with the GIMP, found that it had silently added a comment in to them, and removed the comment. You can download my tar.gz of example PGMs to save a bit of time generating test data. Then I just had to add a couple of test functions thus--in Parse.hs/module Parse -- Run on our raw test file expect: Right Greymap w: 10X h: 10 max: 255 testRawPGM = do str <- L8.readFile "test_raw.pgm" print (parse parseRawPGM str) -- Run on our ASCII test file, expect: Right Greymap w: 10X h: 10 max: 255 testASCIIPGM = do str <- L8.readFile "test_ascii.pgm" print (parse parseASCIIPGM str)

Just to prove that they work as expected I can now run:*Parse> :r [2 of 2] Compiling Parse ( Parse.hs, interpreted ) Ok, modules loaded: PNM, Parse. *Parse> testRawPGM Right Greymap w: 10X h: 10 max: 255 *Parse> testASCIIPGM Right Greymap w: 10X h: 10 max: 255 *Parse>

I haven’t had a crack at the other assignments yet. Will put something in the comments on the book site if I do.


  • Be respectful. You may want to read the comment guidelines before posting.
  • You can use Markdown syntax to format your comments. You can only use level 5 and 6 headings.
  • You can add class="your language" to code blocks to help highlight.js highlight them correctly.

Privacy note: This form will forward your IP address, user agent and referrer to the Akismet, StopForumSpam and Botscout spam filtering services. I don’t log these details. Those services will. I do log everything you type into the form. Full privacy statement.