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
我还制作了Plane
的{{1}}和Sphere
个实例。
我正在尝试将球体和平面存储在同一个列表中,但它不起作用。我理解它不应该有效,因为Shape
和Sphere
是两种不同的类型,但它们都是Plane
的实例,所以不应该有效吗?如何在列表中存储形状和平面?
Shape
答案 0 :(得分:53)
这个问题代表了面向对象和功能性思维之间的转折点。有时即使是复杂的Haskeller仍处于这种心理转变中,他们的设计常常属于托马斯答案中提到的existential typeclass模式。
这个问题的一个功能性解决方案涉及将类型类转换为数据类型(通常一旦完成,对类型类的需求就消失了):
data Shape = Shape {
intersect :: Ray -> Maybe Point,
surfaceNormal :: Point -> Vector Double
}
现在您可以轻松构建Shape
的列表,因为它是单态类型。由于Haskell不支持向下转换,因此删除Plane
和Sphere
之间的代表性区别不会丢失任何信息。特定数据类型成为构造Shape
s:
plane :: Point -> Vector Double -> Shape
sphere :: Point -> Double -> Shape
如果您无法捕获有关Shape
数据类型中的形状的所有信息,您可以使用代数数据类型枚举案例,如Thomas建议的那样。但如果可能,我会建议不要这样做;相反,试着找到你需要的形状的基本特征,而不是仅仅列出例子。
答案 1 :(得分:25)
你正在寻找一个异构列表,大多数Haskellers并不特别喜欢,即使他们在第一次学习Haskell时问自己同样的问题。
你写道:
shapes :: (Shape t) => [t]
这表示列表的类型为t
,所有这些都是相同的,恰好是一个Shape(形状相同!)。换句话说 - 不,它不应该如何运作。
处理它的两种常用方法(首先是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语言进行一些扩展,即ExplicitForAll
和ExistentialQuantification
。我们必须明确定义showIt
(使用模式匹配来解构AnyShow
类型),因为您不能对使用存在量化的数据类型使用字段名称。
还有更多的解决方案(希望另一个答案会使用Data.Dynamic
- 如果没有人会这样做而您感兴趣的话可以阅读它并随时发布任何阅读产生的问题。