(通常)从自定义数据类型构建解析器?

时间:2017-03-03 16:24:11

标签: haskell

我正在研究需要与服务器通信的网络流媒体客户端。服务器以字节为单位对响应进行编码,例如“1 \ NULJohn \ NULTeddy \ NUL501 \ NUL”,其中'\ NUL'是分隔符。上面的响应转换为“这是类型1的消息(由服务器硬编码),它告诉客户端用户的ID是什么(这里,”John Teddy“的用户ID是”501“)。 / p>

我天真地定义了自定义数据类型

data User
  { firstName :: String
  , lastName :: String
  , id :: Int
  }

以及此数据类型的解析器

parseID :: Parser User
parseID = ...

然后,在解析器成功地匹配这样的响应之后,只需编写一个处理程序来完成某项工作(例如,写入数据库)。这非常简单。

但是,服务器有几乎100种类型的不同响应,客户端需要解析。我怀疑必须有更优雅的方式来完成这项工作,而不是像这样编写100个几乎相同的解析器,因为毕竟所有haksell编码器都是懒惰的。我是泛型编程的新手,所以有人可以告诉我是否有可以完成这项工作的软件包吗?

2 个答案:

答案 0 :(得分:5)

对于这些问题,我转向generics-sop而不是直接使用泛型。 generics-sop 构建于Generics之上,提供以统一方式处理记录中所有字段的功能。

在这个答案中,我使用了 base 附带的ReadP解析器,但任何其他Applicative解析器都可以。一些初步进口:

{-# language DeriveGeneric #-}
{-# language FlexibleContexts #-}
{-# language FlexibleInstances #-}
{-# language TypeFamilies #-}
{-# language DataKinds #-}
{-# language TypeApplications #-} -- for the Proxy

import Text.ParserCombinators.ReadP (ReadP,readP_to_S)
import Text.ParserCombinators.ReadPrec (readPrec_to_P)
import Text.Read (readPrec)
import Data.Proxy
import qualified GHC.Generics as GHC
import Generics.SOP

我们定义一个类型类,它可以为每个实例生成一个Applicative解析器。在这里,我们只定义IntBool的实例:

class HasSimpleParser c where
    getSimpleParser :: ReadP c

instance HasSimpleParser Int where
    getSimpleParser = readPrec_to_P readPrec 0

instance HasSimpleParser Bool where
    getSimpleParser = readPrec_to_P readPrec 0

现在我们为记录定义一个通用解析器,其中每个字段都有一个HasSimpleParser实例:

recParser :: (Generic r, Code r ~ '[xs], All HasSimpleParser xs) => ReadP r
recParser = to . SOP . Z <$> hsequence (hcpure (Proxy @HasSimpleParser) getSimpleParser)

Code r ~ '[xs], All HasSimpleParser xs约束意味着“此类型只有一个构造函数,字段类型列表为xs,所有字段类型都有HasSimpleParser个实例”。

hcpure构造了一个n-ary产品(NP),其中每个组件都是r字段的解析器。 (NP产品将每个组件包装在一个类型构造函数中,在我们的例子中是解析器类型ReadP)。

然后我们使用hsequence将解析器的n-ary产品转换为n-ary产品的解析器。

最后,我们将fmap插入到生成的解析器中,然后使用to将n-ary产品重新转换为原始r记录。需要ZSOP构造函数才能将n-ary产品转换为to函数所期望的乘积之和。

好的,让我们定义一个示例记录并使其成为Generics.SOP.Generic的实例:

data Foo = Foo { x :: Int, y :: Bool } deriving (Show, GHC.Generic)

instance Generic Foo -- Generic from generics-sop

让我们检查一下是否可以使用Foo解析recParser

main :: IO ()
main = do
    print $ readP_to_S (recParser @Foo) "55False"

结果是

[(Foo {x = 55, y = False},"")]

答案 1 :(得分:4)

您可以编写自己的解析器 - 但是已经有一个可以为您解析的包:cassava虽然SO通常不是搜索库建议的地方,但我希望包含此答案人们正在寻找解决方案,但没有时间自己实施,并寻找一个开箱即用的解决方案。

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}

import Data.Csv
import Data.Vector
import Data.ByteString.Lazy as B
import GHC.Generics

data Person = P { personId :: Int
                , firstName :: String
                , lastName :: String
                } deriving (Eq, Generic, Show)

 -- the following are provided by friendly neighborhood Generic
instance FromRecord Person
instance ToRecord Person

main :: IO ()
main = do B.writeFile "test" "1\NULThomas\NULof Aquin"
          Right thomas <- decodeWith (DecodeOptions 0) NoHeader <$> 
                              B.readFile "test"

          print (thomas :: Vector Person)

基本上,cassava允许您将所有X分隔的结构解析为Vector,前提是您可以记下FromRecord实例(需要parseRecord :: Parser …函数才能工作。

关于Generic的旁注直到最近我才认为 - 一切 - 在haskell中有一个Generic实例,或者可以派生一个。好吧,我不希望将一些ThreadId序列化为CSV / JSON,并且碰巧发现未装箱的类型并不那么容易“精通”!

在我忘记它之前 - 当你谈到流媒体和服务器等时,cassava-conduit可能会有所帮助。