让我们从以下
开始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。我想读取相关数据类型的票据,然后编写将从磁盘上的内容读取的东西上运行的通用代码。在某些时候,我必须将类型信息堵塞到序列化数据中。
答案 0 :(得分:4)
如果你真正想要的是异构列表,那么使用存在类型。如果你想序列化,那么使用Cereal + ByteString。如果你想要动态类型,这是我认为你的实际目标,那么使用Data.Dynamic。如果这不是您想要的,或者您希望我扩展,请按井号键。
根据您的编辑,我认为没有任何理由不会使用thunks列表。 IO ()
以什么方式无法表示“从FTP服务器下载文件”和“从flac转换为MP3”的操作?
答案 1 :(得分:3)
我首先要强调所有我们快乐的听众,XWrap
是非常好的方式,很多时候你自己写一个比写作更快它使用Template Haskell。
你说你可以取回“至少一个XWrap
”,好像这意味着你无法从A
或你那里恢复B
和XWrap
类型无法使用你的类型类。不对!你甚至可以定义
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