5

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

如果有的话,关于类型类的现代共识是什么?他们是邪恶的吗?它们方便吗?应该使用它们,何时使用?

这是我的案例研究。我正在编写一个 RTS 风格的游戏,我有不同类型的“单位”(坦克、侦察兵等)。假设我想获得每个单位的最大生命值。我对如何定义它们的类型的两个想法如下:

ADT 的不同构造函数:

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

maxHealth :: Unit -> Int

maxHealth Scout  = 10

maxHealth Tank = 20

Unit的Typeclass,每一种都是一个实例

class Unit a where
    maxHealth :: a -> Int

instance Unit Scout where
    maxHealth scout = 10

instance Unit Tank where
    maxHealth tank = 20

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

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

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

4

3 回答 3

7

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

看看这篇博文。您使用 sum 类型的第一种方法是可以的,但是如果您想允许用户使用新单位或其他方式修改游戏,我建议您使用类似的方法

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字段或特殊权力列表)来增加 Unit 类型......不一定有明确的方法。

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

于 2013-07-25T04:53:46.127 回答
3

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

于 2013-07-25T07:19:39.543 回答
2

我不打算就使用类型类的确定时间给出答案,但我目前正在编写一个库,该库使用您为 Unit 类描述的两种方法。我一般倾向于 sum 类型,但是 typeclass 方法有一个很大的优势:它为您提供了s 之间的类型级区别Unit

这迫使您对接口进行更多的编写,因为任何需要在Units 上进行多态的函数都必须仅使用最终在您的抽象类型类基础上定义的函数。就我而言,它还重要的是让我可以使用Unit-type 类型作为幻像类型中的类型参数。

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

class Protocol p where ...

data Protocol1 = Protocol1
data Protocol2 = Protocol2

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

并且有Sockets 有一个幻像类型参数

newtype Socket p = Socket ...

现在我可以将调用错误协议上的函数变成类型错误。

funOnProto1 :: Socket Protocol1 -> ...

而如果Socket只是一个 sum 类型,这将无法在编译时检查。

于 2013-07-25T05:38:14.370 回答