Haskell数据序列化实现了一个公共类型类的一些数据

时间:2012-10-14 01:53:05

标签: haskell serialization typeclass

让我们从以下

开始
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 ]

现在假设我们有show和read的自定义实现,分别命名为show'和read',它们使用Show作为序列化机制。我希望show'和read'有类型

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

所以我可以做像

这样的事情
f :: String -> [Q]
f d = map (\x -> spooge $ read' x) d

数据本来可以

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

总之,我想序列化各种类型的东西,它们共享一个共同的类型类,所以我可以自动调用它们在反序列化的东西上的单独实现。

现在,我意识到你可以写一些模板haskell,它会生成一个包装类型,比如

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

并序列化包装类型,这将保证类型信息将与它一起存储,并且我们能够让自己至少回到XWrap ...但有更好的方式使用haskell ninja-ery ?

修改

好的,我需要更具体的应用程序。这是一个API。用户将根据自己的需要定义As,B和fs。我不希望他们通过其他代码更新他们的XWraps,交换机或任何东西。我最愿意妥协的是在某种格式的A,B等所有地方的一个列表。为什么呢?

这是应用程序。 A是“从FTP服务器下载文件”。 B是“从flac转换为mp3”。 A包含用户名,密码,端口等信息。 B包含文件路径信息。可能有很多As和Bs。数百人。尽可能多的人愿意编入该计划。两个只是一个例子。 A和B是X,X应称为“门票”。 Q是IO()。 Spooge是runTicket。我想读取相关数据类型的票据,然后编写将从磁盘上的内容读取的东西上运行的通用代码。在某些时候,我必须将类型信息堵塞到序列化数据中。

4 个答案:

答案 0 :(得分:4)

如果你真正想要的是异构列表,那么使用存在类型。如果你想序列化,那么使用Cereal + ByteString。如果你想要动态类型,这是我认为你的实际目标,那么使用Data.Dynamic。如果这不是您想要的,或者您希望我扩展,请按井号键。

根据您的编辑,我认为没有任何理由不会使用thunks列表。 IO ()以什么方式无法表示“从FTP服务器下载文件”和“从flac转换为MP3”的操作?

答案 1 :(得分:3)

我首先要强调所有我们快乐的听众,XWrap 非常好的方式,很多时候你自己写一个比写作更快它使用Template Haskell。

你说你可以取回“至少一个XWrap”,好像这意味着你无法从A或你那里恢复BXWrap类型无法使用你的类型类。不对!你甚至可以定义

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

如果你不希望它们混合在一起,你应该单独串行它们!

这比haskell ninja-ery更好;也许你不需要处理任意实例,也许只需要处理那些实例。


真的需要你原来的类型吗?如果您想使用存在类型,因为您只想spooge反序列化数据,为什么不序列化Q本身,或者有一些序列化的中间数据类型PoisedToSpooge,这可以反序列化为您提供真正良好欺骗所需的所有数据。为什么不把它作为X的实例?

您可以向X类添加一个转换为PoisedToSpooge的方法。

你可以把它称为像toPoisedToSpooge这样有趣的东西,它可以很好地脱离舌头,你不觉得吗? :)

无论如何,这会在解决烦人的模糊类型的同时消除你的类型系统复杂性

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

您可以将read'替换为

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

并定义

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

我们当然可以写得更加简洁

f = map (spooge.stringToPoisedToSpoogeToDeserialise)

虽然我认识到这里有讽刺意味,建议让你的代码更简洁。 :)

答案 2 :(得分:1)

我假设你想用反序列化的门票做更多的事情 而不是运行它们,因为如果不是,你也可以要求用户提供一堆String -> IO() 或类似的,根本不需要任何聪明。

如果是这样,万岁!我认为推荐这样的高级语言功能并不合适。

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

这一切都取决于read'的类型。 read' :: Ticket a => String -> a非常有用, 因为它对无效数据唯一能做的就是崩溃。 如果我们将类型更改为read' :: Ticket a => String -> Maybe a,则可以允许我们从磁盘读取 尝试所有可能性或完全失败。 (或者你可以使用解析器:parse :: Ticket a => String -> Maybe (a,String)。)

让我们使用GADT为我们提供没有语法和更好错误信息的ExistentialQuantification:

{-# 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

注意MkTicket构造函数如何免费提供上下文Ticketable a! GADT很棒。

制作Ticket和Ticketable的实例会很好,但是那不会起作用,因为会有 隐藏在其中的模糊类型a。让我们看一下读取Ticketable类型的函数并让它们被读取 票。

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

你可以使用一些不寻常的哨兵字符串,例如 "\n-+-+-+-+-+-Ticket-+-+-+-Border-+-+-+-+-+-+-+-\n"要分隔您的序列化数据或更好,请使用单独的文件 共。对于这个例子,我只使用" \ n"作为分隔符。

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

现在让我们摆脱Just并忽略Nothing s:

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

让我们制作一张简单的票,只打印一些目录的内容

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)

和更琐碎的票

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!"

然后把它们放在一起:

myreaders = [ticketize readDir,ticketize readHW]

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

答案 3 :(得分:0)

只需使用Either即可。您的用户甚至不必自己包装。你有你的反序列化器将它包装在Either中。我不确切知道你的序列化协议是什么,但我认为你有办法检测哪种请求,下面的例子假设第一个字节区分了这两个请求:

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>

然后你甚至不需要输入类spooge。只需使其成为Either A B

的函数
spooge :: Either A B -> Q