让我们从问题 2 开始,因为它更容易回答。您的方法是正确的:当您解析内容时,您从输入字符串中删除这些字符,并返回一个包含解析结果和剩余字符串的元组。但是,没有理由从头开始编写所有这些内容(可能作为学术练习除外) - 有很多解析器会为您解决这个问题。我将使用的是Parsec
. 如果您不熟悉 monadic 解析,您应该首先阅读RWH中的部分Parsec
。
至于问题 1,如果你使用ByteString
而不是String
,那么解析单个字节很容易,因为单个字节是ByteString
s 的原子元素!
还有Char
/ByteString
接口的问题。使用Parsec
,这不是问题,因为您可以将 aByteString
视为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 个字符。” - 这也没有强制执行。
编辑:
仅仅因为您看到 do-notation,并不一定意味着您正在使用不纯的代码。一些单子(比如这个解析器)仍然是纯粹的——它们只是为了方便而使用。例如,您可以使用 type 编写解析器parser :: String -> (a, String)
,或者,我们在这里所做的是使用新类型:data Parser a = Parser (String -> (a, String))
并且 have parser :: Parser a
; 然后我们编写一个 monad 实例Parser
来获得有用的 do-notation。需要明确的是,Parsec
它支持单子解析,但我们的解析器不是单子 - 或者更确切地说,使用Identity
单子,这只是newtype Identity a = Identity { runIdentity :: a }
,而且只是必要的,因为如果我们使用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
本身只是一种新类型:
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
中间的所有这些函数都只是用来漂亮地打印错误。如果您正在解析 10 的数千个字符并发生错误,您将无法仅查看它并查看发生的位置 - 但Parsec
会告诉您行和列。如果我们将所有类型专门化为我们的Parser
,并假装它Identity
是 just type Identity a = a
,那么所有的单子都消失了,你可以看到解析器不是不纯的。正如你所看到的,Parsec
它比这个问题所需的要强大得多——我只是因为熟悉而使用它,但如果你愿意编写自己的原始函数,比如many
and digit
,那么你可以不用使用newtype Parser a = Parser (ByteString -> (a, ByteString))
.