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.