0

让我们从以下开始

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、Bs 和 fs。我不希望他们破解其余代码来更新他们的 XWraps、开关或任何东西。我最愿意妥协的是以某种格式列出所有 A、B 等的列表。为什么?

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

4

4 回答 4

4

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

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

于 2012-10-14T01:59:08.883 回答
4

我首先想向我们所有快乐的听众强调这XWrap 一个非常好的方法,而且很多时候你可以自己编写一个比使用 Template Haskell 更快地编写它。

你说你可以取回“至少一个XWrap”,好像这意味着你不能恢复类型ABXWrap或者你不能在它们上使用你的类型类。不对!你甚至可以定义

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)

尽管我认识到这里建议使您的代码更简洁的讽刺意味。:)

于 2012-10-14T03:10:34.757 回答
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

现在让我们摆脱Justs 并忽略Nothings:

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"]
于 2012-10-14T22:10:19.177 回答
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>

然后你甚至不需要 type-class spooge。只需使其成为以下功能Either A B

spooge :: Either A B -> Q
于 2012-10-14T03:32:08.817 回答