I'm starting to learn Haskell and wish to parse a PPM image for execrsice. The structure of the PPM format is rather simple, but it is tricky. It's described here. First of all, I defined a type for a PPM Image:
data Pixel = Pixel { red :: Int, green :: Int, blue :: Int} deriving(Show)
data BitmapFormat = TextualBitmap | BinaryBitmap deriving(Show)
data Header = Header { format :: BitmapFormat
, width :: Int
, height :: Int
, colorDepth :: Int} deriving(Show)
data PPM = PPM { header :: Header
, bitmap :: [Pixel]
}
bitmap should contain the entire image. This is where the first challange comes - the part that contains the actual image data in PPM can be either textual or binary (described in the header).
For textual bitmaps I wrote the following function:
parseTextualBitmap :: String -> [Pixel]
parseTextualBitmap = map textualPixel . chunksOf 3 . wordsBy isSpace
where textualPixel (r:g:b:[]) = Pixel (read r) (read g) (read b)
I'm not sure what to do with binary bitmaps, though. Using read converts a string representation of numbers to numbers. I want to convert "\x01" to 1 of type Int.
The second challange is parsing the header. I wrote the following function:
parseHeader :: String -> Header
parseHeader = constructHeader . wordsBy isSpace . filterComments
where
filterComments = unlines . map (takeWhile (/= '#')) . lines
formatFromText s
| s == "P6" = BinaryBitmap
| s == "P3" = TextualBitmap
constructHeader (format:width:height:colorDepth:_) =
Header (formatFromText format) (read width) (read height) (read colorDepth)
Which works pretty well. Now I should write the module exported function (let's call it parsePPM) which gets the entire file content (String) and then return PPM. The function should call parseHeader, deterime the bitmap format, call the apropriate parse(Textual|Binary)Bitmap and then construct a PPM with the result. Once parseHeader returns I should start decoding the bitmap from the point that parseHeader stopped in. However, I cannot know in which point of the string parseHeader stopped. The only solution I could think of is that instead of Header, parseHeader will return (Header,String), when the second element of the tuple is the remainder retrieved by constructHeader (which currently named as _). But I'm not really sure it's the "Haskell Way" of doing things.
To sum up my questions:
1. How do I decode the binary format into a list of Pixel
2. How can I know in which point the header ends
Since I'm learning Haskell by myself I have no one to actually review my code, so in addition to answering my questions I will appriciate any comment about the way I code (coding style, bugs, alternative way to do things, etc...).
Lets start with question 2 because it is easier to answer. Your approach is correct: as you parse things, you remove those characters from the input string, and return a tuple containing the result of the parse, and the remaining string. However, thereis no reason to write all this from scratch (except perhaps as an academic exercise) - there are plenty of parsers which will take care of this issue for you. The one I will use is
Parsec. If you are new to monadic parsing you should first read the section onParsecin RWH.As for question 1, if you use
ByteStringinstead ofString, then parsing single bytes is easy since single bytes are the atomic elements ofByteStrings!There is also the issue of the
Char/ByteStringinterface. WithParsec, this is a non-issue since you can treat aByteStringas a sequence ofByteorChar- we will see this later.I decided to just write the full parser - this is a very simple language so with all the primitives defined for you in the
Parseclibrary, it is very easy and very concise.The file header:
First, we write the 'primitive' parsers - that is, parsing bytes, parsing textual numbers, and parsing whitespace (which the PPM format uses as a seperator):
digitparses a single digit - you'll notice that many function names explain what the parser does - andmany1will apply the given parser 1 or more times. Then we read the resulting string to return an actual number (as opposed to a string). In this case, the inputByteStringis being treated as text.For this parser, we parse a single
Char- which is really just a byte. It is just returned as aChar. We could safely make the return typeParser Word8because the universe of values that can be returned is[0..255]oneOftakes a list ofCharand parses any one of the characters in the order given - again, theByteStringis being treated asText.Now we can write the parser for the header.
A few notes.
choicetakes a list of parsers and tries them in order.try ptakes the parser p, and 'remembers' the state beforepstarts parsing. If p succeeds, thentry p == p. If p fails, then the state before p started is restored and you pretend you never triedp. This is necessary due to howchoicebehaves.For the pixels, we have two choices as of now:
We could use
many1 (whitespace 1 >> parseIntegral)- but this wouldn't enforce the fact that we know what the length should be. Then, converting the list of numbers to a list of pixels is trivial.For binary data:
Note how the two are almost identical. You could probably generalize this function (it would be especially useful if you decided to parse the other types of pixel data - monochrome and greyscale).
Now to bring it all together:
This should be self-explanatory. Parse the header, then parse the body based on the format. Here are some examples to try it on. They are the ones from the specification page.
Several notes: this doesn't handle comments, which are part of the spec. The error messages are not very useful; you can use the
<?>function to create your own error messages. The spec also indicates 'The lines should not be longer than 70 characters.' - this is also not enforced.edit:
Just because you see do-notation, doesn't necessarily mean that you are working with impure code. Some monads (like this parser) are still pure - they are just used for convenience. For example, you can write your parser with the type
parser :: String -> (a, String), or, what we have done here, is we use a new type:data Parser a = Parser (String -> (a, String))and haveparser :: Parser a; we then write a monad instance forParserto get the useful do-notation. To be clear,Parsecsupports monadic parsing, but our parser is not monadic - or rather, uses theIdentitymonad, which is justnewtype Identity a = Identity { runIdentity :: a }, and is only necessary because if we usedtype Identity a = awe would have 'overlapping instances' errors everywhere, which is not good.So then, the type of
Parseris reallyParsecT ByteString () Identity. That is, the parser input isByteString, the user state is()- which just means we aren't using the user state, and the monad in which we are parsing isIdentity.ParsecTis itself just a newtype of:All those functions in the middle are just used to pretty-print errors. If you are parsing 10's of thousands of characters and an error occurs, you won't be able to just look at it and see where that happened - but
Parsecwill tell you the line and column. If we specialize all the types to ourParser, and pretend thatIdentityis justtype Identity a = a, then all the monads disappear and you can see that the parser is not impure. As you can see,Parsecis a lot more powerful than is required for this problem - I just used it due to familiarity, but if you were willing to write your own primitive functions likemanyanddigit, then you could get away with usingnewtype Parser a = Parser (ByteString -> (a, ByteString)).