Parsing 在Haskell中解析PPM图像

Parsing 在Haskell中解析PPM图像,parsing,haskell,ppm,Parsing,Haskell,Ppm,我开始学习Haskell,并希望为Exercise解析PPM图像。PPM格式的结构相当简单,但很棘手。它被描述了。首先,我为PPM图像定义了一种类型: data Pixel = Pixel { red :: Int, green :: Int, blue :: Int} deriving(Show) data BitmapFormat = TextualBitmap | BinaryBitmap deriving(Show) data Header = Header { format :: Bi

我开始学习Haskell,并希望为Exercise解析PPM图像。PPM格式的结构相当简单,但很棘手。它被描述了。首先,我为PPM图像定义了一种类型:

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]
               }
位图
应包含整个图像。这是第一个挑战——包含PPM中实际图像数据的部分可以是文本或二进制(在标题中描述)。 对于文本位图,我编写了以下函数:

parseTextualBitmap :: String -> [Pixel]
parseTextualBitmap = map textualPixel . chunksOf 3 . wordsBy isSpace
                     where textualPixel (r:g:b:[]) = Pixel (read r) (read g) (read b)
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)
不过,我不知道如何处理二进制位图。使用
read
将数字的字符串表示形式转换为数字。我想将“\x01”转换为Int类型的1

第二个挑战是解析报头。我编写了以下函数:

parseTextualBitmap :: String -> [Pixel]
parseTextualBitmap = map textualPixel . chunksOf 3 . wordsBy isSpace
                     where textualPixel (r:g:b:[]) = Pixel (read r) (read g) (read b)
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)
这很有效。现在我应该编写模块导出函数(我们称之为
parsePPM
),它获取整个文件内容(
String
),然后返回
PPM
。该函数应调用
parseHeader
,确定位图格式,调用适当的
parse(文本二进制)位图
,然后使用结果构造PPM。一旦parseHeader返回,我应该从parseHeader停止的点开始解码位图。然而,我不知道parseHeader在字符串的哪一点停止。我能想到的唯一解决方案是,当元组的第二个元素是constructHeader检索到的剩余元素时(当前命名为389;),parseHeader将返回
(Header,String)
,而不是
Header
。但我不确定这是否是哈斯克尔的做事方式

总结我的问题: 1.如何将二进制格式解码为
像素列表
2.我如何知道标题在哪一点结束


因为我自己在学习Haskell,所以没有人来实际检查我的代码,所以除了回答我的问题外,我还将对我的代码编写方式(编码风格、错误、替代方法等)发表评论。

让我们从问题2开始,因为它更容易回答。您的方法是正确的:解析内容时,从输入字符串中删除这些字符,并返回包含解析结果和剩余字符串的元组。但是,没有理由从头开始编写所有这些内容(也许作为一个学术练习除外)——有很多解析器将为您解决这个问题。我将使用的是。如果你是一元语法的新手,你应该首先阅读

至于问题1,如果您使用
ByteString
而不是
String
,那么解析单个字节很容易,因为单个字节是
ByteString
s的原子元素

还有
Char
/
ByteString
接口的问题。使用
Parsec
,这不是问题,因为您可以通过testring将
视为
字节序列或
字符序列-我们将在后面看到这一点

我决定只编写完整的解析器——这是一种非常简单的语言,因此,使用
Parsec
库中为您定义的所有原语,它非常简单、简洁

文件头:

import Text.Parsec.Combinator
import Text.Parsec.Char
import Text.Parsec.ByteString
import Text.Parsec 
import Text.Parsec.Pos

import Data.ByteString (ByteString, pack)
import qualified Data.ByteString.Char8 as C8

import Control.Monad (replicateM)
import Data.Monoid
首先,我们编写“基元”解析器——即解析字节、解析文本数字和解析空白(PPM格式将其用作分隔符):

digit
解析单个数字-您会注意到许多函数名解释了解析器的功能-并且
many1
将应用给定的解析器1次或多次。然后读取结果字符串以返回实际数字(与字符串相反)。在这种情况下,输入
ByteString
被视为文本

