Link Search Menu Expand Document

目录

  1. Parser 单子(Parser Monad)
    1. 使用 Alternative 自动回溯
  2. Builder 单子(Builder Monad)
    1. 利用 Builder 处理文本格式化

Parser 单子(Parser Monad)

Z.Data.Parser 中的 Parser 用于高性能、可恢复的二进制解析和简单的文本解析,例如网络协议、JSON 等。可以使用 Z.Data.Parser 中基本的解析函数例如 takeWhileint 等编写解析器。

import qualified Z.Data.Parser as P
import Z.Data.ASCII 

data Date = Date { year :: Int, month :: Int, day :: Int } deriving Show

dateParser :: P.Parser Date
dateParser = do
    y <- P.int
    P.word8 HYPHEN 
    m <- P.int
    P.word8 HYPHEN 
    d <- P.int
    return $ Date y m d

Z.Data.Parser 中的 Parser 直接作用于 Bytes:

> P.parse' dateParser "2020-12-12"
Date 2020 12 12
> P.parse' dateParser "2020-JAN-12"
Left ["Z.Data.Parser.Numeric.int","Z.Data.Parser.Base.takeWhile1: no satisfied byte at [74,65,78,45,49,50]"]
> P.parse dateParser "2020-12-12, 08:00"
([44,32,48,56,58,48,48], Right (Date {year = 2020, month = 12, day = 12}))
> P.parseChunk dateParser "2020-"
Partial _
> let (P.Partial f) = P.parseChunk dateParser "2020-"
> let (P.Partial f') = f "05-05"    -- incrementally provide input
> f' ""                             -- push empty chunk to signal EOF
Success Date {year = 2020, month = 5, day = 5}

二进制协议可以使用带有 TypeApplications 扩展的 decodePrim / decodePrimLE / decodePrimBE,假设您要实现一个 MessagePack str format 解析器:

import           Data.Bits
import           Data.Word
import qualified Z.Data.Parser as P
import qualified Z.Data.Text   as T

msgStr :: P.Parser T.Text
msgStr = do
    tag <- P.anyWord8
    case tag of
        t | t .&. 0xE0 == 0xA0 -> str (t .&. 0x1F)
        0xD9 -> str =<< P.anyWord8
        0xDA -> str =<< P.decodePrimBE @Word16
        0xDB -> str =<< P.decodePrimBE @Word32
        _    -> P.fail' "unknown tag"
  where
    str !l = do
        bs <- P.take (fromIntegral l)
        case T.validateMaybe bs of
            Just t -> return (Str t)
            _  -> P.fail' "illegal UTF8 Bytes"

parsecmegaparsec 不同,Z中的 Parser 提供有限的错误报告且不支持将其用作单子转换器(Monad Transformer)。 但是提供了 PrimMonad 的实例,该实例允许一些有限的副作用,例如可变量和数组的操作。

使用 Alternative 自动回溯

attoparsec 相似,Z中的 Parser 与 <|>Alternative 实例的函数)一起使用时总是回溯,这意味着失败的分支在不做任何特殊操作的情况下不会消费任何输入:

import Control.Applicative
...
p = fooParser <|> barParser <|> quxParser

在上面的代码中,如果任何 parser 失败,则从输入的开头重试下一个 parser。虽然并非总是需要回溯,还是建议使用 peek。或者在语法或协议可以被解析为 LL(1) 语法的时候使用peekMaybe, 因为它比回溯更快。

Builder 单子(Builder Monad)

Z.Data.Builder 中的 Builder 是解析过程的逆过程,即将 Haskell 数据类型写入 Bytes ,也就是 Writer 单子。用法非常类似于 Parser

import qualified Z.Data.Builder as B
import Z.Data.ASCII 

data Date = Date { year :: Int, month :: Int, day :: Int } deriving Show

dataBuilder :: Date -> B.Builder ()
dataBuilder (Date y m d) = do
    int' y
    B.word8 HYPHEN 
    int' m
    B.word8 HYPHEN 
    int' d
  where
    int' x | x > 10    = B.int x
           | otherwise = B.word8 DIGIT_0 >> B.int x

类型 Builder 内部记录了一个 buffer writing 函数,因此可以快速写出。使用 build/buildText 运行一个 Builder ,分别产生 BytesText

> B.build (dataBuilder $ Date 2020 11 1)
[50,48,50,48,45,49,49,45,48,49]
> B.buildText (dataBuilder $ Date 2020 11 1)
"2020-11-01"

可以使用 encodePrim/encodePrimLE/encodePrimBE 来构建二进制 Builder ,让我们仍然采用 MessagePack str format 举个例子:

import           Data.Bits
import           Data.Word
import qualified Z.Data.Builder as B
import qualified Z.Data.Text    as T
import qualified Z.Data.Vector  as V

msgStr :: T.Text -> B.Builder ()
msgStr t = do
    let bs = T.getUTF8Bytes t
    case V.length bs of
        len | len <= 31      ->  B.word8 (0xA0 .|. fromIntegral len)
            | len < 0x100    ->  B.encodePrim (0xD9 :: Word8, fromIntegral len :: Word8)
            | len < 0x10000  ->  B.encodePrim (0xDA :: Word8, BE (fromIntegral len :: Word16))
            | otherwise      ->  B.encodePrim (0xDB :: Word8, BE (fromIntegral len :: Word32))
    B.bytes bs

请注意,我们直接使用 Unalign a, Unalign b => Unalign (a, b) 实例在一行内写若干个 primitive types。 Unalign 类提供了基本的读写接口,可以从 raw bytes 读取 primitive types,也可以将 primitive types 写入 raw bytes。(raw bytes 的偏移量未对齐)

利用 Builder 处理文本格式化

与通常提供 printf 或类似方法的其他标准库不同,建议在Z中直接使用 Builder 格式化文本格式:

-- 与 print("The result are %d, %d", x, y) 相似的
-- 如果你确认所有Builder都将写utf-8格式的字符串
-- 你可以直接使用unsafeBuildText以减少一次校验

B.unsafeBuildText $ do
    "The result are " >> B.double x >> ", " >> B.double y

-- Or use do syntax

B.unsafeBuildText $ do
    "The result are " 
    B.double x 
    ", " 
    B.double y
...

Monadic Builder 的优势在于,您可以重用 Control.Monad 中的所有控制结构,例如条件,循环等。Builder () 具有 IsString 实例,可以将字符串字面值写成稍作调整的UTF-8格式:

  • \NUL 将被写为 \xC0\x80
  • \xD800 ~ \xDFFF 将被编码为三个字节,作为普通的UTF-8的替代。

只要您不写 \0 or \55296 ~ \57343, 就可以在 unsafeBuildText 中放心的使用字符串字面值。