Evan
Evan

Reputation: 402

Haskell data serialization of some data implementing a common type class

Let's start with the following

data A = A String deriving Show
data B = B String deriving Show

class X a where
    spooge :: a -> Q

[ Some implementations of X for A and B ]

Now let's say we have custom implementations of show and read, named show' and read' respectively which utilize Show as a serialization mechanism. I want show' and read' to have types

show' :: X a => a -> String
read' :: X a => String -> a

So I can do things like

f :: String -> [Q]
f d = map (\x -> spooge $ read' x) d

Where data could have been

[show' (A "foo"), show' (B "bar")]

In summary, I wanna serialize stuff of various types which share a common typeclass so I can call their separate implementations on the deserialized stuff automatically.

Now, I realize you could write some template haskell which would generate a wrapper type, like

data XWrap = AWrap A | BWrap B deriving (Show)

and serialize the wrapped type which would guarantee that the type info would be stored with it, and that we'd be able to get ourselves back at least an XWrap... but is there a better way using haskell ninja-ery?

EDIT

Okay I need to be more application specific. This is an API. Users will define their As, and Bs and fs as they see fit. I don't ever want them hacking through the rest of the code updating their XWraps, or switches or anything. The most i'm willing to compromise is one list somewhere of all the A, B, etc. in some format. Why?

Here's the application. A is "Download a file from an FTP server." B is "convert from flac to mp3". A contains username, password, port, etc. information. B contains file path information. There could be MANY As and Bs. Hundreds. As many as people are willing to compile into the program. Two was just an example. A and B are Xs, and Xs shall be called "Tickets." Q is IO (). Spooge is runTicket. I want to read the tickets off into their relevant data types and then write generic code that will runTicket on the stuff read' from the stuff on disk. At some point I have to jam type information into the serialized data.

Upvotes: 0

Views: 396

Answers (4)

AndrewC
AndrewC

Reputation: 32455

I'll assume you want to do more things with deserialised Tickets than run them, because if not you may as well ask the user to supply a bunch of String -> IO() or similar, nothing clever needed at all.

If so, hooray! It's not often I feel it's appropriate to recommend advanced language features like this.

class Ticketable a where
    show' :: a -> String
    read' :: String -> Maybe a
    runTicket :: a -> IO ()
    -- other useful things to do with tickets

This all hinges on the type of read'. read' :: Ticket a => String -> a isn't very useful, because the only thing it can do with invalid data is crash. If we change the type to read' :: Ticket a => String -> Maybe a this can allow us to read from disk and try all the possibilities or fail altogether. (Alternatively you could use a parser: parse :: Ticket a => String -> Maybe (a,String).)

Let's use a GADT to give us ExistentialQuantification without the syntax and with nicer error messages:

{-# LANGUAGE GADTs #-}

data Ticket where
   MkTicket :: Ticketable a => a -> Ticket

showT :: Ticket -> String
showT (MkTicket a) = show' a

runT :: Ticket -> IO()
runT (MkTicket a) = runTicket a

Notice how the MkTicket contstuctor supplies the context Ticketable a for free! GADTs are great.

It would be nice to make Ticket and instance of Ticketable, but that won't work, because there would be an ambiguous type a hidden in it. Let's take functions that read Ticketable types and make them read Tickets.

ticketize :: Ticketable a => (String -> Maybe a) -> (String -> Maybe Ticket)
ticketize = ((.).fmap) MkTicket  -- a little pointfree fun

You could use some unusual sentinel string such as "\n-+-+-+-+-+-Ticket-+-+-+-Border-+-+-+-+-+-+-+-\n" to separate your serialised data or better, use separate files altogether. For this example, I'll just use "\n" as the separator.

readTickets :: [String -> Maybe Ticket] -> String -> [Maybe Ticket]
readTickets readers xs = map (foldr orelse (const Nothing) readers) (lines xs) 

orelse :: (a -> Maybe b) -> (a -> Maybe b) -> (a -> Maybe b)
(f `orelse` g) x = case f x of
     Nothing -> g x
     just_y  -> just_y

Now let's get rid of the Justs and ignore the Nothings:

runAll :: [String -> Maybe Ticket] -> String -> IO ()
runAll ps xs = mapM_ runT . catMaybes $ readTickets ps xs

Let's make a trivial ticket that just prints the contents of some directory

newtype Dir = Dir {unDir :: FilePath} deriving Show
readDir xs = let (front,back) = splitAt 4 xs in
        if front == "dir:" then Just $ Dir back else Nothing

instance Ticketable Dir where
    show' (Dir p) = "dir:"++show p
    read' = readDir
    runTicket (Dir p) = doesDirectoryExist p >>= flip when 
         (getDirectoryContents >=> mapM_ putStrLn $ p)

and an even more trivial ticket

data HelloWorld = HelloWorld deriving Show
readHW "HelloWorld" = Just HelloWorld
readHW _ = Nothing
instance Ticketable HelloWorld where
    show' HelloWorld = "HelloWorld"
    read' = readHW
    runTicket HelloWorld = putStrLn "Hello World!"

and then put it all together:

myreaders = [ticketize readDir,ticketize readHW]

main = runAll myreaders $ unlines ["HelloWorld",".","HelloWorld","..",",HelloWorld"]

Upvotes: 1

Thomas M. DuBuisson
Thomas M. DuBuisson

Reputation: 64740

If what you really want is a heterogeneous list then use existential types. If you want serialization then use Cereal + ByteString. If you want dynamic typing, which is what I think your actual goal is, then use Data.Dynamic. If none of this is what you want, or you want me to expand please press the pound key.

Based on your edit, I don't see any reason a list of thunks won't work. In what way does IO () fail to represent both the operations of "Download a file from an FTP server" and "convert from flac to MP3"?

Upvotes: 4

Gabriella Gonzalez
Gabriella Gonzalez

Reputation: 35099

Just use Either. Your users don't even have to wrap it themselves. You have your deserializer wrap it in the Either for you. I don't know exactly what your serialization protocol is, but I assume that you have some way to detect which kind of request, and the following example assumes the first byte distinguishes the two requests:

deserializeRequest :: IO (Either A B)
deserializeRequest = do
    byte <- get1stByte
    case byte of
        0 -> do
            ...
            return $ Left $ A <A's fields>
        1 -> do
            ...
            return $ Right $ B <B's fields>

Then you don't even need to type-class spooge. Just make it a function of Either A B:

spooge :: Either A B -> Q

Upvotes: 0

AndrewC
AndrewC

Reputation: 32455

I'd first like to stress for all our happy listeners out there that XWrap is a very good way, and a lot of the time you can write one yourself faster than writing it using Template Haskell.

You say you can get back "at least an XWrap", as if that meant you couldn't recover the types A and B from XWrap or you couldn't use your typeclass on them. Not true! You can even define

separateAB :: [XWrap] -> ([A],[B])

If you didn't want them mixed together, you should serialise them seperately!

This is nicer than haskell ninja-ery; maybe you don't need to handle arbitrary instances, maybe just the ones you made.


Do you really need your original types back? If you feel like using existential types because you just want to spooge your deserialised data, why not either serialise the Q itself, or have some intermediate data type PoisedToSpooge that you serialise, which can deserialise to give you all the data you need for a really good spooging. Why not make it an instance of X too?

You could add a method to your X class that converts to PoisedToSpooge.

You could call it something fun like toPoisedToSpooge, which trips nicely off the tongue, don't you think? :)

Anyway this would remove your typesystem complexity at the same time as resolving the annoying ambiguous type in

f d = map (\x -> spooge $ read' x) d  -- oops, the type of read' x depends on the String

You can replace read' with

stringToPoisedToSpoogeToDeserialise :: String -> PoisedToSpooge -- use to deserialise

and define

f d = map (\x -> spooge $ stringToPoisedToSpoogeToDeserialise x) -- no ambiguous type

which we could of course write more succincly as

f = map (spooge.stringToPoisedToSpoogeToDeserialise)

although I recognise the irony here in suggesting making your code more succinct. :)

Upvotes: 4

Related Questions