parseByte :: Integral a => Parser a
parseByte = fmap (fromIntegral . fromEnum) $ tokenPrim show (\pos tok _ -> updatePosChar pos tok) Just
对于这个解析器,我们解析单个
Char
——它实际上只是一个字节。它只是作为
字符返回。我们可以安全地创建返回类型
解析器Word8
,因为可以返回的值的范围是
[0..255]

whitespace1 :: Parser ()
whitespace1 = many1 (oneOf "\n ") >> return ()
oneOf
获取一个
Char
列表,并按照给定的顺序解析任何一个字符-同样,
ByteString
被视为
Text

现在我们可以为头部编写解析器了

parseHeader :: Parser Header 
parseHeader = do
  f <- choice $ map try $ 
         [string "P3" >> return TextualBitmap
         ,string "P6" >> return BinaryBitmap]
  w <- whitespace1 >> parseIntegral
  h <- whitespace1 >> parseIntegral
  d <- whitespace1 >> parseIntegral
  return $ Header f w h d
我们可以使用
many1(空格1>>parseIntegral)
——但这并不能强制我们知道长度应该是多少。然后,将数字列表转换为像素列表非常简单

对于二进制数据:

parseBinary :: Header -> Parser [Pixel]
parseBinary h = do
  whitespace1
  xs <- replicateM (3 * width h * height h) parseByte
  return $ map (\[a,b,c] -> Pixel a b c) $ chunksOf 3 xs
因此,
Parser
的类型实际上是
ParsecT ByteString()Identity
。也就是说,解析器输入是
ByteString
,用户状态是
()
——这意味着我们没有使用用户状态,而解析的monad是
Identity
ParsecT本身就是一种新类型的:

forall b.
    State s u
    -> (a -> State s u -> ParseError -> m b)
    -> (ParseError -> m b)
    -> (a -> State s u -> ParseError -> m b)
    -> (ParseError -> m b)
    -> m b

<> P>中间的所有函数只用于打印错误。如果您正在解析10个数千个字符,并且发生了错误,您将无法仅查看它并查看发生的位置-但是
Parsec
将告诉您行和列。如果我们将所有类型专门化为
解析器
,并假设
标识
只是
类型标识a=a
,那么所有的单子都会消失,您可以看到解析器不是不纯的。正如您所看到的,
Parsec
比这个问题所需的功能强大得多-我只是因为熟悉才使用它,但是如果您愿意编写自己的基本函数,如
many
digit
,那么您可以不用使用
newtype Parser a=Parser(ByteString->(a,ByteString))

没有足够的时间给出完整的答案,但你应该看和。返回未解析的余数绝对是“哈斯克尔方法”。只需看看
读取
类型类是如何在
读取
类型类方面实现的。@JonPurdy-如果我理解正确,我无法混合

example0 :: ByteString
example0 = C8.pack $ unlines 
  ["P3"
  , "4 4"
  , "15"
  , " 0  0  0    0  0  0    0  0  0   15  0 15"
  , " 0  0  0    0 15  7    0  0  0    0  0  0"
  , " 0  0  0    0  0  0    0 15  7    0  0  0"
  , "15  0 15    0  0  0    0  0  0    0  0  0" ]

example1 :: ByteString
example1 = C8.pack ("P6 4 4 15 ") <> 
  pack [0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 15, 0, 0, 0, 0, 15, 7, 
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 7, 0, 0, 0, 15,
        0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]
>:i Parser
type Parser = Parsec ByteString ()
        -- Defined in `Text.Parsec.ByteString'
>:i Parsec
type Parsec s u = ParsecT s u Data.Functor.Identity.Identity
        -- Defined in `Text.Parsec.Prim'
forall b.
    State s u
    -> (a -> State s u -> ParseError -> m b)
    -> (ParseError -> m b)
    -> (a -> State s u -> ParseError -> m b)
    -> (ParseError -> m b)
    -> m b