我在某个数据结构的两个实现之间徘徊,并且来自Haskell社区的关于什么是正确/标准的输入将被赞赏。
以ADT“Server”为例,它将多个服务器定义为nullary数据构造函数。
data Server = Server1
| Server2
| Server3
现在,对于每个这些服务器,我希望(除其他外)获得IP地址。假设我可以静态编码,我可以使用一些函数“getURL”和模式匹配。
getUrl :: Server -> String
getUrl Server1 = "192.168.1.1"
等等。现在任何使用服务器的函数都可以将Server放入类型并调用getURL。
serverStuff :: Server -> IO ()
这种方法似乎具有简单的非多态函数的好处,但代价是在getURL中有大量的模式匹配。另外,如果程序员添加了一个服务器但忘记将模式添加到getURL,除非用-Wall进行编译,否则它们会在没有警告的情况下得到运行时错误。
使用类型类攻击同样的问题,我可以将我的多构造函数ADT分解为一组特定于服务器的ADT,并为URL创建一个类型类。
data Server1 = Server1
data Server2 = Server2
data Server3 = Server3
class Server a where
getUrl :: a -> String
instance Server Server1 where
getUrl Server1 = "192.168.1.1"
等等。现在我不再使用之前使用的简单的非多态函数,而是创建像
这样的东西。serverStuff :: Server a => a -> IO ()
并处理ad-hoc多态性(函数专业化等)的含义。
从好的方面来说,类型类方法易于扩展,将模式匹配分解为更小的块,允许更大的抽象,例如分组服务器(data ServerCenter1 = Server1 | Server2 | Server3
),如果你没有声明getUrl,你仍然可以得到运行时错误(没有编译器警告),你至少在创建实例时被迫做出决定。
所以,我被撕裂,但倾向于实例作为一种更好的做事方式。有没有一种标准的方法来处理这个问题,还是一种“看似干净的东西”?
答案 0 :(得分:13)
如果您确信您需要包含服务器类型的唯一信息,我只是将它们实现为字符串周围的新类型:
newtype Server = Server { getURL :: String }
使其成为完整记录(如在hammar的注释中)将允许您在仅更改构造函数时添加信息,但代价是GeneralizedNewtypeDeriving。
一般来说,我会使用类型来表示事物和变量的类来表示细节,因此无效构造函数仅用于表示摘要,例如: data Status = Published | Draft
(或内置Bool)。除非有特殊原因,否则应避免将硬编码数据(如IP地址)转换为类型系统或函数。
如果您想要特定于服务器的行为,则可以轻松地将字段添加到记录中:
data Server = Server {
getURL :: String
, doSomething :: a -> IO () --Or any other functional signature
}
但是,我建议不要这样做,因为它会使其他代码模糊不清:
runSomething :: Server -> a -> IO ()
runSomething server arg = (doSomething server) arg
可以做任何事情,你需要找到该字段的最后一次更新来确定什么(因为函数没有Show
实例)。如果依赖性依赖于服务器的某些属性,我倾向于编码该属性,然后调度该属性,例如。
data ServerType = Production
| Development
data Server = Server {
getURL :: String
, serverType :: ServerType
}
runSomething :: Server -> a -> IO ()
runSomething server arg = case serverType server of
Production -> foo arg
Development -> bar arg
我认为这种方法优于将服务器名称硬编码到函数中,因为它解释了为什么给定服务器具有给定行为(并使特定服务器的行为更局部化),并且在记录字段中具有函数,因为它可以更容易地告诉runSomething的给定调用将做什么(因为可以检查并记录ServerType)。