什么时候使用Haskell类型类的权威指南?

时间:2013-07-25 04:51:26

标签: haskell types functional-programming typeclass

所以,我非常了解代数类型和类型类,但我对它的软件工程/最佳实践方面很感兴趣。

对于类型类,现代共识是什么?他们是邪恶的吗?它们方便吗?它们应该被使用吗?何时使用?

这是我的案例研究。我正在写一个RTS风格的游戏,我有不同类型的“单位”(坦克,侦察等)。说我想获得每个单位的最大健康状况。关于如何定义类型的两个想法如下:

ADT的不同构造函数:

data Unit = Scout ... | Tank ...

maxHealth :: Unit -> Int

maxHealth Scout  = 10

maxHealth Tank = 20

单元的类型类,每种类型都是一个实例

class Unit a where
    maxHealth :: a -> Int

instance Unit Scout where
    maxHealth scout = 10

instance Unit Tank where
    maxHealth tank = 20

显然,最终产品中还会有更多的领域和功能。 (例如,每个单元将具有不同的位置等,因此并非所有功能都是恒定的。)

诀窍是,可能有些功能对某些单位有意义,但对其他单位则不然。例如,每个单位都有一个getPosition函数,但是坦克可能有一个getArmour函数,对没有装甲的侦察兵没有意义。

如果我希望其他Haskeller能够理解并遵循我的代码,那么这种“普遍接受”的写法是什么?

3 个答案:

答案 0 :(得分:7)

大多数Haskell程序员都对不必要的类型类不满。这些伤害类型的推断;如果没有技巧,你甚至无法列出Unit的名单;在GHC中,有所有秘密字典传递;他们以某种方式使哈多克斯难以阅读;他们可以导致脆弱的等级制度...也许其他人可以给你更多的理由。我想一个好的规则就是在避免使用它们时更加痛苦。例如,如果没有Eq,您必须手动传递函数来比较两个[[[Int]]](或使用一些临时运行时测试),这是一个难点ML编程。

看看this blog post。你使用总和类型的第一种方法是好的,但是如果你想让用户用新单位或其他方式修改游戏,我会建议像

data Unit = Unit { name :: String, maxHealth :: Int }

scout, tank :: Unit
scout = Unit { name = "scout", maxHealth = 10 }
tank  = Unit { name = "tank",  maxHealth = 20 }

allUnits = [ scout
           , tank
           , Unit { name = "another unit", maxHealth = 5 }
           ]

在你的例子中,你需要编码一个坦克有装甲的地方但是侦察兵没有。显而易见的可能性是使用Maybe Armor字段或特殊权力列表等额外信息来增加单位类型......这不一定是明确的方式。

一个重量级的解决方案,可能是矫枉过正的,是使用像Vinyl这样的库来提供可扩展的记录,为您提供一种子类型。

答案 1 :(得分:3)

我倾向于只在手动生成和传递实例时才使用类型类。在我写的代码中,这几乎从来没有。

答案 2 :(得分:2)

我不打算回答使用类型类的确定时间的答案,但我目前正在编写一个库,它使用您为Unit类描述的两种方法。我一般都依赖于和类型,但是类型类方法有一个很大的优点:它为你提供了{em>类型级之间的区别{/ 1}}。

这会强制您稍微写入您的接口,因为任何需要在Unit上进行多态化的函数必须仅使用最终定义的函数,最终在您的抽象类型类基础上。就我而言,重要的是我还可以使用Unit - 类型作为幻像类型中的类型参数。

例如,我正在编写一个Haskell绑定到Nanomsg(来自ZMQ原作者的类似ZMQ的项目)。在Nanomsg中,您有Unit种类型,它们共享表示和语义。每个Socket只有一个Socket,某些函数只能在特定Protocol的{​​{1}}上调用。我可以让这些函数抛出错误或返回Socket,但我将Protocol定义为共享类的单独类型。

Maybe

并让Protocol具有幻像类型参数

class Protocol p where ...

data Protocol1 = Protocol1
data Protocol2 = Protocol2

instance Protocol Protocol1 where ...
instance Protocol Protocol2 where ...

现在我可以在错误的协议上调用函数使其成为类型错误。

Socket

如果newtype Socket p = Socket ... 只是一个和类型,那么在编译时就无法检查。