如何制作一个返回Vector而不是列表的自定义Attoparsec解析器组合器?

时间:2016-08-05 15:42:32

标签: haskell attoparsec

{-# 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#'s Fparsec中可能出现的问题,所以我一直在看Attoparsec的来源,但我无法找到办法。实际上,我无法弄清many Haskell库中Control.Applicative base的定义。我认为它会存在,因为那是关于Hackage的文档所指向的,但没有这样的运气。

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

对我来说,一个选项是跳过Attoparsec并在ST monad中实现一个完整的解析器,但我宁愿避免它,除非作为最后的手段。

3 个答案:

答案 0 :(得分:1)

private async Task<bool> SendMessageUsingAWSProfileAsync(EmailMessage message, CancellationToken token) { MailMessage mailMessage = GetMailMessage(message); if (mailMessage == null) { _logger.DebugFormat("Unable to create MailMessage from message: {0}.", message); return false; } SendRawEmailResponse response; var credentials = new BasicAWSCredentials(_settings.GetString("AWS.Key"), _settings.GetString("AWS.Secret")); using (var amazonClient = new AmazonSimpleEmailServiceClient(credentials, _awsRegion)) { using (MemoryStream stream = ConvertMailMessageToMemoryStream(mailMessage)) { // Send Email using AWS API var rawMessage = new RawMessage(stream); List<String> destinations = new List<string>(); var sendRequest = new SendRawEmailRequest(rawMessage) { Source = mailMessage.From.Address, }; response = await amazonClient.SendRawEmailAsync(sendRequest, token).ConfigureAwait(false); } } message.MessageId = response.MessageId; message.FromServiceId = _settings.GetString("AWS.Key"); message.CommunicationDirectionEnum = CommunicationDirectionEnum.Out; await LogMessageAsync(message, token).ConfigureAwait(false); await AuditMessageAsync(message, token).ConfigureAwait(false); return true; } private MailMessage GetMailMessage(EmailMessage message) { if (message == null) throw new ArgumentNullException(nameof(message)); if (string.IsNullOrEmpty(message?.Body)) { _logger.DebugFormat("Empty email body."); return null; } if (string.IsNullOrEmpty(message.ToEmailID)) { _logger.DebugFormat("To EmailID is empty."); return null; } List<string> mailAddresses = GetToEmailList(message.ToEmailID); List<string> bccMailAddresses = GetToEmailList(message.BccEmailId); if (mailAddresses == null || mailAddresses.Count == 0) { _logger.DebugFormat("No valid To EmailID."); return null; } var result = new MailMessage(); foreach (var toAddress in mailAddresses) { result.To.Add(new MailAddress(toAddress)); } foreach (var bccAddress in bccMailAddresses) { result.Bcc.Add(new MailAddress(bccAddress)); } return result; } public static MemoryStream ConvertMailMessageToMemoryStream(MailMessage message) { Assembly assembly = typeof(SmtpClient).Assembly; Type mailWriterType = assembly.GetType("System.Net.Mail.MailWriter"); var stream = new MemoryStream() ConstructorInfo mailWriterConstructor = mailWriterType.GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, null, new[] { typeof(Stream) }, null); object mailWriter = mailWriterConstructor.Invoke(new object[] { stream }); MethodInfo sendMethod = typeof(MailMessage).GetMethod("Send", BindingFlags.Instance | BindingFlags.NonPublic); sendMethod.Invoke(message, BindingFlags.Instance | BindingFlags.NonPublic, null, new[] { mailWriter, true, true }, null); MethodInfo closeMethod = mailWriter.GetType() .GetMethod("Close", BindingFlags.Instance | BindingFlags.NonPublic); closeMethod.Invoke(mailWriter, BindingFlags.Instance | BindingFlags.NonPublic, null, new object[] { }, null); return stream; } 是数组,在引擎盖下。关于数组的棘手问题是它们是固定长度的。您预先分配了一定长度的数组,扩展它的唯一方法是将元素复制到一个更大的数组中。

这使得链表在表示可变长度序列时只需更好。 (这也是为什么命令式语言中的列表实现通过分配具有额外空间的数组并且仅在空间耗尽时复制来弥补复制成本的原因。)如果您事先不知道有多少元素要去最好的办法是使用一个列表(如果需要,可以使用fromList将列表复制到Vector中。这就是为什么Vector返回一个列表的原因:它会尽可能多地运行解析器,而事先并不了解它的数量。

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

答案 1 :(得分:1)

一些想法:

数据结构

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

另请考虑使用Dequeue代替列表。

有状态解析

与Parsec不同,Attoparsec不提供用户状态。但是,你 也许可以使用runScanner函数(link)

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可以用来解析一个令牌流,那么如何写 你自己的tokenizer并让Parsec创建AST。

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

延迟转换

不是在解析时将数字转换为Ints,而是让parseManyNumbers返回ByteString并将转换推迟到 你真的需要这些价值观。这使您可以避免重新启用 这些值作为实际列表。

答案 2 :(得分:1)

Haskell中有一个可扩展的向量实现,它基于优秀的AMT算法:"persistent-vector"。不幸的是,到目前为止,图书馆在社区中并不为人所知。但是为了给你一个关于算法性能的线索,我会说它是驱动Scala和Clojure中标准向量实现的算法。

我建议您在列表专用实现的影响下围绕该数据结构实现解析器。这里的函数是,btw:

-- | 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