【问题标题】:Definitive guide on when to use Haskell typeclasses? [closed]关于何时使用 Haskell 类型类的权威指南? [关闭]
【发布时间】:2013-07-24 19:52:58
【问题描述】:

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

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

这是我的案例研究。我正在写一个 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 能够理解并遵循我的代码,那么“普遍接受”的编写方式是什么?

【问题讨论】:

  • 我明白你的要求,但作为旁注,你上面的代码是不合法的,不会编译。 Scout 和 Tank 是同一类型 Unit 的不同构造函数 - 您不能为每个 constructor 定义单独的实例。您打算使用 DataKinds 扩展吗?
  • 你也可以阅读我对类似问题的回答here

标签: haskell types functional-programming typeclass


【解决方案1】:

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

看看this blog post。您使用 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 }
           ]

在您的示例中,您需要在某处编码坦克有装甲但侦察兵没有。显而易见的可能性是使用额外的信息来增加 Unit 类型,例如 Maybe Armor 字段或特殊权力列表......不一定有明确的方法。

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

【讨论】:

【解决方案2】:

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

【讨论】:

    【解决方案3】:

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

    这迫使你稍微多写一点你的接口,因为任何需要在Units 之上多态的函数必须最终只使用在你的抽象类型类基础上定义的函数。就我而言,它还重要的是让我可以使用 Unit-type 类型作为幻像类型中的类型参数。

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

    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 类型,这将无法在编译时检查。

    【讨论】:

    • 只是要指出:幻像类型/GADT 不需要这种编程风格 - 您可以通过我在我的回答。
    • 确实如此。我没有很好地与直接风格进行比较,只是副产品风格。在我自己的应用程序中,直接样式感觉很烦人,所以我参加了一个课程。我们会看看我是否保留它。
    猜你喜欢
    • 2016-02-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-09-06
    • 1970-01-01
    • 2011-07-11
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多