Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

(Generically) Build Parsers from custom data types?

Tags:

haskell

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?

like image 648
user2812201 Avatar asked Jan 05 '23 10:01

user2812201


1 Answers

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},"")]
like image 102
danidiaz Avatar answered Jan 12 '23 22:01

danidiaz