我正在编写一个简单的CLI应用程序,它允许用户修改一些文档,真实的东西更复杂,但是假设我有一个跟踪当前文档并将CLI上指定的操作应用于所有文档的状态:
data MyState = MyState { doc :: [Document] }
run :: UserAction -> State MyState ()
run a = do s <- get ; put $ s { doc = map (edit a) (doc s) }
我有一些函数可以修改Document
,它目前只是一个内存数据结构:
data Document = Document [Stuff] [OtherStuff]
edit :: Document -> UserAction -> Document
现在我想重构一下,把公共接口拉成一个类:
class Document d where edit :: d -> UserAction -> d
instance Document MemoryDocument where edit = ... -- as before
instance Document RemoteDocument where edit = ... -- use HDBC etc
但我怎样才能轻松整合这个?
明显的变化是我无法在一个州处理不同类型的文件的问题:
data (Document a) => MyState a = MyState { doc :: [a] }
由于a
这里需要是一个动态类型,MemoryDocument或RemoteDocument。我可以使用包装器类型来模拟它,但这是很多不必要的样板代码(即每个实例的每个类函数一个模式)
data MyState = MyState { doc :: [DocumentWrapper] }
data DocumentWrapper = MD MemoryDocument | RD RemoteDocument
import Control.Applicative
instance Document DocumentWrapper where
edit (MD d) = MD <$> edit d
edit (RD d) = RD <$> edit d
有没有办法避免这种情况,也许有RankNTypes?
答案 0 :(得分:5)
老实说,如果你有两种类型,那么
[Either MemoryDocument RemoteDocument]
绝对是要走的路。事实上,如果你有一个静态数量的文件包装在一个sum类型可能是正确的举动。
您所描述的内容称为存在类型,您可以在其中删除类型信息,以便将异构数据存储在一个列表中。你可以这样做
{-# LANGUAGE ExistentialQuantification #-}
data DocBox = forall a. Document a => DocBox a
然后你有一个[DocBox]
,你可以像使用它一样使用它。定义
instance Document DocBox where
edit (DocBox d) a = DocBox $ edit d a
但这通常被认为是不好的做法,因为坦率地说它太过分了。你有两种类型,使用一个:)它就是它的用途。