如何制作一个自定义的Attoparsec语法分析器组合子,返回Vector而不是列表


{-# LANGUAGE OverloadedStrings #-}
import Data.Attoparsec.Text
import Control.Applicative(many)
import Data.Word
parseManyNumbers :: Parser [Int] -- I'd like many to return a Vector instead
parseManyNumbers = many (decimal <* skipSpace)
main :: IO ()
main = print $ parseOnly parseManyNumbers "131 45 68 214"

上面只是一个例子,但我需要解析Haskell中的大量基元值,并且需要使用数组而不是列表。这在F#的Fparsec中是可能的,所以我已经查看了Attoparsec的源代码,但我不知道如何做到这一点。事实上,我不知道base Haskell库中Control.Applicative中的many是在哪里定义的。我以为它会在那里,因为这就是关于Hackage的文档指向的地方,但没有这样的运气。

此外,我很难决定在这里使用什么数据结构,因为我在Haskell中找不到像可调整大小的数组这样方便的东西,但我宁愿不使用低效的基于树的结构。

对我来说,一个选择是跳过Attoparsec,在ST monad中实现整个解析器,但我宁愿避免它,除非万不得已。

Haskell中有一个可增长的向量实现,它基于伟大的AMT算法:"持久向量"。不幸的是,到目前为止,这个图书馆在社区中的知名度还不高。然而,为了让您了解该算法的性能,我要说的是,正是该算法驱动了Scala和Clojure中的标准矢量实现。

我建议您在列表专用实现的影响下,围绕该数据结构实现解析器。这里的功能是,顺便说一句:

-- | One or more.
some :: f a -> f [a]
some v = some_v
  where
    many_v = some_v <|> pure []
    some_v = (fmap (:) v) <*> many_v
-- | Zero or more.
many :: f a -> f [a]
many v = many_v
  where
    many_v = some_v <|> pure []
    some_v = (fmap (:) v) <*> many_v

一些想法:

数据结构

我认为Ints列表中最实用的数据结构是[Vector Int]。如果每个分量Vector足够长(即长度为1k(,您将获得良好的空间经济性。你会有编写自己的"列表操作"来遍历它,但您将避免重新复制在单个Vector Int中返回数据时必须执行的数据。

还可以考虑使用Dequeue而不是列表。

状态解析

与Parsec不同,Attoparsec不提供用户状态。然而,你可能能够利用runScanner功能(链接(:

runScanner :: s -> (s -> Word8 -> Maybe s) -> Parser (ByteString, s)

(它还返回解析后的ByteString,在您的情况下,这可能会有问题,因为它会非常大。也许您可以编写一个不这样做的替代版本。(

使用unsafeFreezeunsafeThaw,可以增量填充矢量。您的s数据结构可能看起来类似于:

data MyState = MyState 
             { inNumber   :: Bool           -- True if seen a digit
             , val        :: Int            -- value of int being parsed
             , vecs       :: [ Vector Int ] -- past parsed vectors 
             , v          :: Vector Int     -- current vector we are filling
             , vsize      :: Int            -- number of items filled in current vector
             }

也许你用的不是[Vector Int],而是Dequeue (Vector Int)

然而,我认为这种方法会很慢,因为解析函数会被调用到每一个字符。

将列表表示为单个令牌

Parsec可以用来解析令牌流,那么写您自己的标记化器,并让Parsec创建AST。

关键思想是将这些大型Ints序列表示为单个令牌。这使您在解析它们时有了更多的自由度。

延迟转换

不需要在解析时将数字转换为Ints,只需让parseManyNumbers返回ByteString并将转换推迟到你实际上需要价值观。这使你能够避免具体化值作为实际列表。

Vectors是隐藏在引擎盖下的数组。数组的棘手之处在于它们是固定长度的。预先分配一个特定长度的数组,扩展它的唯一方法是将元素复制到一个更大的数组中。

这使得链表在表示可变长度序列方面简单地更好。(这也是命令式语言中的列表实现通过分配具有额外空间的数组并仅在空间用完时进行复制来分摊复制成本的原因。(如果你事先不知道会有多少元素,那么最好使用列表(如果需要的话,可以在之后使用fromList将列表复制到Vector中(。这就是many返回一个列表的原因:它尽可能多次地运行解析器,而事先不知道会有多少

另一方面,如果你碰巧知道你正在解析多少个数字,那么Vector可能会更有效。也许你知道总是有n数字,或者协议在序列开始前指定了有多少数字。然后你可以使用replicateM来有效地分配和填充向量。

相关内容

  • 没有找到相关文章

最新更新