我开始学习Haskell并希望为execrsice解析PPM图像。 PPM格式的结构相当简单,但它很棘手。它描述了here。首先,我为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]
}
bitmap
应包含整个图像。这是第一个挑战的地方 - 包含PPM中实际图像数据的部分可以是文本或二进制(在标题中描述)。
对于文本位图,我编写了以下函数:
parseTextualBitmap :: String -> [Pixel]
parseTextualBitmap = map textualPixel . chunksOf 3 . wordsBy isSpace
where textualPixel (r:g:b:[]) = Pixel (read r) (read g) (read b)
但是,我不知道如何处理二进制位图。使用read
将数字的字符串表示形式转换为数字。我想将“\ x01”转换为Int。类型的1
第二个挑战是解析标题。我写了以下函数:
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(Textual|Binary)Bitmap
,然后构造带有结果的PPM。一旦parseHeader返回,我应该从parseHeader停止的那一点开始解码位图。但是,我不知道字符串parseHeader在哪个点停止。我能想到的唯一解决方案是,当元组的第二个元素是constructHeader(当前名为_)检索的余数时,parseHeader将返回Header
而不是(Header,String)
。但我不确定这是做事的“Haskell方式”。
总结我的问题:
1.如何将二进制格式解码为Pixel
列表
2.我怎么知道标题在哪个点结束
由于我自己学习Haskell,所以我没有人真正查看我的代码,所以除了回答我的问题之外,我还会对我编码的方式(编码风格,错误,替代方法,等...)。
答案 0 :(得分:3)
让我们从问题2开始,因为它更容易回答。您的方法是正确的:在解析事物时,从输入字符串中删除这些字符,并返回包含解析结果和剩余字符串的元组。但是,没有理由从头开始编写所有这些内容(除非作为学术练习) - 有很多解析器会为您解决这个问题。我将使用的是Parsec
。如果您不熟悉monadic解析,则应首先阅读the section on Parsec
in RWH.
对于问题1,如果使用ByteString
而不是String
,则解析单个字节很容易,因为单个字节是ByteString
s的原子元素!
还存在Char
/ ByteString
接口的问题。使用Parsec
,这不是问题,因为您可以将ByteString
视为Byte
或Char
的序列 - 我们稍后会看到这一点。
我决定编写完整的解析器 - 这是一种非常简单的语言,因此在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格式用作分隔符):
parseIntegral :: (Read a, Integral a) => Parser a
parseIntegral = fmap read (many1 digit)
digit
解析一个数字 - 您会注意到许多函数名称解释了解析器的作用 - 而many1
将应用给定的解析器1次或更多次。然后我们读取结果字符串以返回实际数字(而不是字符串)。在这种情况下,输入ByteString
将被视为文本。
parseByte :: Integral a => Parser a
parseByte = fmap (fromIntegral . fromEnum) $ tokenPrim show (\pos tok _ -> updatePosChar pos tok) Just
对于这个解析器,我们解析一个Char
- 这实际上只是一个字节。它只是作为Char
返回。我们可以安全地生成返回类型Parser 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
一些笔记。 choice
获取解析器列表并按顺序尝试它们。 try p
获取解析器p,并在p
开始解析之前“记住”状态。如果p成功,则try p == p
。如果p失败,则恢复p开始之前的状态,并假装您从未尝试过p
。由于choice
的行为方式,这是必要的。
对于像素,我们现在有两个选择:
parseTextual :: Header -> Parser [Pixel]
parseTextual h = do
xs <- replicateM (3 * width h * height h) (whitespace1 >> parseIntegral)
return $ map (\[a,b,c] -> Pixel a b c) $ chunksOf 3 xs
我们可以使用many1 (whitespace 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
注意两者几乎完全相同。你可以推广这个函数(如果你决定解析其他类型的像素数据 - 单色和灰度,它会特别有用。)
现在把它们放在一起:
parsePPM :: Parser PPM
parsePPM = do
h <- parseHeader
fmap (PPM h) $
case format h of
TextualBitmap -> parseTextual h
BinaryBitmap -> parseBinary h
这应该是不言自明的。解析头部,然后根据格式解析主体。以下是一些尝试它的例子。它们是规范页面中的那些。
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 ]
几个注意事项:这不会处理注释,这是规范的一部分。错误消息不是很有用;您可以使用<?>
函数创建自己的错误消息。规范还指出'行不应超过70个字符。' - 这也没有强制执行。
编辑:
仅仅因为你看到了注释,并不一定意味着你正在使用不纯的代码。一些monad(像这个解析器)仍然是纯粹的 - 它们只是为了方便而使用。例如,您可以使用parser :: String -> (a, String)
类型编写解析器,或者我们在此处执行的操作是使用新类型:data Parser a = Parser (String -> (a, String))
并使用parser :: Parser a
;然后我们为Parser
编写一个monad实例来获得有用的do-notation。要清楚,Parsec
支持monadic解析,但我们的解析器不是monadic - 或者更确切地说,使用Identity
monad,它只是newtype Identity a = Identity { runIdentity :: a }
,并且只是必要因为如果我们使用{ {1}}我们在任何地方都会遇到“重叠实例”错误,这是不好的。
type Identity a = a
那么,>: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'
的类型确实是Parser
。也就是说,解析器输入是ParsecT ByteString () Identity
,用户状态是ByteString
- 这意味着我们没有使用用户状态,我们正在解析的monad是()
。 Identity
本身只是一种新类型:
ParsecT
中间的所有这些功能仅用于漂亮打印错误。如果您正在解析数十个字符中的10个并且发生错误,那么您将无法查看它并查看发生的位置 - 但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
将告诉您行和列。如果我们将所有类型专门化为Parsec
,并假装Parser
只是Identity
,则所有monad都会消失,您可以看到解析器不是不纯的。正如您所看到的,type Identity a = a
比此问题所需的功能强大得多 - 我只是因熟悉而使用它,但如果您愿意编写自己的原始函数,如Parsec
和{{ 1}},然后你就可以使用many
。