2017-03-03 67 views
4

我正在处理需要与服务器通信的网络流式客户端。服务器以字节串编码响应,例如“1 \ NULJohn \ NULTeddy \ NUL501 \ NUL”,其中'\ NUL'是分隔符。上述响应转换为“这是类型1(由服务器硬编码)的消息,告诉客户端用户的ID是什么(这里,”John Teddy“的用户ID是”501“)。(通常)从自定义数据类型构建解析器?

那么天真,我定义自定义数据类型

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

和对这种数据类型

parseID :: Parser User 
parseID = ... 

然后解析器一个只写了一个处理程序做了一些工作(例如,写入到数据库)解析器成功完成这样的响应之后,这非常简单。

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

+0

泛型可以做到这一点。你可以在类似attoparsec的地方建立一个通用的解析器,并使用一个'Parseable'类型类,为实现'Generic'的任何东西提供一个默认的实现。那么你只需要'实例可解析用户在哪里'来解析它。 – bheklilr

+0

很高兴知道。我在哪里可以找到更多细节?我没有谷歌“attoparsec泛型可解析”,但是,搜索结果并不是很有帮助。 – user2812201

+1

'Parsable'将会是你自己写的类型类。 Attoparsec是一个在解析字节串中体面的库。 Generic是一个内置的类型类,它提供了获取可以在代码中操作的数据类型的通用表示的函数。例如,aeson提供了一个'FromJSON'类型类,它可以利用'Generic',这样你就可以做'FromJSON MyType where'而无需额外的工作来获得将JSON解析为'MyType'的值的能力。 – bheklilr

回答

5

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

在这个答案我使用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元产品(NP),其中每个组件是r对应字段的解析器。 (NP产品将每个组件包装在一个类型构造器中,在我们的例子中是分析器类型ReadP)。

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

最后,我们将fmap导入解析器,然后使用to将n元产品转换回原始r记录。将n元产品转换为函数所期望的和的乘积需要ZSOP构造函数。


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

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

instance Generic Foo -- Generic from generics-sop 

让我们来看看,如果我们可以分析FoorecParser

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

结果是

[(Foo {x = 55, y = False},"")] 
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) 

基本上木薯用户可以分析所有的X相分离结构为Vector,只要你能写下FromRecord实例(这需要一个parseRecord :: Parser …功能工作。在Generic

附注直到最近我思考 - 一切 - 在haskell有一个通用的实例,或可以派生一个。好吧,这不是我想要序列化一些ThreadId CSV/JSON的情况,碰巧发现unboxed类型不是那么容易“genericked”!

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