I'm working on a network streaming client that needs to talk to the server. The server encodes the responses in bytestrings, for example, "1\NULJohn\NULTeddy\NUL501\NUL", where '\NUL' is the separator. The above response translates to "This is a message of type 1(hard coded by the server), which tells the client what the ID of a user is(here, the user id of "John Teddy" is "501").
So naively I define a custom data type
data User
{ firstName :: String
, lastName :: String
, id :: Int
}
and a parser for this data type
parseID :: Parser User
parseID = ...
Then one just writes a handler to do some job(e.g., write to a database) after the parser succesfully mathes a response like this. This is very straightforward.
However, the server has almost 100 types of different responses like this that the client needs to parse. I suspect that there must be a much more elegant way to do the job rather than writing 100 almost identical parsers like this, because, after all, all haksell coders are lazy. I am a total newbie to generic programming so can some one tell me if there is a package that can do this job?
For these kinds of problems I turn to generics-sop instead of using generics directly. generics-sop is built on top of Generics and provides functions for manipulating all the fields in a record in a uniform way.
In this answer I use the ReadP parser which comes with base, but any other Applicative
parser would do. Some preliminary imports:
{-# 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
We define a typeclass that can produce an Applicative
parser for each of its instances. Here we define only the instances for Int
and Bool
:
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
Now we define a generic parser for records in which every field has a HasSimpleParser
instance:
recParser :: (Generic r, Code r ~ '[xs], All HasSimpleParser xs) => ReadP r
recParser = to . SOP . Z <$> hsequence (hcpure (Proxy @HasSimpleParser) getSimpleParser)
The Code r ~ '[xs], All HasSimpleParser xs
constraint means "this type has only one constructor, the list of field types is xs
, and all the field types have HasSimpleParser
instances".
hcpure
constructs an n-ary product (NP
) where each component is a parser for the corresponding field of r
. (NP
products wrap each component in a type constructor, which in our case is the parser type ReadP
).
Then we use hsequence
to turn a n-ary product of parsers into the parser of an n-ary product.
Finally, we fmap into the resulting parser and turn the n-ary product back into the original r
record using to
. The Z
and SOP
constructors are required for turning the n-ary product into the sum-of-products the to
function expects.
Ok, let's define an example record and make it an instance of Generics.SOP.Generic
:
data Foo = Foo { x :: Int, y :: Bool } deriving (Show, GHC.Generic)
instance Generic Foo -- Generic from generics-sop
Let's check if we can parse Foo
with recParser
:
main :: IO ()
main = do
print $ readP_to_S (recParser @Foo) "55False"
The result is
[(Foo {x = 55, y = False},"")]
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With