不同类型的清单是什么?

45
data Plane = Plane { point :: Point, normal :: Vector Double }
data Sphere = Sphere { center :: Point, radius :: Double }

class Shape s where
    intersect :: s -> Ray -> Maybe Point
    surfaceNormal :: s -> Point -> Vector Double

我已经把 PlaneSphere 都设为 Shape 的实例。

我尝试把球体和平面存储在同一个列表中,但是它不起作用。我知道这不应该起作用,因为 SpherePlane 是两种不同的类型,但它们都是 Shape 的实例,所以它不应该起作用吗?那我该如何将形状和平面存储在列表中呢?

shapes :: (Shape t) => [t]
shapes = [ Sphere { center = Point [0, 0, 0], radius = 2.0 },
         Plane { point = Point [1, 2, 1], normal = 3 |> [0.5, 0.6, 0.2] }
         ]

1
http://www.haskell.org/haskellwiki/Heterogenous_collections - Cat Plus Plus
2
我知道异构集合,但这是我想避免的东西。 - Arlen
2个回答

62

这个问题代表了面向对象和函数式思维之间的一个转折点。即使是经验丰富的Haskellers有时仍处于这种心理转变中,他们的设计经常陷入Thomas答案中提到的存在类型类模式。

解决这个问题的函数式方案涉及将类型类实现为一个数据类型(通常一旦完成这个步骤,就不再需要类型类):

data Shape = Shape {
    intersect :: Ray -> Maybe Point,
    surfaceNormal :: Point -> Vector Double
}

现在你可以轻松构建一个 Shape 的列表,因为它是单态类型。由于 Haskell 不支持向下转换,因此通过删除 PlaneSphere 之间的表示区别,不会丢失任何信息。这些特定的数据类型变成了构建 Shape 的函数:

plane :: Point -> Vector Double -> Shape
sphere :: Point -> Double -> Shape
如果 Shape 数据类型无法捕获您需要了解的有关形状的所有内容,则可以使用代数数据类型枚举不同情况,就像 Thomas 建议的那样。但是,如果可能的话,我建议避免这样做;相反,尝试找到您所需要的形状的基本特征,而不仅仅是列出一些示例。

6
这段话的意思是,我们不应该把类型类(Type classes)看作面向对象的继承或接口,而是应该把它们看作子类型。它们只是一组类型可以支持的函数。 - Ankur
如果您需要对某个物体进行类型检查,例如飞机或球体,那么您会建议使用ADT风格吗? - CMCDragonkai
1
@CMCDragonkai,我可能会创建一个名为“数据球”的对象,它具有球体所需的属性,创建一个名为“数据平面”的对象,它具有平面所需的属性,然后编写注入函数,例如toShape :: Sphere -> Shape。也许可以将其放在一个名为ToShape的类中(作为事后想法)。 - luqui
那怎么样才能让你把球体和平面放在同一个列表中呢? - CMCDragonkai
@CMCDragonkai,这不会。但是它可以让你将它们对应的“形状”放入一个列表中,这是一样的。请参阅http://lukepalmer.wordpress.com/2010/01/24/haskell-antipattern-existential-typeclass/ 了解原因。 - luqui
显示剩余2条评论

27
您正在寻找一个异构列表,这是大多数Haskellers不太喜欢的,即使他们在学习Haskell时也曾问过自己这个问题。
您可以这样写:
shapes :: (Shape t) => [t]

这段话的意思是,列表的类型为 t,所有元素都是相同的且恰好是形状(相同的形状!)。换句话说 - 不,它不应该像你想象的那样工作。
处理这个问题有两种常见的方法(首先是 Haskell 98 的方法,其次是我不建议的更高级的方法): 使用新类型 来静态地联合感兴趣的子类型:
data Foo = F deriving Show
data Bar = B deriving Show

data Contain = CFoo Foo | CBar Bar deriving Show
stuffExplicit :: [Contain]
stuffExplicit = [CFoo F, CBar B]

main = print stuffExplicit

这很好,因为它很直观,你不会失去关于列表中包含的内容的任何信息。你可以确定第一个元素是一个Foo,第二个元素是一个Bar。缺点是,你必须显式地添加每个组件类型,通过创建一个新的Contain类型构造函数。如果这是不可取的,请继续阅读。 使用存在类型:另一种解决方案涉及失去有关元素的信息 - 你只保留,比如说,元素属于特定类的知识。因此,你只能在列表元素上使用来自该类的操作。例如,下面的代码只会记住元素属于Show类,因此你只能对元素使用多态的Show函数:
data AnyShow = forall s. Show s => AS s

showIt (AS s) = show s

stuffAnyShow :: [AnyShow]
stuffAnyShow = [AS F, AS B]

main = print (map showIt stuffAnyShow)

这需要对Haskell语言进行一些扩展,即ExplicitForAllExistentialQuantification。我们必须显式地定义showIt(使用模式匹配来解构AnyShow类型),因为你不能为使用存在量化的数据类型使用字段名称。
还有更多的解决方案(希望另一个答案会使用Data.Dynamic - 如果没有人使用并且您感兴趣,则可以阅读相关文献并随时发布任何阅读所产生的问题)。

第一种方法是我尝试的第一件事,但效果不太好。我不确定是否喜欢第二种方法!我也想避免使用扩展程序。 - Arlen

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