Haskell类型类检查

5

我希望使用Haskell来实现一个游戏,并希望使用类型类的系统来实现道具系统。它将像这样工作:

data Wood = Wood Int

instance Item Wood where
  image a = "wood.png"
  displayName a = "Wood"

instance Flammable Wood where
  burn (Wood health) | health' <= 0 = Ash
                     | otherwise    = Wood health'
      where health' = health - 100

其中Item和Flammable类大致如下:

class Item a where
  image :: a -> String
  displayName :: a -> String

class Flammable a where
  burn :: (Item b) => a -> b

为了达成这个目的,我需要一种检测值是否为类型类实例的方法。 Data.Data模块提供了类似的功能,这让我相信这是可能的。

2
我不确定你所做的事情是否符合 Haskell 类型模型。一个值是类型类的实例应该在静态上可证明。 - millimoose
4
数值不能是类型类的实例。类型是类型类的实例。 - n. m.
4
请查看这个常见问题解答条目,该条目讨论了在制作角色扮演游戏时是否应为每种怪物定义一种类型并为它们定义一种类型类的问题。 - ehird
2个回答

8

在这里,类型类可能不是正确的选择。考虑使用普通的代数数据类型,例如:

data Item = Wood Int | Ash

image Wood = "wood.png"
image Ash = ...

displayName Wood = "Wood"
displayName Ash = "Ash"

burn :: Item -> Maybe Item
burn (Wood health) | health' <= 0 = Just Ash
                   | otherwise    = Just (Wood health')
  where health' = health - 100
burn _ = Nothing -- Not flammable

如果这样做让添加新项目变得太困难,你可以改为在数据类型本身中对操作进行编码。
data Item = Item { image :: String, displayName :: String, burn :: Maybe Item }

ash :: Item
ash = Item { image = "...", displayName = "Ash", burn :: Nothing }

wood :: Int -> Item
wood health = Item { image = "wood.png", displayName = "Wood", burn = Just burned }
    where burned | health' <= 0 = ash
                 | otherwise    = wood health'
          health' = health - 100

然而,这使得添加新功能更加困难。同时执行两个操作的问题被称为表达式问题。有一位名叫Ralf Lämmel博士的讲师在Channel 9上做了一个精彩的演讲,更深入地解释了这个问题并讨论了各种非解决方案,如果你有时间,非常值得观看。
有一些解决方法,但它们比我所展示的这两种设计要复杂得多,因此我建议如果符合您的需求,则使用其中之一,并不必担心表达式问题,除非必须解决。

第一个可能有效(尽管我认为没有Data.Data的黑客不行),但这会使添加更多项变得更加困难(而且有超过200个)。第二个根本行不通,因为有些物品是不易燃的。我的意思是,你可能会有可燃的“木材”,可以放入库存中,还有可燃的“僵尸”,可以战斗。这两个物品共享一个共同特点,但也各自具有另一个特点,而另一个物品则不具备该特点。 - adrusi
这就是我想使用类型类的原因,但障碍在于我必须能够测试玩家正在交互的物品是否易燃,这意味着测试它是否是类型类的成员。 - adrusi
@adrusi:某物是否是类型类的成员是编译时属性,因此它并不适合你所尝试做的事情。根据你所描述的,第二种方法似乎应该可行,尽管你可能需要进行一些修改。这取决于某些类型物品之间有哪些共同属性。例如,如果任何物品都可以具有任何组合的属性,则可以为每个属性只设置一个Maybe字段,就像我为易燃物品设置的那个字段一样。没有该属性的物品将在那里只有一个Nothing - hammar

5
这里有个问题:
burn :: (Item b) => a -> b

这意味着burn的结果值必须是多态的。它必须能够填充Item的任何实例的任何空缺。
现在,很明显你正在尝试编写类似于这样的代码(在具有接口和子类化的虚构面向对象语言中):
Interface Item {
  String getImage();
  String getDisplayName();
}

Interface Flammable {
  Item burn();
}

在这种代码中,你在说burn会产生某个物品,没有任何关于它是什么类型的保证。这就是“对于所有”和“存在”之间的区别。在Haskell代码中想要表达的是“存在”,但实际上表达的是“对于所有”。
现在,如果你确信要进行“存在”功能,可以考虑使用存在类型。但请注意。如果你计划编写如下代码:
if (foo instanceof Flammable) {
  ...
}

那么,您几乎肯定做错了,并会遇到很多痛苦和折磨。相反,考虑使用Hammar提出的替代方案。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接